@gobing-ai/ts-ai-runner 0.2.7 → 0.2.9

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,50 @@
1
+ import { BunSyncProcessExecutor } from '@gobing-ai/ts-runtime';
2
+ export function buildIdentityPreamble(ctx) {
3
+ const sections = [
4
+ `You are agent \`${ctx.agentId}\` (${ctx.agentType}) in workspace \`${ctx.workspace}\`.`,
5
+ ];
6
+ if (ctx.taskId !== undefined) {
7
+ const title = ctx.taskTitle === undefined ? '' : ` — ${ctx.taskTitle}`;
8
+ sections.push(`Your current task: #${ctx.taskId}${title}.`);
9
+ }
10
+ if (ctx.purpose !== undefined && ctx.purpose.trim() !== '')
11
+ sections.push(`Your purpose: ${ctx.purpose}.`);
12
+ if (ctx.systemPrompt !== undefined && ctx.systemPrompt.trim() !== '')
13
+ sections.push(ctx.systemPrompt);
14
+ if ((ctx.peers?.length ?? 0) > 0) {
15
+ sections.push([
16
+ 'Peer agents in this workspace:',
17
+ ...(ctx.peers ?? []).map((peer) => `- \`${peer.id}\` (${peer.type}) — ${peer.purpose ?? '(no purpose set)'}`),
18
+ ].join('\n'));
19
+ }
20
+ sections.push([
21
+ 'Communication:',
22
+ '- Send a message with: spur message send --to <agent-id> "<message>"',
23
+ '- Reply to a message with: spur message reply <msg-id> "<response>"',
24
+ '- Read pending messages with: spur message inbox',
25
+ ].join('\n'));
26
+ if (ctx.gitBranch !== undefined) {
27
+ sections.push(['Git context:', `branch: ${ctx.gitBranch}`, `dirty: ${ctx.gitDirty === true ? 'true' : 'false'}`].join('\n'));
28
+ }
29
+ if ((ctx.guardrails?.length ?? 0) > 0) {
30
+ sections.push(['Guardrails:', ...(ctx.guardrails ?? []).map((guardrail) => `- ${guardrail}`)].join('\n'));
31
+ }
32
+ return `${sections.join('\n\n')}\n`;
33
+ }
34
+ export function getGitContext(workspacePath, executor = new BunSyncProcessExecutor()) {
35
+ const git = Bun.which('git');
36
+ if (git === null)
37
+ return null;
38
+ const branch = runGit(executor, git, ['-C', workspacePath, 'branch', '--show-current']);
39
+ if (branch === null || branch === '')
40
+ return null;
41
+ const status = runGit(executor, git, ['-C', workspacePath, 'status', '--porcelain']);
42
+ const dirtyCount = status === null || status === '' ? 0 : status.split('\n').filter(Boolean).length;
43
+ return ['Git context:', `branch: ${branch}`, `dirty: ${dirtyCount === 0 ? 'false' : `${dirtyCount} files`}`].join('\n');
44
+ }
45
+ function runGit(executor, command, args) {
46
+ const result = executor.runSync({ command, args, rejectOnError: false, forceBuffered: true });
47
+ if (result.exitCode !== 0)
48
+ return null;
49
+ return result.stdout.trim();
50
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  export * from './agent-detector';
2
+ export * from './agent-spec';
2
3
  export * from './agents/shims';
3
4
  export * from './ai-runner';
4
5
  export * from './doctor-runner';
6
+ export * from './identity';
7
+ export * from './message-service';
5
8
  export * from './slash-command';
9
+ export * from './team-agent-process';
10
+ export * from './team-orchestrator';
6
11
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC"}
package/dist/index.js CHANGED
@@ -1,5 +1,10 @@
1
1
  export * from './agent-detector.js';
2
+ export * from './agent-spec.js';
2
3
  export * from './agents/shims.js';
3
4
  export * from './ai-runner.js';
4
5
  export * from './doctor-runner.js';
6
+ export * from './identity.js';
7
+ export * from './message-service.js';
5
8
  export * from './slash-command.js';
9
+ export * from './team-agent-process.js';
10
+ export * from './team-orchestrator.js';
@@ -0,0 +1,13 @@
1
+ import type { InboxMessage, InboxMessageDao } from '@gobing-ai/ts-db/inbox';
2
+ export declare class MessageService {
3
+ private readonly dao;
4
+ constructor(dao: InboxMessageDao);
5
+ enqueue(fromId: string | null, toId: string, body: string, inReplyTo?: string): Promise<string>;
6
+ drain(toId: string): Promise<InboxMessage[]>;
7
+ deliver(msgId: string): Promise<void>;
8
+ fail(msgId: string, error: string): Promise<void>;
9
+ inbox(toId: string, limit?: number, offset?: number): Promise<InboxMessage[]>;
10
+ countPending(toId: string): Promise<number>;
11
+ static formatMessage(msg: InboxMessage): string;
12
+ }
13
+ //# sourceMappingURL=message-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message-service.d.ts","sourceRoot":"","sources":["../src/message-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE5E,qBAAa,cAAc;IACX,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,eAAe;IAEjD,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAI/F,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAI5C,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIrC,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIjD,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAI7E,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAI3C,MAAM,CAAC,aAAa,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM;CAGlD"}
@@ -0,0 +1,27 @@
1
+ export class MessageService {
2
+ dao;
3
+ constructor(dao) {
4
+ this.dao = dao;
5
+ }
6
+ enqueue(fromId, toId, body, inReplyTo) {
7
+ return this.dao.enqueue(fromId, toId, body, inReplyTo);
8
+ }
9
+ drain(toId) {
10
+ return this.dao.drainPending(toId);
11
+ }
12
+ deliver(msgId) {
13
+ return this.dao.markDelivered(msgId);
14
+ }
15
+ fail(msgId, error) {
16
+ return this.dao.markFailed(msgId, error);
17
+ }
18
+ inbox(toId, limit, offset) {
19
+ return this.dao.inbox(toId, limit, offset);
20
+ }
21
+ countPending(toId) {
22
+ return this.dao.countPending(toId);
23
+ }
24
+ static formatMessage(msg) {
25
+ return `[task from=${msg.fromId ?? 'operator'} id=${msg.id}] ${msg.body}`;
26
+ }
27
+ }
@@ -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.7",
3
+ "version": "0.2.9",
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.7"
50
+ "@gobing-ai/ts-db": "^0.2.9",
51
+ "@gobing-ai/ts-runtime": "^0.2.9",
52
+ "@gobing-ai/ts-utils": "^0.2.9"
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
+ }