@gobing-ai/ts-ai-runner 0.2.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,36 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { type PipeProcessSpawner } from '@gobing-ai/ts-runtime';
3
+ import type { AgentSpec } from './agent-spec';
4
+ export interface AgentProcessOptions {
5
+ spec: AgentSpec;
6
+ command: string[];
7
+ env?: Record<string, string>;
8
+ cwd?: string;
9
+ processSpawner?: PipeProcessSpawner;
10
+ }
11
+ type ProcessStatus = 'running' | 'stopped' | 'errored';
12
+ export declare class TeamAgentProcess {
13
+ readonly agentId: string;
14
+ readonly identityPreamble: string;
15
+ private readonly command;
16
+ private readonly env;
17
+ private readonly cwd;
18
+ private readonly processSpawner;
19
+ private subprocess;
20
+ private status;
21
+ private exitCode;
22
+ private readonly subscribers;
23
+ constructor(options: AgentProcessOptions);
24
+ start(): Promise<void>;
25
+ stop(): Promise<void>;
26
+ send(message: string): Promise<{
27
+ ok: boolean;
28
+ }>;
29
+ subscribe(callback: (data: Buffer) => void): () => void;
30
+ getStatus(): ProcessStatus;
31
+ getPid(): number | null;
32
+ getExitCode(): number | null;
33
+ private pipe;
34
+ }
35
+ export {};
36
+ //# sourceMappingURL=team-agent-process.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"team-agent-process.d.ts","sourceRoot":"","sources":["../src/team-agent-process.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAA2C,KAAK,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AACzG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAG9C,MAAM,WAAW,mBAAmB;IAChC,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,kBAAkB,CAAC;CACvC;AAED,KAAK,aAAa,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;AAEvD,qBAAa,gBAAgB;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAW;IACnC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAqC;IACzD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAqB;IACzC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAqB;IACpD,OAAO,CAAC,UAAU,CAA4B;IAC9C,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqC;gBAErD,OAAO,EAAE,mBAAmB;IAgBlC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAoBtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAwBrB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAA;KAAE,CAAC;IAWrD,SAAS,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI;IAOvD,SAAS,IAAI,aAAa;IAI1B,MAAM,IAAI,MAAM,GAAG,IAAI;IAIvB,WAAW,IAAI,MAAM,GAAG,IAAI;YAId,IAAI;CAerB"}
@@ -0,0 +1,125 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { BunPipeProcessSpawner } from '@gobing-ai/ts-runtime';
3
+ import { buildIdentityPreamble } from './identity.js';
4
+ export class TeamAgentProcess {
5
+ agentId;
6
+ identityPreamble;
7
+ command;
8
+ env;
9
+ cwd;
10
+ processSpawner;
11
+ subprocess = null;
12
+ status = 'stopped';
13
+ exitCode = null;
14
+ subscribers = new Set();
15
+ constructor(options) {
16
+ this.agentId = options.spec.id;
17
+ this.identityPreamble = buildIdentityPreamble({
18
+ agentId: options.spec.id,
19
+ agentType: options.spec.type,
20
+ workspace: options.spec.workspace,
21
+ purpose: options.spec.purpose,
22
+ systemPrompt: typeof options.spec.config.systemPrompt === 'string' ? options.spec.config.systemPrompt : undefined,
23
+ });
24
+ this.command = options.command;
25
+ this.env = options.env;
26
+ this.cwd = options.cwd ?? options.spec.workspace;
27
+ this.processSpawner = options.processSpawner ?? new BunPipeProcessSpawner();
28
+ }
29
+ async start() {
30
+ if (this.status === 'running')
31
+ return;
32
+ const [command, ...args] = this.command;
33
+ if (command === undefined)
34
+ throw new Error(`${this.agentId}: command must not be empty`);
35
+ this.subprocess = this.processSpawner.spawn({
36
+ command,
37
+ args,
38
+ ...(this.cwd !== undefined ? { cwd: this.cwd } : {}),
39
+ ...(this.env !== undefined ? { env: this.env } : {}),
40
+ });
41
+ this.status = 'running';
42
+ this.exitCode = null;
43
+ if (this.subprocess.stdout !== null)
44
+ this.pipe(this.subprocess.stdout);
45
+ if (this.subprocess.stderr !== null)
46
+ this.pipe(this.subprocess.stderr);
47
+ void this.subprocess.exited.then((code) => {
48
+ this.exitCode = code;
49
+ if (this.status === 'running')
50
+ this.status = code === 0 ? 'stopped' : 'errored';
51
+ });
52
+ }
53
+ async stop() {
54
+ const process = this.subprocess;
55
+ if (process === null) {
56
+ this.status = 'stopped';
57
+ return;
58
+ }
59
+ try {
60
+ process.endStdin();
61
+ }
62
+ catch {
63
+ // Process may have already closed stdin.
64
+ }
65
+ process.kill('SIGTERM');
66
+ const timeout = Bun.sleep(5000).then(() => 'timeout');
67
+ const result = await Promise.race([process.exited, timeout]);
68
+ if (result === 'timeout') {
69
+ process.kill('SIGKILL');
70
+ this.exitCode = await process.exited;
71
+ }
72
+ else {
73
+ this.exitCode = result;
74
+ }
75
+ this.status = 'stopped';
76
+ this.subprocess = null;
77
+ }
78
+ async send(message) {
79
+ if (this.status !== 'running' || this.subprocess === null)
80
+ return { ok: false };
81
+ try {
82
+ this.subprocess.writeStdin(`${message}\n`);
83
+ return { ok: true };
84
+ }
85
+ catch {
86
+ this.status = 'errored';
87
+ return { ok: false };
88
+ }
89
+ }
90
+ subscribe(callback) {
91
+ this.subscribers.add(callback);
92
+ return () => {
93
+ this.subscribers.delete(callback);
94
+ };
95
+ }
96
+ getStatus() {
97
+ return this.status;
98
+ }
99
+ getPid() {
100
+ return this.subprocess?.pid ?? null;
101
+ }
102
+ getExitCode() {
103
+ return this.exitCode;
104
+ }
105
+ async pipe(stream) {
106
+ const reader = stream.getReader();
107
+ try {
108
+ while (true) {
109
+ const chunk = await reader.read();
110
+ if (chunk.done)
111
+ break;
112
+ const buffer = Buffer.from(chunk.value);
113
+ for (const subscriber of this.subscribers)
114
+ subscriber(buffer);
115
+ }
116
+ }
117
+ catch {
118
+ if (this.status === 'running')
119
+ this.status = 'errored';
120
+ }
121
+ finally {
122
+ reader.releaseLock();
123
+ }
124
+ }
125
+ }
@@ -0,0 +1,36 @@
1
+ import type { AgentSpec } from './agent-spec';
2
+ import { MessageService } from './message-service';
3
+ import { TeamAgentProcess } from './team-agent-process';
4
+ type TeamEvent = 'agent.started' | 'agent.stopped' | 'message.sent';
5
+ type TeamListener = (payload: unknown) => void;
6
+ type AgentProcessFactory = (options: ConstructorParameters<typeof TeamAgentProcess>[0]) => TeamAgentProcess;
7
+ export interface TeamOrchestratorOptions {
8
+ processFactory?: AgentProcessFactory;
9
+ }
10
+ export declare class TeamOrchestrator {
11
+ private readonly configDir;
12
+ private readonly messageService;
13
+ private specs;
14
+ private readonly running;
15
+ private readonly listeners;
16
+ private readonly processFactory;
17
+ constructor(configDir: string, messageService: MessageService, options?: TeamOrchestratorOptions);
18
+ loadSpecs(): AgentSpec[];
19
+ getSpec(id: string): AgentSpec | undefined;
20
+ startAgent(id: string): Promise<TeamAgentProcess>;
21
+ stopAgent(id: string): Promise<void>;
22
+ restartAgent(id: string): Promise<TeamAgentProcess>;
23
+ sendMessage(fromId: string | null, toId: string, body: string, inReplyTo?: string): Promise<string>;
24
+ getRunningAgents(): Map<string, TeamAgentProcess>;
25
+ getAgentStatus(id: string): 'running' | 'stopped' | 'errored' | 'unknown';
26
+ getPeerSpecs(workspace: string, excludeId?: string): AgentSpec[];
27
+ stopAll(): Promise<void>;
28
+ on(event: TeamEvent, listener: TeamListener): () => void;
29
+ private requireSpec;
30
+ private requireAgentName;
31
+ private injectPendingMessages;
32
+ private flushInbox;
33
+ private emit;
34
+ }
35
+ export {};
36
+ //# sourceMappingURL=team-orchestrator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"team-orchestrator.d.ts","sourceRoot":"","sources":["../src/team-orchestrator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAI9C,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAExD,KAAK,SAAS,GAAG,eAAe,GAAG,eAAe,GAAG,cAAc,CAAC;AACpE,KAAK,YAAY,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;AAC/C,KAAK,mBAAmB,GAAG,CAAC,OAAO,EAAE,qBAAqB,CAAC,OAAO,gBAAgB,CAAC,CAAC,CAAC,CAAC,KAAK,gBAAgB,CAAC;AAE5G,MAAM,WAAW,uBAAuB;IACpC,cAAc,CAAC,EAAE,mBAAmB,CAAC;CACxC;AAED,qBAAa,gBAAgB;IAOrB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,cAAc;IAPnC,OAAO,CAAC,KAAK,CAAmB;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuC;IAC/D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA2C;IACrE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAsB;gBAGhC,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,cAAc,EAC/C,OAAO,GAAE,uBAA4B;IAKzC,SAAS,IAAI,SAAS,EAAE;IAKxB,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;IAKpC,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAkCjD,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQpC,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAKnD,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAQzG,gBAAgB,IAAI,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAIjD,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS;IAMzE,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,EAAE;IAK1D,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAI9B,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,YAAY,GAAG,MAAM,IAAI;IAOxD,OAAO,CAAC,WAAW;IAMnB,OAAO,CAAC,gBAAgB;IAKxB,OAAO,CAAC,qBAAqB;YAIf,UAAU;IASxB,OAAO,CAAC,IAAI;CAGf"}
@@ -0,0 +1,131 @@
1
+ import { loadAgentSpecs } from './agent-spec.js';
2
+ import { getAgentShim, isAgentName } from './agents/shims.js';
3
+ import { buildIdentityPreamble } from './identity.js';
4
+ import { MessageService } from './message-service.js';
5
+ import { TeamAgentProcess } from './team-agent-process.js';
6
+ export class TeamOrchestrator {
7
+ configDir;
8
+ messageService;
9
+ specs = [];
10
+ running = new Map();
11
+ listeners = new Map();
12
+ processFactory;
13
+ constructor(configDir, messageService, options = {}) {
14
+ this.configDir = configDir;
15
+ this.messageService = messageService;
16
+ this.processFactory = options.processFactory ?? ((processOptions) => new TeamAgentProcess(processOptions));
17
+ }
18
+ loadSpecs() {
19
+ this.specs = loadAgentSpecs(this.configDir);
20
+ return [...this.specs];
21
+ }
22
+ getSpec(id) {
23
+ if (this.specs.length === 0)
24
+ this.loadSpecs();
25
+ return this.specs.find((spec) => spec.id === id);
26
+ }
27
+ async startAgent(id) {
28
+ const spec = this.requireSpec(id);
29
+ const agentType = this.requireAgentName(spec.type);
30
+ const peers = this.getPeerSpecs(spec.workspace, spec.id).map((peer) => ({
31
+ id: peer.id,
32
+ type: peer.type,
33
+ purpose: peer.purpose,
34
+ }));
35
+ const identityPreamble = buildIdentityPreamble({
36
+ agentId: spec.id,
37
+ agentType: spec.type,
38
+ workspace: spec.workspace,
39
+ purpose: spec.purpose,
40
+ systemPrompt: typeof spec.config.systemPrompt === 'string' ? spec.config.systemPrompt : undefined,
41
+ peers,
42
+ });
43
+ const command = getAgentShim(agentType).getPromptCommand({
44
+ input: identityPreamble,
45
+ purpose: spec.purpose,
46
+ systemPrompt: typeof spec.config.systemPrompt === 'string' ? spec.config.systemPrompt : undefined,
47
+ peers,
48
+ });
49
+ const process = this.processFactory({
50
+ spec,
51
+ command: [command.command, ...command.args],
52
+ cwd: spec.workspace,
53
+ });
54
+ await process.start();
55
+ this.running.set(id, process);
56
+ await this.injectPendingMessages(process);
57
+ this.emit('agent.started', { id });
58
+ return process;
59
+ }
60
+ async stopAgent(id) {
61
+ const process = this.running.get(id);
62
+ if (process === undefined)
63
+ return;
64
+ await process.stop();
65
+ this.running.delete(id);
66
+ this.emit('agent.stopped', { id });
67
+ }
68
+ async restartAgent(id) {
69
+ await this.stopAgent(id);
70
+ return this.startAgent(id);
71
+ }
72
+ async sendMessage(fromId, toId, body, inReplyTo) {
73
+ const msgId = await this.messageService.enqueue(fromId, toId, body, inReplyTo);
74
+ const process = this.running.get(toId);
75
+ if (process !== undefined)
76
+ await this.flushInbox(process, 'live stdin injection failed');
77
+ this.emit('message.sent', { id: msgId, fromId, toId });
78
+ return msgId;
79
+ }
80
+ getRunningAgents() {
81
+ return new Map(this.running);
82
+ }
83
+ getAgentStatus(id) {
84
+ const process = this.running.get(id);
85
+ if (process === undefined)
86
+ return this.getSpec(id) === undefined ? 'unknown' : 'stopped';
87
+ return process.getStatus();
88
+ }
89
+ getPeerSpecs(workspace, excludeId) {
90
+ if (this.specs.length === 0)
91
+ this.loadSpecs();
92
+ return this.specs.filter((spec) => spec.workspace === workspace && spec.id !== excludeId);
93
+ }
94
+ async stopAll() {
95
+ await Promise.all([...this.running.keys()].map((id) => this.stopAgent(id)));
96
+ }
97
+ on(event, listener) {
98
+ const listeners = this.listeners.get(event) ?? new Set();
99
+ listeners.add(listener);
100
+ this.listeners.set(event, listeners);
101
+ return () => listeners.delete(listener);
102
+ }
103
+ requireSpec(id) {
104
+ const spec = this.getSpec(id);
105
+ if (spec === undefined)
106
+ throw new Error(`Agent spec not found: ${id}`);
107
+ return spec;
108
+ }
109
+ requireAgentName(type) {
110
+ if (!isAgentName(type))
111
+ throw new Error(`Unsupported agent type: ${type}`);
112
+ return type;
113
+ }
114
+ injectPendingMessages(process) {
115
+ return this.flushInbox(process, 'startup stdin injection failed');
116
+ }
117
+ async flushInbox(process, failLabel) {
118
+ const messages = await this.messageService.drain(process.agentId);
119
+ for (const message of messages) {
120
+ const result = await process.send(MessageService.formatMessage(message));
121
+ if (result.ok)
122
+ await this.messageService.deliver(message.id);
123
+ else
124
+ await this.messageService.fail(message.id, failLabel);
125
+ }
126
+ }
127
+ emit(event, payload) {
128
+ for (const listener of this.listeners.get(event) ?? [])
129
+ listener(payload);
130
+ }
131
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/ts-ai-runner",
3
- "version": "0.2.8",
3
+ "version": "0.3.0",
4
4
  "description": "@gobing-ai/ts-ai-runner — Coding-agent shims, detection, doctor checks, and prompt execution.",
5
5
  "keywords": [
6
6
  "typescript",
@@ -47,7 +47,9 @@
47
47
  "release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-ai-runner-v<version> && git push --tags' && exit 1"
48
48
  },
49
49
  "dependencies": {
50
- "@gobing-ai/ts-runtime": "^0.2.8"
50
+ "@gobing-ai/ts-db": "^0.3.0",
51
+ "@gobing-ai/ts-runtime": "^0.3.0",
52
+ "@gobing-ai/ts-utils": "^0.3.0"
51
53
  },
52
54
  "devDependencies": {
53
55
  "@types/bun": "1.3.14"
@@ -43,19 +43,11 @@ export class AgentDetector {
43
43
 
44
44
  /** Probe one agent by name. */
45
45
  async detectOne(agent: string): Promise<DetectedAgent> {
46
- if (!isAgentName(agent)) {
47
- return { name: agent, installed: false, version: null, channels: [], error: `Unknown agent: ${agent}` };
48
- }
46
+ if (!isAgentName(agent)) return unavailable(agent, `Unknown agent: ${agent}`);
49
47
  try {
50
48
  return this.parseResult(agent, await this.runner.runVersionCommand(agent, { timeout: this.timeout }));
51
49
  } catch (error) {
52
- return {
53
- name: agent,
54
- installed: false,
55
- version: null,
56
- channels: [],
57
- error: error instanceof Error ? error.message : String(error),
58
- };
50
+ return unavailable(agent, error instanceof Error ? error.message : String(error));
59
51
  }
60
52
  }
61
53
 
@@ -64,41 +56,17 @@ export class AgentDetector {
64
56
  const output = `${result.stdout}\n${result.stderr}`.trim();
65
57
  const lower = output.toLowerCase();
66
58
  if (lower.includes('command not found') || lower.includes('enoent') || lower.includes('not recognized')) {
67
- return {
68
- name: agent,
69
- installed: false,
70
- version: null,
71
- channels: [],
72
- error: `${command}: command not found`,
73
- };
59
+ return unavailable(agent, `${command}: command not found`);
74
60
  }
75
61
  if (result.signal !== undefined || result.exitCode === null) {
76
- return {
77
- name: agent,
78
- installed: false,
79
- version: null,
80
- channels: [],
81
- error: result.signal ?? 'Process timed out',
82
- };
62
+ return unavailable(agent, result.signal ?? 'Process timed out');
83
63
  }
84
64
  if (result.exitCode !== 0) {
85
- return {
86
- name: agent,
87
- installed: false,
88
- version: null,
89
- channels: [],
90
- error: `Non-zero exit code ${result.exitCode}: ${result.stderr.slice(0, 200)}`,
91
- };
65
+ return unavailable(agent, `Non-zero exit code ${result.exitCode}: ${result.stderr.slice(0, 200)}`);
92
66
  }
93
67
  const match = VERSION_PATTERN.exec(output);
94
68
  if (match?.groups?.version === undefined) {
95
- return {
96
- name: agent,
97
- installed: false,
98
- version: null,
99
- channels: [],
100
- error: 'Could not parse version output',
101
- };
69
+ return unavailable(agent, 'Could not parse version output');
102
70
  }
103
71
  return {
104
72
  name: agent,
@@ -109,3 +77,8 @@ export class AgentDetector {
109
77
  };
110
78
  }
111
79
  }
80
+
81
+ /** Build an "unavailable" detection result for an agent with the given error. */
82
+ function unavailable(name: AgentName | string, error: string): DetectedAgent {
83
+ return { name, installed: false, version: null, channels: [], error };
84
+ }
@@ -0,0 +1,128 @@
1
+ import { basename, join } from 'node:path';
2
+ import { NodeSyncFileSystem, parseYamlObject, type SyncFileSystem, stringifyYamlObject } from '@gobing-ai/ts-runtime';
3
+
4
+ export interface AgentSpec {
5
+ id: string;
6
+ name: string;
7
+ type: string;
8
+ workspace: string;
9
+ purpose: string;
10
+ tags: string[];
11
+ config: Record<string, unknown>;
12
+ autoStart?: boolean;
13
+ }
14
+
15
+ export class ValueError extends Error {
16
+ constructor(message: string) {
17
+ super(message);
18
+ this.name = 'ValueError';
19
+ }
20
+ }
21
+
22
+ export function validateAgentId(id: string): string {
23
+ if (!/^[a-z][a-z0-9_-]{1,63}$/.test(id)) {
24
+ throw new ValueError(`Invalid agent id "${id}": expected 2-64 chars, lowercase alphanumeric, "_" or "-"`);
25
+ }
26
+ return id;
27
+ }
28
+
29
+ export function loadAgentSpecs(configDir: string, fs: SyncFileSystem = new NodeSyncFileSystem()): AgentSpec[] {
30
+ const entries = safeReadDir(configDir, fs)
31
+ .filter((entry) => entry.endsWith('.yaml') || entry.endsWith('.yml'))
32
+ .sort();
33
+ const specs = entries.map((entry) => parseAgentSpec(fs.readFile(join(configDir, entry)), entry));
34
+ const seen = new Set<string>();
35
+ for (const spec of specs) {
36
+ validateAgentId(spec.id);
37
+ if (seen.has(spec.id)) throw new ValueError(`Duplicate agent id "${spec.id}" in ${configDir}`);
38
+ seen.add(spec.id);
39
+ }
40
+ return specs;
41
+ }
42
+
43
+ export async function saveAgentSpec(
44
+ spec: AgentSpec,
45
+ configDir: string,
46
+ fs: SyncFileSystem = new NodeSyncFileSystem(),
47
+ ): Promise<void> {
48
+ validateAgentId(spec.id);
49
+ fs.mkdir(configDir);
50
+ fs.writeFile(join(configDir, `${spec.id}.yaml`), serializeAgentSpec(spec));
51
+ }
52
+
53
+ export async function deleteAgentSpec(
54
+ id: string,
55
+ configDir: string,
56
+ fs: SyncFileSystem = new NodeSyncFileSystem(),
57
+ ): Promise<void> {
58
+ validateAgentId(id);
59
+ fs.unlink(join(configDir, `${id}.yaml`));
60
+ }
61
+
62
+ function safeReadDir(configDir: string, fs: SyncFileSystem = new NodeSyncFileSystem()): string[] {
63
+ try {
64
+ return fs.readDir(configDir);
65
+ } catch (error) {
66
+ if (error instanceof Error && 'code' in error && (error as NodeJS.ErrnoException).code === 'ENOENT') {
67
+ return [];
68
+ }
69
+ throw error;
70
+ }
71
+ }
72
+
73
+ function parseAgentSpec(source: string, fileName: string): AgentSpec {
74
+ const parsed = parseYamlObject(source);
75
+ const spec = {
76
+ id: requireString(parsed, 'id', fileName),
77
+ name: requireString(parsed, 'name', fileName),
78
+ type: requireString(parsed, 'type', fileName),
79
+ workspace: requireString(parsed, 'workspace', fileName),
80
+ purpose: requireString(parsed, 'purpose', fileName),
81
+ tags: requireStringArray(parsed, 'tags', fileName),
82
+ config: requireRecord(parsed, 'config', fileName),
83
+ ...(typeof parsed.autoStart === 'boolean' ? { autoStart: parsed.autoStart } : {}),
84
+ };
85
+ return spec;
86
+ }
87
+
88
+ function serializeAgentSpec(spec: AgentSpec): string {
89
+ const record: Record<string, unknown> = {
90
+ id: spec.id,
91
+ name: spec.name,
92
+ type: spec.type,
93
+ workspace: spec.workspace,
94
+ purpose: spec.purpose,
95
+ tags: spec.tags,
96
+ ...(spec.autoStart !== undefined ? { autoStart: spec.autoStart } : {}),
97
+ config: spec.config,
98
+ };
99
+ return stringifyYamlObject(record);
100
+ }
101
+
102
+ function requireString(source: Record<string, unknown>, key: keyof AgentSpec, fileName: string): string {
103
+ const value = source[key];
104
+ if (typeof value !== 'string' || value.trim() === '') {
105
+ throw new ValueError(`${basename(fileName)}: "${key}" must be a non-empty string`);
106
+ }
107
+ return value;
108
+ }
109
+
110
+ function requireStringArray(source: Record<string, unknown>, key: keyof AgentSpec, fileName: string): string[] {
111
+ const value = source[key];
112
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) {
113
+ throw new ValueError(`${basename(fileName)}: "${key}" must be a string array`);
114
+ }
115
+ return value;
116
+ }
117
+
118
+ function requireRecord(
119
+ source: Record<string, unknown>,
120
+ key: keyof AgentSpec,
121
+ fileName: string,
122
+ ): Record<string, unknown> {
123
+ const value = source[key];
124
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
125
+ throw new ValueError(`${basename(fileName)}: "${key}" must be an object`);
126
+ }
127
+ return value as Record<string, unknown>;
128
+ }
@@ -22,6 +22,16 @@ export interface PromptOptions {
22
22
  model?: string;
23
23
  /** Output mode passed through to the agent CLI. */
24
24
  mode?: OutputMode;
25
+ /** Team-mode purpose included in the identity preamble. */
26
+ purpose?: string;
27
+ /** Caller-defined prompt tags. */
28
+ tags?: string[];
29
+ /** Additional system prompt rendered in the identity preamble. */
30
+ systemPrompt?: string;
31
+ /** Current task identifier included in the identity preamble. */
32
+ taskId?: string;
33
+ /** Peer agents included in the identity preamble. */
34
+ peers?: Array<{ id: string; type: string; purpose?: string }>;
25
35
  }
26
36
 
27
37
  /** Pure command builder for one coding-agent CLI. */
package/src/ai-runner.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { NodeProcessExecutor, type ProcessExecutor, type ProcessResult } from '@gobing-ai/ts-runtime';
2
2
  import { type AgentName, getAgentShim, type PromptOptions } from './agents/shims';
3
+ import { buildIdentityPreamble } from './identity';
3
4
 
4
5
  /** Result returned by every AI runner dispatch method. */
5
6
  export interface AgentRunResult {
@@ -61,7 +62,14 @@ export class AiRunner {
61
62
  promptOptions: PromptOptions,
62
63
  options: AgentRunOptions = {},
63
64
  ): Promise<AgentRunResult> {
64
- return this.invoke(agent, 'prompt', getAgentShim(agent).getPromptCommand(promptOptions), options, false);
65
+ const enrichedPromptOptions = this.withIdentityPreamble(agent, promptOptions, options);
66
+ return this.invoke(
67
+ agent,
68
+ 'prompt',
69
+ getAgentShim(agent).getPromptCommand(enrichedPromptOptions),
70
+ options,
71
+ false,
72
+ );
65
73
  }
66
74
 
67
75
  /** Run an agent authentication command, or return null when unsupported. */
@@ -94,4 +102,33 @@ export class AiRunner {
94
102
  durationMs: result.durationMs,
95
103
  };
96
104
  }
105
+
106
+ private withIdentityPreamble(
107
+ agent: AgentName,
108
+ promptOptions: PromptOptions,
109
+ options: AgentRunOptions,
110
+ ): PromptOptions {
111
+ if (!hasIdentityOptions(promptOptions)) return promptOptions;
112
+ const workspace = options.cwd ?? this.defaultCwd ?? process.cwd();
113
+ const preamble = buildIdentityPreamble({
114
+ agentId: agent,
115
+ agentType: agent,
116
+ workspace,
117
+ purpose: promptOptions.purpose,
118
+ systemPrompt: promptOptions.systemPrompt,
119
+ taskId: promptOptions.taskId,
120
+ peers: promptOptions.peers,
121
+ });
122
+ const input = promptOptions.input === undefined ? preamble : `${preamble}\n${promptOptions.input}`;
123
+ return { ...promptOptions, input };
124
+ }
125
+ }
126
+
127
+ function hasIdentityOptions(options: PromptOptions): boolean {
128
+ return (
129
+ options.purpose !== undefined ||
130
+ options.systemPrompt !== undefined ||
131
+ options.taskId !== undefined ||
132
+ (options.peers !== undefined && options.peers.length > 0)
133
+ );
97
134
  }