Enable canonical versioned Go module consumption

Why: Allow consumers to download an immutable module revision without nested source checkouts or local path replacements.

Evidence: SELF package tests and the prior canonical-path rejection are recorded in the Dev dependency-workspace task card; remote consumption is replayed against the committed candidate.

Impact: Change the module identity and its Go imports while preserving API behavior. Existing historical submodule revisions remain buildable.

Refs: Versioned dependencies U1/U3; feature/dependency-workspace
This commit is contained in:
2026-10-13 15:11:37 +00:00
parent 5773e74086
commit f32d431784
10 changed files with 832 additions and 1 deletions
+2
View File
@@ -1,3 +1,5 @@
*.test
*.out
dist/
node_modules/
*.tgz
+1 -1
View File
@@ -1,3 +1,3 @@
module agent-system-core
module git-ssh.u1680832.nyat.app/weichen/Agent-System-Core.git
go 1.27
+29
View File
@@ -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"
}
}
}
}
+25
View File
@@ -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"
}
}
+672
View File
@@ -0,0 +1,672 @@
export type ID = string;
export interface Project {
id: ID;
owner?: ID;
name: string;
createdAt: string;
updatedAt: string;
settings?: Record<string, unknown>;
}
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<P = unknown> {
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.<key>}}、{{steps.<id>.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 {};
+4
View File
@@ -0,0 +1,4 @@
// Agent-System-Core 契约的 TypeScript 镜像。
// 线格式契约以 Go 类型(camelCase JSON 标签)与 testdata/golden/*.json 黄金样本为准;
// 本文件手工镜像同一契约,前端按未知 type/字段静默忽略(前向兼容原则)。
export {};
+24
View File
@@ -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 })
}
+46
View File
@@ -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 })
}
})
+16
View File
@@ -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.
+13
View File
@@ -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"]
}