diff --git a/.gitignore b/.gitignore index ead874c..f176516 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ *.test *.out dist/ +node_modules/ +*.tgz diff --git a/go.mod b/go.mod index f495345..dc346cc 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module agent-system-core +module git-ssh.u1680832.nyat.app/weichen/Agent-System-Core.git go 1.27 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d98ddd6 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,29 @@ +{ + "name": "@agent-system/contracts", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@agent-system/contracts", + "version": "0.1.0", + "devDependencies": { + "typescript": "5.9.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..40434fa --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "@agent-system/contracts", + "version": "0.1.0", + "type": "module", + "description": "Agent-System public TypeScript wire contracts", + "files": ["package"], + "types": "./package/index.d.ts", + "exports": { + ".": { + "types": "./package/index.d.ts", + "import": "./package/index.js" + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "scripts": { + "build": "node scripts/build-package.mjs", + "check:package": "node scripts/build-package.mjs --check", + "typecheck": "tsc -p tsconfig.package.json --noEmit", + "test": "npm run check:package && node --test scripts/test-package.mjs" + }, + "devDependencies": { + "typescript": "5.9.3" + } +} diff --git a/package/index.d.ts b/package/index.d.ts new file mode 100644 index 0000000..21f42cb --- /dev/null +++ b/package/index.d.ts @@ -0,0 +1,672 @@ +export type ID = string; +export interface Project { + id: ID; + owner?: ID; + name: string; + createdAt: string; + updatedAt: string; + settings?: Record; +} +export interface Conversation { + id: ID; + projectId: ID; + owner?: ID; + title: string; + profileId?: ID; + pinned?: boolean; + archived?: boolean; + allowCrossRead?: boolean; + crossPrivate?: boolean; + permissionMode?: 'default' | 'plan' | 'auto_edit' | ''; + sessionNote?: string; + createdAt: string; + updatedAt: string; +} +export type Role = 'user' | 'assistant' | 'system'; +export type PartType = 'text' | 'thinking' | 'tool_call' | 'tool_result' | 'artifact_ref' | 'hitl' | 'plan' | 'error' | 'card' | 'check'; +export interface Part { + type: PartType; + text?: string; + toolCall?: { + id: string; + name: string; + arguments?: string; + durationMs?: number; + }; + toolResult?: { + callId: string; + status: 'ok' | 'error'; + summary?: string; + truncated?: boolean; + }; + artifact?: { + artifactId: ID; + name: string; + kind?: string; + }; + hitl?: { + cardId: ID; + question: string; + resolved: boolean; + answer?: string; + }; + plan?: PlanSnapshot; + card?: { + kind: string; + data: UINode[]; + cardId?: ID; + }; + error?: { + code: string; + message: string; + }; + check?: { + passed: boolean; + reason?: string; + }; + process?: boolean; + durationMs?: number; +} +export interface Message { + id: ID; + conversationId: ID; + runId?: ID; + role: Role; + parts: Part[]; + createdAt: string; + excluded?: boolean; + variantGroup?: string; + variantIndex?: number; +} +export type RunKind = 'conversation' | 'subagent' | 'tool'; +export type RunStatus = 'pending' | 'running' | 'waiting_hitl' | 'completed' | 'failed' | 'cancelled' | 'interrupted'; +export type RunPhase = 'thinking' | 'acting' | 'responding' | 'finalizing' | 'done'; +export interface Usage { + promptTokens: number; + completionTokens: number; + totalTokens: number; + durationMs: number; + toolCalls: number; + model?: string; + lastPromptTokens?: number; + lastCompletionTokens?: number; + cacheReadTokens?: number; + cacheWriteTokens?: number; +} +export interface Run { + id: ID; + parentRunId?: ID; + conversationId: ID; + projectId: ID; + owner?: ID; + kind: RunKind; + profileId?: ID; + status: RunStatus; + phase?: RunPhase; + agentName?: string; + createdAt: string; + endedAt?: string; + usage?: Usage; + error?: { + code: string; + message: string; + }; +} +export interface Artifact { + id: ID; + projectId: ID; + owner?: ID; + name: string; + kind: string; + mime?: string; + size: number; + sourceRun?: ID; + createdAt: string; + updatedAt: string; +} +export interface PlanSnapshot { + planId: ID; + title: string; + steps: { + id: string; + text: string; + status: 'pending' | 'in_progress' | 'done' | 'skipped'; + agentRun?: ID; + }[]; +} +export interface TodoList { + conversationId: ID; + items: { + id: string; + text: string; + status: 'pending' | 'in_progress' | 'done'; + }[]; +} +export type StreamEventType = 'message.delta' | 'thinking.delta' | 'tool.call.delta' | 'tool.call' | 'tool.output' | 'tool.result' | 'run.phase' | 'run.check' | 'hitl.request' | 'hitl.resolved' | 'plan.update' | 'todo.update' | 'agent.spawn' | 'agent.progress' | 'agent.result' | 'artifact.created' | 'artifact.updated' | 'run.completed' | 'run.failed' | 'error' | 'card.created' | 'card.updated' | 'card.status' | 'card.resolved' | 'surface.updated'; +export interface StreamEvent

{ + seq: number; + runId?: ID; + conversationId: ID; + ts: string; + v: number; + type: StreamEventType; + payload?: P; +} +export interface MessageDeltaPayload { + index: number; + text: string; +} +export interface ToolCallDeltaPayload { + index: number; + callId?: string; + name?: string; + delta: string; +} +export interface ToolOutputPayload { + callId: string; + stream: 'stdout' | 'stderr'; + text: string; +} +export interface ToolResultPayload { + callId: string; + status: string; + summary?: string; + truncated?: boolean; + durationMs?: number; +} +export interface AgentSpawnPayload { + agentRun: ID; + name: string; + task: string; +} +export interface AgentProgressPayload { + agentRun: ID; + note?: string; + phase?: RunPhase; +} +export interface AgentResultPayload { + agentRun: ID; + status: RunStatus; + summary?: string; +} +export interface HITLResolvedPayload { + cardId: ID; + answer?: string; + kind: 'choice' | 'free' | 'timeout' | 'cancelled'; +} +export interface RunCompletedPayload { + usage: Usage; + context?: ContextObservation; +} +interface ContextSection { + name: string; + estTokens: number; +} +export interface ContextObservation { + items: string[]; + truncations?: string[]; + estChars: number; + sections?: ContextSection[]; + prefixHash?: string; +} +export interface RunCheckPayload { + passed: boolean; + reason?: string; +} +export interface RunFailedPayload { + code: string; + message: string; +} +export type CardKind = 'progress' | 'confirm' | 'compare' | 'review-list' | 'result' | 'error' | 'status'; +export type CardStatus = 'active' | 'action_required' | 'done' | 'resolved' | 'failed' | 'dismissed'; +export interface ActivityCard { + id: ID; + projectId: ID; + conversationId?: ID; + sourceRunId?: ID; + anchorMessageId?: ID; + kind: CardKind; + title: string; + status: CardStatus; + tone?: string; + body?: UINode[]; + actions?: { + id: string; + label: string; + style?: 'primary' | 'default' | 'danger'; + command?: string; + }[]; + pinned?: boolean; + createdAt: string; + updatedAt: string; +} +export interface CardPayload { + card: ActivityCard; +} +export interface CardStatusPayload { + cardId: ID; + status: CardStatus; + tone?: string; + note?: string; +} +export interface CardResolvedPayload { + cardId: ID; + actionId?: string; + answer?: string; +} +export type HITLKind = 'choice' | 'input' | 'confirm' | 'form'; +export interface HITLChoice { + id: string; + label: string; + description: string; + impact: string; + recommended?: boolean; +} +export interface HITLCard { + id: ID; + runId: ID; + conversationId: ID; + question: string; + kind: HITLKind; + choices?: HITLChoice[]; + allowFreeInput: boolean; + freeInputHint?: string; + timeoutSec?: number | null; + form?: UINode[]; + createdAt: string; + resolved: boolean; + anchorMessageId?: ID; + blocked?: boolean; + blockReason?: string; + detail?: string; +} +export interface HITLResponse { + cardId: ID; + choiceId?: string; + freeText?: string; + formData?: unknown; +} +export type UINodeType = 'section' | 'list' | 'kv' | 'form' | 'markdown' | 'doc-ref' | 'actions' | 'chart-lite' | 'avatar' | 'progress' | 'compare' | 'tree' | 'table' | 'steps' | 'tabs' | 'timeline'; +export interface UINode { + type: UINodeType; + /** Surface 层节点级标注(Generative UI U2):整页唯一稳定 id 与事实来源标识,旧路径不填。 */ + id?: string; + source?: string[]; + title?: string; + items?: string[]; + kv?: [string, string][]; + fields?: UIField[]; + markdown?: string; + docId?: ID; + actions?: { + id: string; + label: string; + style?: 'primary' | 'default' | 'danger'; + }[]; + chart?: { + kind: 'bar' | 'line'; + labels: string[]; + series: number[]; + }; + children?: UINode[]; + avatar?: { + name?: string; + sub?: string; + tone?: string; + stack?: { + name?: string; + sub?: string; + tone?: string; + }[]; + }; + progress?: { + label?: string; + value: number; + tone?: string; + }; + compare?: { + left: { + title?: string; + tone?: string; + nodes?: UINode[]; + }; + right: { + title?: string; + tone?: string; + nodes?: UINode[]; + }; + }; + tree?: { + nodes: { + id: string; + label: string; + tone?: string; + badge?: string; + count?: number; + selected?: boolean; + children?: unknown[]; + }[]; + }; + table?: { + columns: { + key: string; + label: string; + }[]; + rows: { + id?: string; + cells: [string, string][]; + actions?: UINode['actions']; + }[]; + }; + steps?: { + current: number; + steps: { + label: string; + status?: 'pending' | 'active' | 'done' | 'failed'; + tone?: string; + }[]; + }; + tabs?: { + active?: string; + tabs: { + id: string; + label: string; + nodes?: UINode[]; + }[]; + }; + timeline?: { + items: { + time?: string; + title: string; + tone?: string; + markdown?: string; + ref?: string; + }[]; + }; +} +export interface UIField { + key: string; + label: string; + type: 'text' | 'textarea' | 'number' | 'boolean' | 'select'; + options?: string[]; + required?: boolean; + placeholder?: string; + help?: string; + default?: string; +} +export interface PromptTrigger { + kind: 'intent' | 'command' | 'attachment' | 'manual'; + keywords?: string[]; + command?: string; +} +export interface PromptModule { + id: string; + layer: 'base' | 'env' | 'task' | 'agent' | 'tool' | 'aux' | 'style'; + version: number; + title: string; + description?: string; + priority?: number; + triggers?: PromptTrigger[]; + requiresAttachments?: boolean; + body: string; + outputContract?: string; + source?: 'builtin' | 'plugin' | 'user'; + owner?: string; + basedOnVersion?: number; + drifted?: boolean; + disabled?: boolean; + /** 子代理 profile。只对 agent.sub.* 有意义;空字段表示沿用内置。 */ + tools?: string[]; + model?: string; + maxTurns?: number; +} +export interface PromptBlock { + moduleId: string; + version: number; + layer: string; + body: string; +} +export interface PromptHitRecord { + moduleId: string; + layer: string; + reason: string; + injected: boolean; +} +export interface AgentProfile { + id: string; + owner?: string; + name: string; + description?: string; + builtin?: boolean; + source?: string; + systemPrompt: string; + /** 提示词库模块引用(有序;空 = 默认 base+env 全集)。 */ + promptModules?: string[]; + model: { + chat?: string; + lightweight?: string; + embedding?: string; + rerank?: string; + }; + tools: { + allow: string[]; + deny?: string[]; + confirm?: string[]; + }; + context: { + maxHistoryMessages: number; + summarizeBeyond?: boolean; + memoryBudgetPct?: number; + knowledgeBudgetPct?: number; + maxToolResultChars?: number; + }; + hitl: { + askOnAmbiguity: boolean; + timeoutSec?: number | null; + }; + budget: { + maxTokens?: number | null; + maxToolCalls?: number | null; + maxTurns?: number | null; + }; +} +export interface SettingDesc { + key: string; + group: string; + label: string; + description?: string; + type: 'string' | 'number' | 'boolean' | 'enum' | 'secret'; + default?: unknown; + options?: string[]; + effect: 'hot' | 'new-run' | 'restart'; + secret?: boolean; + /** P2 受管:system = 系统受管项,仅系统管理面可写(用户/项目层拒绝)。 */ + scope?: 'system'; +} +export interface PluginManifest { + id: string; + name: string; + version: string; + apiVersion: string; + requires?: { + network?: string[]; + }; + provides: { + tools?: string[]; + workflows?: string[]; + intents?: string[]; + prompts?: string[]; + panels?: string[]; + cards?: string[]; + settings?: string[]; + commands?: string[]; + workspaces?: string[]; + skills?: string[]; + mcpServers?: string[]; + knowledge?: string[]; + parsers?: string[]; + agents?: string[]; + channels?: string[]; + pages?: string[]; + }; + scope?: { + tenants?: string[]; + projects?: string[]; + }; + backend?: { + mode?: 'inproc' | 'sidecar'; + command?: string; + args?: string[]; + schemaVersion?: number; + }; +} +export interface PluginPanel { + id: string; + title: string; + icon?: string; + body: UINode[]; + refreshTool?: string; +} +/** 模板占位符:{{input}}、{{slots.}}、{{steps..output}}。 */ +export interface WorkflowDef { + id: string; + title: string; + description?: string; + intentId?: string; + /** 整条工作流的 critic 重试上限。 */ + maxRetries?: number; + steps: WorkflowStep[]; +} +export interface StepCritic { + criteria: string; + /** 本步重试上限;缺省 1 次。 */ + maxRetries?: number; +} +export interface WorkflowStep { + id: string; + title: string; + kind: 'prompt' | 'tool' | 'hitl' | 'artifact' | 'card' | 'script'; + prompt?: string; + /** prompt 步骤的子代理 profile,缺省 general。 */ + agent?: string; + tool?: string; + args?: unknown; + hitl?: { + question: string; + kind: HITLKind; + choices?: HITLChoice[]; + allowFreeInput: boolean; + freeInputHint?: string; + }; + artifact?: string; + card?: { + kind: CardKind; + title: string; + tone?: string; + body?: UINode[]; + actions?: { + id: string; + label: string; + style?: 'primary' | 'default' | 'danger'; + command?: string; + }[]; + }; + /** script 步骤在插件目录内的相对路径。 */ + script?: string; + critic?: StepCritic; +} +export interface PluginWorkspace { + id: string; + title: string; + icon?: string; + shortcut?: string; + badgeTool?: string; + body: UINode[]; + refreshTool?: string; +} +export interface PluginPageDecl { + id: string; + title: string; + module: string; +} +export interface ParsedDocument { + meta: { + filename: string; + mime?: string; + parser: string; + confidence?: number; + crossCheck?: { + with: string; + diffBlocks: number; + verdict: 'agree' | 'differ'; + }; + parsedAt: string; + }; + blocks: { + type: 'heading' | 'para' | 'list' | 'table' | 'code' | 'image-ref'; + level?: number; + text?: string; + items?: string[]; + rows?: string[][]; + ref?: string; + confidence?: number; + }[]; + warnings?: string[]; +} +export interface RetrievalHit { + doc: { + id: ID; + owner?: ID; + projectId?: ID; + source: string; + title: string; + ref?: string; + chunk: string; + createdAt: string; + }; + score: number; + kind: 'fulltext' | 'vector' | 'plugin'; +} +export interface UploadInfo { + artifactId: ID; + filename: string; + mime?: string; + size: number; + parsed?: ParsedDocument; +} +export interface SurfaceCompat { + schemaVersion: number; + catalogVersion: string; + pluginVersion?: string; +} +export interface SurfaceIdentity { + surfaceId: ID; + owner: string; + projectId?: ID; + conversationId?: ID; + sourceRunId?: ID; + pluginId: string; +} +export interface SurfaceSpec { + title: string; + nodes: UINode[]; +} +export interface SurfaceSnapshot { + identity: SurfaceIdentity; + compat: SurfaceCompat; + revision: number; + baseRevision?: number; + contextVersion?: string; + activationEpoch: number; + spec: SurfaceSpec; + semanticText?: string; +} +export interface SurfaceMeta { + surfaceId: ID; + title: string; + pluginId: string; + revision: number; + conversationId?: ID; + updatedAt: string; +} +export {}; diff --git a/package/index.js b/package/index.js new file mode 100644 index 0000000..a615d8c --- /dev/null +++ b/package/index.js @@ -0,0 +1,4 @@ +// Agent-System-Core 契约的 TypeScript 镜像。 +// 线格式契约以 Go 类型(camelCase JSON 标签)与 testdata/golden/*.json 黄金样本为准; +// 本文件手工镜像同一契约,前端按未知 type/字段静默忽略(前向兼容原则)。 +export {}; diff --git a/scripts/build-package.mjs b/scripts/build-package.mjs new file mode 100644 index 0000000..637729b --- /dev/null +++ b/scripts/build-package.mjs @@ -0,0 +1,24 @@ +// Explicit generation only: authenticated Git acquisition must never run package scripts. +import { execFileSync } from 'node:child_process' +import { mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const root = dirname(dirname(fileURLToPath(import.meta.url))) +const check = process.argv.includes('--check') +const output = check ? mkdtempSync(join(tmpdir(), 'agent-contracts-build-')) : join(root, 'package') +try { + mkdirSync(output, { recursive: true }) + execFileSync(process.execPath, [join(root, 'node_modules/typescript/bin/tsc'), '-p', join(root, 'tsconfig.package.json'), '--outDir', output], { cwd: root, stdio: 'inherit' }) + for (const extension of ['js', 'd.ts']) renameSync(join(output, `types.${extension}`), join(output, `index.${extension}`)) + if (check) { + for (const file of ['index.js', 'index.d.ts']) { + if (!readFileSync(join(root, 'package', file)).equals(readFileSync(join(output, file)))) { + throw new Error(`package/${file} is stale; run npm run build before committing the provider`) + } + } + } +} finally { + if (check) rmSync(output, { recursive: true, force: true }) +} diff --git a/scripts/test-package.mjs b/scripts/test-package.mjs new file mode 100644 index 0000000..4975def --- /dev/null +++ b/scripts/test-package.mjs @@ -0,0 +1,46 @@ +import assert from 'node:assert/strict' +import { execFileSync, spawnSync } from 'node:child_process' +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { test } from 'node:test' + +const root = dirname(dirname(fileURLToPath(import.meta.url))) + +test('packed contracts compile in a consumer without the Core checkout', () => { + const consumer = mkdtempSync(join(tmpdir(), 'agent-contracts-consumer-')) + try { + const pack = spawnSync('npm', ['pack', '--json', '--ignore-scripts', '--pack-destination', consumer], { + cwd: root, encoding: 'utf8', + }) + assert.equal(pack.status, 0, `Core must expose an independently packable contracts package:\n${pack.stderr}`) + const [artifact] = JSON.parse(pack.stdout) + assert.equal(artifact.name, '@agent-system/contracts') + assert.ok(artifact.files.some(file => file.path === 'package/index.d.ts'), 'declarations must be packed') + assert.ok(artifact.files.some(file => file.path === 'package/index.js'), 'ESM entry must be packed') + assert.ok(!artifact.files.some(file => file.path.endsWith('.go')), 'Go implementation is outside the npm package') + + writeFileSync(join(consumer, 'package.json'), JSON.stringify({ private: true, type: 'module' })) + writeFileSync(join(consumer, 'consumer.ts'), ` +import type { ActivityCard, Message, SurfaceSnapshot } from '@agent-system/contracts' +const card: ActivityCard = { id: 'card', projectId: 'project', conversationId: 'conversation', kind: 'progress', status: 'active', title: 'Working', createdAt: '2026-10-13T00:00:00Z', updatedAt: '2026-10-13T00:00:00Z' } +const message: Message = { id: 'message', conversationId: 'conversation', role: 'assistant', parts: [{ type: 'text', text: card.title }], createdAt: card.createdAt } +type Surface = SurfaceSnapshot +// @ts-expect-error unknown message roles are not part of the wire contract +const invalidRole: Message['role'] = 'operator' +export { message, invalidRole }; export type { Surface } +`) + const tsc = [join(root, 'node_modules/typescript/bin/tsc'), '--noEmit', '--strict', '--skipLibCheck', '--module', 'NodeNext', '--moduleResolution', 'NodeNext', 'consumer.ts'] + const missing = spawnSync(process.execPath, tsc, { cwd: consumer, encoding: 'utf8' }) + assert.notEqual(missing.status, 0, 'the consumer must not resolve Core types before package installation') + assert.match(missing.stdout, /TS2307.*@agent-system\/contracts/) + execFileSync('npm', ['install', '--ignore-scripts', '--no-audit', '--no-fund', '--offline', '--package-lock=false', join(consumer, artifact.filename)], { cwd: consumer, stdio: 'pipe' }) + execFileSync(process.execPath, tsc, { cwd: consumer, stdio: 'inherit' }) + execFileSync(process.execPath, ['--input-type=module', '-e', "import * as contracts from '@agent-system/contracts'; if (Object.keys(contracts).length) throw Error('contracts must remain type-only')"], { cwd: consumer, stdio: 'pipe' }) + const declaration = readFileSync(join(consumer, 'node_modules/@agent-system/contracts/package/index.d.ts'), 'utf8') + assert.match(declaration, /export interface SurfaceSnapshot/) + } finally { + rmSync(consumer, { recursive: true, force: true }) + } +}) diff --git a/ts/README.md b/ts/README.md new file mode 100644 index 0000000..eb2f2f2 --- /dev/null +++ b/ts/README.md @@ -0,0 +1,16 @@ +# TypeScript contracts package + +`ts/types.ts` is the sole TypeScript contract source. `npm run build` emits the +tracked `package/index.d.ts` and type-only ESM entry for `@agent-system/contracts`. +Consumers import types from the package instead of copying this source. + +Run `npm ci --ignore-scripts`, remove acquisition credentials from the execution +environment, then run `npm run build && npm test`. `npm test` detects stale +generated output and installs an `npm pack --ignore-scripts` artifact into an +isolated consumer that has no Core checkout. + +The generated package is committed with its source so fixed Git dependencies work +with lifecycle scripts disabled. The package has no `prepare`, `install`, or +`prepack` hook. Pin a reachable provider commit in each consumer's manifest and +lockfile. This package version is metadata; no Registry upload or release tag is +created by these commands. diff --git a/tsconfig.package.json b/tsconfig.package.json new file mode 100644 index 0000000..e13a135 --- /dev/null +++ b/tsconfig.package.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "declaration": true, + "rootDir": "ts", + "outDir": "package", + "skipLibCheck": true + }, + "files": ["ts/types.ts"] +}