@gobing-ai/ts-ai-runner 0.3.1 → 0.3.2
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.
- package/README.md +336 -32
- package/dist/agent-detector.d.ts +1 -0
- package/dist/agent-detector.d.ts.map +1 -1
- package/dist/agent-detector.js +13 -5
- package/dist/agent-spec.d.ts +6 -0
- package/dist/agent-spec.d.ts.map +1 -1
- package/dist/agent-spec.js +12 -8
- package/dist/ai-runner.d.ts +18 -2
- package/dist/ai-runner.d.ts.map +1 -1
- package/dist/ai-runner.js +52 -6
- package/dist/doctor-runner.d.ts +7 -0
- package/dist/doctor-runner.d.ts.map +1 -1
- package/dist/doctor-runner.js +69 -15
- package/dist/events.d.ts +38 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +0 -0
- package/dist/identity.d.ts +3 -0
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +2 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/messages.d.ts +4 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +4 -0
- package/dist/team-agent-process.d.ts +12 -4
- package/dist/team-agent-process.d.ts.map +1 -1
- package/dist/team-agent-process.js +31 -19
- package/dist/team-orchestrator.d.ts +13 -8
- package/dist/team-orchestrator.d.ts.map +1 -1
- package/dist/team-orchestrator.js +32 -25
- package/package.json +4 -4
- package/src/agent-detector.ts +14 -5
- package/src/agent-spec.ts +20 -8
- package/src/ai-runner.ts +75 -13
- package/src/doctor-runner.ts +77 -16
- package/src/events.ts +25 -0
- package/src/identity.ts +3 -0
- package/src/index.ts +2 -1
- package/src/messages.ts +6 -0
- package/src/team-agent-process.ts +36 -21
- package/src/team-orchestrator.ts +36 -25
- package/dist/message-service.d.ts +0 -13
- package/dist/message-service.d.ts.map +0 -1
- package/dist/message-service.js +0 -27
- package/src/message-service.ts +0 -33
package/README.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# @gobing-ai/ts-ai-runner
|
|
2
2
|
|
|
3
|
-
Coding-agent command shims, installation detection, doctor checks, slash-command translation, and
|
|
3
|
+
Coding-agent command shims, installation detection, doctor checks, slash-command translation, identity preamble construction, and team-mode orchestration for downstream CLIs.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @gobing-ai/ts-ai-runner
|
|
9
|
+
```
|
|
4
10
|
|
|
5
11
|
## What It Provides
|
|
6
12
|
|
|
@@ -8,26 +14,175 @@ Coding-agent command shims, installation detection, doctor checks, slash-command
|
|
|
8
14
|
|
|
9
15
|
| Export | Purpose |
|
|
10
16
|
|--------|---------|
|
|
11
|
-
| `AiRunner` | Runs help, version, auth, and
|
|
17
|
+
| `AiRunner` | Runs help, version, auth, prompt, and slash commands through a pluggable process executor; can also build a prompt command without executing it. Emits typed events via optional `EventBus<AgentEvents>`. |
|
|
12
18
|
| `AgentDetector` | Probes supported agent CLIs and parses version output |
|
|
13
19
|
| `DoctorRunner` | Combines installation and authentication checks into a usability report |
|
|
14
20
|
| `getAgentShim()` | Returns the pure command builder for one supported agent |
|
|
15
21
|
| `translateSlashCommand()` | Converts Claude-style `/plugin:command` inputs to each agent's dialect |
|
|
16
|
-
| `
|
|
17
|
-
| `
|
|
22
|
+
| `isClaudeStyleSlashCommand()` | Tests whether input matches the `/plugin:command` pattern |
|
|
23
|
+
| `buildIdentityPreamble()` / `getGitContext()` | Builds team-mode identity, communication context, and git metadata for prompts |
|
|
24
|
+
| `loadAgentSpecs()` / `saveAgentSpec()` / `deleteAgentSpec()` | Persist agent definitions as YAML-compatible config |
|
|
25
|
+
| `validateAgentId()` | Enforces agent ID format rules |
|
|
18
26
|
| `MessageService` | Thin service wrapper around `@gobing-ai/ts-db/inbox` |
|
|
19
27
|
| `TeamAgentProcess` | Manages a long-running agent subprocess with pipe-mode stdin/stdout |
|
|
20
|
-
| `TeamOrchestrator` | Loads specs, starts/stops agents,
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
| `TeamOrchestrator` | Loads specs, starts/stops agents, routes durable/live messages, and emits lifecycle events |
|
|
29
|
+
| `AgentEvents` / `AiRunnerProcessEvents` | Typed event maps for agent and process-level observability |
|
|
30
|
+
| `AGENT_SHIMS` / `TIER1_PRIORITY` / `TIER2_AGENTS` / `DISPLAY_ORDER` | Agent registry constants |
|
|
31
|
+
| `isAgentName()` | Type guard for supported agent identifiers |
|
|
32
|
+
|
|
33
|
+
Supported agent identifiers: `claude`, `codex`, `gemini`, `pi`, `opencode` (tier 1), `antigravity`, `openclaw` (tier 2).
|
|
34
|
+
|
|
35
|
+
## Architecture
|
|
36
|
+
|
|
37
|
+
```mermaid
|
|
38
|
+
graph TB
|
|
39
|
+
subgraph Shared ["Shared components"]
|
|
40
|
+
AgentShim["AgentShim<br/>(pure command builder per agent)"]
|
|
41
|
+
Identity["buildIdentityPreamble()<br/>(team-mode identity context)"]
|
|
42
|
+
EventBus["EventBus<AgentEvents><br/>(opt-in observability)"]
|
|
43
|
+
ProcessExecutor["ProcessExecutor<br/>(injectable, ts-runtime)"]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
subgraph OneShot ["One-shot mode"]
|
|
47
|
+
AiRunner["AiRunner<br/>runPromptCommand / runSlashCommand<br/>buildPromptCommand / runHelpCommand<br/>runVersionCommand / runAuthCommand"]
|
|
48
|
+
AgentDetector["AgentDetector<br/>(probes CLIs via AiRunner)"]
|
|
49
|
+
DoctorRunner["DoctorRunner<br/>(install + auth health checks)"]
|
|
50
|
+
|
|
51
|
+
AiRunner -->|"resolves command via"| AgentShim
|
|
52
|
+
AiRunner -->|"delegates execution to"| ProcessExecutor
|
|
53
|
+
AgentDetector -->|"probes versions via"| AiRunner
|
|
54
|
+
DoctorRunner -->|"probes install + auth via"| AiRunner
|
|
55
|
+
AiRunner -.->|"emits events"| EventBus
|
|
56
|
+
AiRunner -.->|"enriches prompt"| Identity
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
subgraph TeamMode ["Team mode (building blocks)"]
|
|
60
|
+
TeamOrchestrator["TeamOrchestrator<br/>loadSpecs / startAgent / stopAgent<br/>sendMessage / getAgentStatus / stopAll"]
|
|
61
|
+
AgentSpec["AgentSpec<br/>(YAML config)<br/>load / save / delete"]
|
|
62
|
+
TeamAgentProcess["TeamAgentProcess<br/>(pipe-mode subprocess)<br/>start / stop / send / subscribe"]
|
|
63
|
+
MessageService["MessageService<br/>(InboxMessageDao wrapper)<br/>enqueue / drain / deliver / fail"]
|
|
64
|
+
|
|
65
|
+
TeamOrchestrator -->|"loads"| AgentSpec
|
|
66
|
+
TeamOrchestrator -->|"creates + manages"| TeamAgentProcess
|
|
67
|
+
TeamOrchestrator -->|"routes through"| MessageService
|
|
68
|
+
TeamOrchestrator -->|"resolves command via"| AgentShim
|
|
69
|
+
TeamOrchestrator -.->|"emits events"| EventBus
|
|
70
|
+
TeamOrchestrator -.->|"builds preamble"| Identity
|
|
71
|
+
TeamAgentProcess -->|"spawns via"| ProcessExecutor
|
|
72
|
+
end
|
|
73
|
+
```
|
|
23
74
|
|
|
24
|
-
|
|
75
|
+
### One-shot prompt flow
|
|
76
|
+
```mermaid
|
|
77
|
+
sequenceDiagram
|
|
78
|
+
participant Caller
|
|
79
|
+
participant AiRunner
|
|
80
|
+
participant AgentShim
|
|
81
|
+
participant Identity as buildIdentityPreamble
|
|
82
|
+
participant Slash as translateSlashCommand
|
|
83
|
+
participant Executor as ProcessExecutor
|
|
84
|
+
participant Agent as Coding Agent (CLI)
|
|
85
|
+
participant EventBus
|
|
86
|
+
|
|
87
|
+
Caller->>AiRunner: runPromptCommand("codex", options)
|
|
88
|
+
AiRunner->>AiRunner: hasIdentityOptions(options)?
|
|
89
|
+
alt has team-mode fields
|
|
90
|
+
AiRunner->>Identity: buildIdentityPreamble({agentId, peers, ...})
|
|
91
|
+
Identity-->>AiRunner: preamble text
|
|
92
|
+
AiRunner->>AiRunner: prepend preamble to input
|
|
93
|
+
end
|
|
94
|
+
AiRunner->>AgentShim: getPromptCommand(enrichedOptions)
|
|
95
|
+
AgentShim-->>AiRunner: { command, args }
|
|
96
|
+
AiRunner->>EventBus: emit("agent.invoke.start")
|
|
97
|
+
AiRunner->>Executor: run({ command, args, cwd, timeout })
|
|
98
|
+
Executor->>Agent: spawn CLI subprocess
|
|
99
|
+
Agent-->>Executor: stdout / stderr / exit
|
|
100
|
+
Executor-->>AiRunner: ProcessResult { exitCode, stdout, stderr, durationMs }
|
|
101
|
+
AiRunner->>EventBus: emit("agent.invoke.exit")
|
|
102
|
+
AiRunner-->>Caller: AgentRunResult
|
|
103
|
+
```
|
|
25
104
|
|
|
26
|
-
|
|
27
|
-
|
|
105
|
+
`runSlashCommand()` follows the same path with an extra step: it calls `translateSlashCommand()` to convert the slash input to the target agent's dialect before delegating to `runPromptCommand()`.
|
|
106
|
+
|
|
107
|
+
### Team mode lifecycle
|
|
108
|
+
|
|
109
|
+
```mermaid
|
|
110
|
+
sequenceDiagram
|
|
111
|
+
participant Host as Host App
|
|
112
|
+
participant Orch as TeamOrchestrator
|
|
113
|
+
participant Spec as AgentSpec (filesystem)
|
|
114
|
+
participant Shim as AgentShim
|
|
115
|
+
participant Identity as buildIdentityPreamble
|
|
116
|
+
participant Proc as TeamAgentProcess
|
|
117
|
+
participant Msg as MessageService
|
|
118
|
+
participant DB as InboxMessageDao
|
|
119
|
+
participant Agent as Coding Agent (CLI)
|
|
120
|
+
participant EventBus
|
|
121
|
+
|
|
122
|
+
Note over Host,EventBus: Starting an agent
|
|
123
|
+
Host->>Orch: startAgent("coder")
|
|
124
|
+
Orch->>Spec: loadAgentSpecs(configDir)
|
|
125
|
+
Spec-->>Orch: AgentSpec[]
|
|
126
|
+
Orch->>Orch: requireSpec("coder") → spec
|
|
127
|
+
Orch->>Orch: getPeerSpecs(workspace, "coder") → peers
|
|
128
|
+
Orch->>Identity: buildIdentityPreamble({agentId, peers, ...})
|
|
129
|
+
Identity-->>Orch: preamble
|
|
130
|
+
Orch->>Shim: getPromptCommand({input: preamble, ...})
|
|
131
|
+
Shim-->>Orch: { command, args }
|
|
132
|
+
Orch->>Proc: new TeamAgentProcess({spec, command})
|
|
133
|
+
Orch->>Proc: start()
|
|
134
|
+
Proc->>Agent: spawn pipe-mode subprocess
|
|
135
|
+
Agent-->>Proc: stdout/stderr streams
|
|
136
|
+
Orch->>Msg: drain("coder")
|
|
137
|
+
Msg->>DB: drain(toId)
|
|
138
|
+
DB-->>Msg: pending messages
|
|
139
|
+
Msg-->>Orch: messages[]
|
|
140
|
+
alt pending messages exist
|
|
141
|
+
loop for each message
|
|
142
|
+
Orch->>Proc: send(formattedMessage)
|
|
143
|
+
Proc->>Agent: write to stdin
|
|
144
|
+
Orch->>Msg: deliver(msg.id)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
Orch->>EventBus: emit("agent.started")
|
|
148
|
+
Orch-->>Host: TeamAgentProcess
|
|
149
|
+
|
|
150
|
+
Note over Host,EventBus: Sending a message (durable + live)
|
|
151
|
+
Host->>Orch: sendMessage(null, "coder", "Implement task 0005")
|
|
152
|
+
Orch->>Msg: enqueue(null, "coder", body)
|
|
153
|
+
Msg->>DB: enqueue(from, to, body)
|
|
154
|
+
DB-->>Msg: messageId
|
|
155
|
+
Msg-->>Orch: messageId
|
|
156
|
+
alt agent is running
|
|
157
|
+
Orch->>Msg: drain("coder")
|
|
158
|
+
Msg->>DB: drain(toId)
|
|
159
|
+
DB-->>Msg: pending messages
|
|
160
|
+
loop for each message
|
|
161
|
+
Orch->>Proc: send(formattedMessage)
|
|
162
|
+
Proc->>Agent: write to stdin
|
|
163
|
+
Orch->>Msg: deliver(msg.id)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
Orch->>EventBus: emit("agent.message.sent")
|
|
167
|
+
Orch-->>Host: messageId
|
|
168
|
+
|
|
169
|
+
Note over Host,EventBus: Stopping an agent
|
|
170
|
+
Host->>Orch: stopAgent("coder")
|
|
171
|
+
Orch->>Proc: stop()
|
|
172
|
+
Proc->>Agent: SIGTERM / kill
|
|
173
|
+
Orch->>Orch: running.delete("coder")
|
|
174
|
+
Orch->>EventBus: emit("agent.stopped")
|
|
175
|
+
Orch-->>Host: void
|
|
28
176
|
```
|
|
29
177
|
|
|
30
|
-
|
|
178
|
+
Key design decisions:
|
|
179
|
+
|
|
180
|
+
- **Shims are pure**: `AgentShim` produces `{ command, args }` without touching the filesystem or launching processes. All side effects live in `AiRunner` and `TeamAgentProcess`.
|
|
181
|
+
- **ProcessExecutor is injectable**: tests inject a stub executor; production uses `NodeProcessExecutor` (or the Bun pipe-process seam for team mode).
|
|
182
|
+
- **Events are opt-in**: `AiRunnerOptions.events` and `TeamOrchestratorOptions.events` accept an `EventBus<AgentEvents>` for structured observability. Without it, the runner is silent.
|
|
183
|
+
- **Team mode is composable**: `AgentSpec`, `TeamAgentProcess`, `MessageService`, and `TeamOrchestrator` are small building blocks. Downstream apps compose them into their own orchestration layer.
|
|
184
|
+
|
|
185
|
+
The package depends on `@gobing-ai/ts-runtime` for process execution, `@gobing-ai/ts-db` for team-mode inbox types, and `@gobing-ai/ts-infra` for structured logging and EventBus. The target agent CLIs are not bundled; install them separately in the host environment.
|
|
31
186
|
|
|
32
187
|
## Detect Installed Agents
|
|
33
188
|
|
|
@@ -78,12 +233,27 @@ console.log(result.stdout);
|
|
|
78
233
|
|
|
79
234
|
`AiRunner` captures `stdout`, `stderr`, `exitCode`, optional termination `signal`, and `durationMs`. It does not throw on non-zero agent exits; callers decide how to handle failures.
|
|
80
235
|
|
|
236
|
+
Every invocation is logged through an injectable logger (`getLogger('ai-runner')` by default): one `debug` line per dispatch and an `error` line on any non-zero exit. Pass a custom `logger` to the constructor to route diagnostics elsewhere or silence them in tests.
|
|
237
|
+
|
|
238
|
+
### Slash commands and command preview
|
|
239
|
+
|
|
240
|
+
`runSlashCommand()` translates a Claude-style `/plugin:command args` input into the target agent's dialect (via `translateSlashCommand()`) and dispatches it as a prompt. Non-slash input passes through unchanged.
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
// For codex, "/review:pr 123" becomes "$review-pr 123" before dispatch.
|
|
244
|
+
await runner.runSlashCommand('codex', '/review:pr 123', { model: 'gpt-5' });
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`buildPromptCommand()` returns the resolved `{ command, args }` **without** executing it — useful for previewing, logging, or dry-running the exact argv a prompt would dispatch. It applies the same identity-preamble enrichment as `runPromptCommand()`.
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
const { command, args } = runner.buildPromptCommand('pi', { input: 'ship it', mode: 'json' });
|
|
251
|
+
// command === 'pi', args === ['--no-session', '-p', 'ship it', '--mode', 'json']
|
|
252
|
+
```
|
|
253
|
+
|
|
81
254
|
### Team identity preambles
|
|
82
255
|
|
|
83
|
-
`PromptOptions` accepts optional team-mode fields. When any of `purpose`, `systemPrompt`, `taskId`,
|
|
84
|
-
or non-empty `peers` is supplied, `AiRunner` prepends an identity preamble before dispatching the
|
|
85
|
-
prompt through the selected agent shim. Existing callers that only pass `input`, `continue`, `model`,
|
|
86
|
-
or `mode` are unchanged.
|
|
256
|
+
`PromptOptions` accepts optional team-mode fields. When any of `purpose`, `systemPrompt`, `taskId`, or non-empty `peers` is supplied, `AiRunner` prepends an identity preamble before dispatching the prompt through the selected agent shim.
|
|
87
257
|
|
|
88
258
|
```ts
|
|
89
259
|
await runner.runPromptCommand('codex', {
|
|
@@ -95,8 +265,7 @@ await runner.runPromptCommand('codex', {
|
|
|
95
265
|
});
|
|
96
266
|
```
|
|
97
267
|
|
|
98
|
-
Use `buildIdentityPreamble()` directly when a host app needs to preview or inject the same context
|
|
99
|
-
outside `AiRunner`:
|
|
268
|
+
Use `buildIdentityPreamble()` directly when a host app needs to preview or inject the same context outside `AiRunner`:
|
|
100
269
|
|
|
101
270
|
```ts
|
|
102
271
|
import { buildIdentityPreamble } from '@gobing-ai/ts-ai-runner';
|
|
@@ -113,6 +282,43 @@ const preamble = buildIdentityPreamble({
|
|
|
113
282
|
});
|
|
114
283
|
```
|
|
115
284
|
|
|
285
|
+
Use `getGitContext()` to auto-detect the current branch and dirty state:
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
import { getGitContext } from '@gobing-ai/ts-ai-runner';
|
|
289
|
+
|
|
290
|
+
const gitBlock = getGitContext('/workspace/spur');
|
|
291
|
+
// "Git context:\nbranch: feat/team-mode\ndirty: 3 files"
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Observability
|
|
295
|
+
|
|
296
|
+
`AiRunner` and `TeamOrchestrator` emit typed events when an `EventBus<AgentEvents>` is provided:
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
import { AiRunner } from '@gobing-ai/ts-ai-runner';
|
|
300
|
+
import { EventBus } from '@gobing-ai/ts-infra';
|
|
301
|
+
import type { AgentEvents } from '@gobing-ai/ts-ai-runner';
|
|
302
|
+
|
|
303
|
+
const bus = new EventBus<AgentEvents>();
|
|
304
|
+
bus.on('agent.invoke.start', (data) => console.log('starting', data.label));
|
|
305
|
+
bus.on('agent.invoke.exit', (data) => console.log('done', data.label, data.exitCode, data.durationMs));
|
|
306
|
+
|
|
307
|
+
const runner = new AiRunner({ events: bus });
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Available events:
|
|
311
|
+
|
|
312
|
+
| Event | When |
|
|
313
|
+
|-------|------|
|
|
314
|
+
| `agent.invoke.start` | Immediately before an agent CLI invocation starts |
|
|
315
|
+
| `agent.invoke.exit` | After an agent CLI invocation exits |
|
|
316
|
+
| `agent.started` | When a long-running team agent process starts |
|
|
317
|
+
| `agent.stopped` | When a long-running team agent process stops |
|
|
318
|
+
| `agent.message.sent` | When a message is sent to a team agent process |
|
|
319
|
+
|
|
320
|
+
`AiRunnerOptions.processEvents` accepts a separate `EventBus<AiRunnerProcessEvents>` for process-level events from the underlying executor.
|
|
321
|
+
|
|
116
322
|
## Inject a Process Executor
|
|
117
323
|
|
|
118
324
|
For tests, dry runs, or sandboxed launchers, inject a `ProcessExecutor` from `@gobing-ai/ts-runtime`:
|
|
@@ -182,7 +388,7 @@ translateSlashCommand('pi', '/rd3:dev-fixall bun run check');
|
|
|
182
388
|
// /skill:rd3-dev-fixall bun run check
|
|
183
389
|
```
|
|
184
390
|
|
|
185
|
-
Non-slash input is returned unchanged.
|
|
391
|
+
Non-slash input is returned unchanged. Use `isClaudeStyleSlashCommand()` to test input before translation.
|
|
186
392
|
|
|
187
393
|
## Command Shims
|
|
188
394
|
|
|
@@ -197,17 +403,41 @@ const command = shim.getPromptCommand({ input: 'Summarize this repository' });
|
|
|
197
403
|
console.log(command.command, command.args);
|
|
198
404
|
```
|
|
199
405
|
|
|
406
|
+
Each shim implements the `AgentShim` interface:
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
interface AgentShim {
|
|
410
|
+
readonly name: AgentName;
|
|
411
|
+
readonly command: string;
|
|
412
|
+
readonly tier: 1 | 2;
|
|
413
|
+
getHelpCommand(): ShimCommand;
|
|
414
|
+
getVersionCommand(): ShimCommand;
|
|
415
|
+
getPromptCommand(options: PromptOptions): ShimCommand;
|
|
416
|
+
getAuthCommand(): ShimCommand | null;
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
Agent-specific behavior:
|
|
421
|
+
|
|
422
|
+
| Agent | CLI | Tier | Auth check | Prompt flags |
|
|
423
|
+
|-------|-----|------|------------|--------------|
|
|
424
|
+
| `claude` | `claude` | 1 | `claude auth status` | `-p`, `--continue`, `--model`, `--output-format` |
|
|
425
|
+
| `codex` | `codex` | 1 | `codex login status` | `exec <prompt>`, `exec resume --last`, `-m`, `--json` |
|
|
426
|
+
| `gemini` | `gemini` | 1 | env-only | `-p`, `-r latest` (resume), `-m`, `-o` |
|
|
427
|
+
| `pi` | `pi` | 1 | `pi --list-models` | `--no-session`, `-p`, `-c` (resume), `--model`, `--mode` |
|
|
428
|
+
| `opencode` | `opencode` | 1 | `opencode providers` | `run`, `-c`, `-m`, `--format json` |
|
|
429
|
+
| `antigravity` | `agy` | 2 | env-only | `chat` |
|
|
430
|
+
| `openclaw` | `openclaw` | 2 | `openclaw health` | `agent --local -m` |
|
|
431
|
+
|
|
200
432
|
This is the right layer for UI previews, audit logging, and custom launchers.
|
|
201
433
|
|
|
202
434
|
## Team Mode Primitives
|
|
203
435
|
|
|
204
|
-
The team-mode APIs are intentionally small building blocks. They do not implement an HTTP API,
|
|
205
|
-
dashboard, or product workflow; downstream apps compose them into their own orchestration layer.
|
|
436
|
+
The team-mode APIs are intentionally small building blocks. They do not implement an HTTP API, dashboard, or product workflow; downstream apps compose them into their own orchestration layer.
|
|
206
437
|
|
|
207
438
|
### Agent specs
|
|
208
439
|
|
|
209
|
-
Agent specs define agents as config. The built-in parser supports the repository's constrained YAML
|
|
210
|
-
subset: scalars, arrays, nested objects, and no anchors/tags/multiline scalars.
|
|
440
|
+
Agent specs define agents as config. The built-in parser supports the repository's constrained YAML subset: scalars, arrays, nested objects, and no anchors/tags/multiline scalars.
|
|
211
441
|
|
|
212
442
|
```ts
|
|
213
443
|
import { loadAgentSpecs, saveAgentSpec } from '@gobing-ai/ts-ai-runner';
|
|
@@ -229,12 +459,11 @@ await saveAgentSpec(
|
|
|
229
459
|
const specs = loadAgentSpecs('./agents');
|
|
230
460
|
```
|
|
231
461
|
|
|
232
|
-
`validateAgentId()` enforces lowercase agent ids with alphanumeric, `_`, and `-` characters.
|
|
462
|
+
`validateAgentId()` enforces lowercase agent ids with alphanumeric, `_`, and `-` characters (2–64 chars). `deleteAgentSpec()` removes a spec file by id.
|
|
233
463
|
|
|
234
464
|
### Durable messages
|
|
235
465
|
|
|
236
|
-
`MessageService` wraps `InboxMessageDao` from `@gobing-ai/ts-db/inbox`. It owns no subprocess
|
|
237
|
-
behavior; it only persists, drains, marks delivery/failure, and formats messages.
|
|
466
|
+
`MessageService` wraps `InboxMessageDao` from `@gobing-ai/ts-db/inbox`. It owns no subprocess behavior; it only persists, drains, marks delivery/failure, and formats messages.
|
|
238
467
|
|
|
239
468
|
```ts
|
|
240
469
|
import { InboxMessageDao } from '@gobing-ai/ts-db/inbox';
|
|
@@ -251,10 +480,11 @@ for (const msg of pending) {
|
|
|
251
480
|
}
|
|
252
481
|
```
|
|
253
482
|
|
|
483
|
+
`MessageService` also exposes `fail(msgId, error)`, `inbox(toId, limit?, offset?)`, and `countPending(toId)`.
|
|
484
|
+
|
|
254
485
|
### Persistent agent processes
|
|
255
486
|
|
|
256
|
-
`TeamAgentProcess` wraps a long-running agent subprocess using the runtime pipe-process seam. It
|
|
257
|
-
supports start/stop, stdin sends, stdout/stderr subscriptions, status, pid, and exit-code queries.
|
|
487
|
+
`TeamAgentProcess` wraps a long-running agent subprocess using the runtime pipe-process seam. It supports start/stop, stdin sends, stdout/stderr subscriptions, status, pid, and exit-code queries.
|
|
258
488
|
|
|
259
489
|
```ts
|
|
260
490
|
import { TeamAgentProcess, type AgentSpec } from '@gobing-ai/ts-ai-runner';
|
|
@@ -286,10 +516,7 @@ unsubscribe();
|
|
|
286
516
|
|
|
287
517
|
### Team orchestrator
|
|
288
518
|
|
|
289
|
-
`TeamOrchestrator` connects specs, shims, processes, and messages. On start it loads an agent spec,
|
|
290
|
-
builds the agent command through the matching shim, starts the process, drains pending inbox messages,
|
|
291
|
-
and injects them live. `sendMessage()` always persists first, then injects immediately when the target
|
|
292
|
-
agent is running.
|
|
519
|
+
`TeamOrchestrator` connects specs, shims, processes, and messages. On start it loads an agent spec, builds the agent command through the matching shim, starts the process, drains pending inbox messages, and injects them live. `sendMessage()` always persists first, then injects immediately when the target agent is running.
|
|
293
520
|
|
|
294
521
|
```ts
|
|
295
522
|
import { InboxMessageDao } from '@gobing-ai/ts-db/inbox';
|
|
@@ -306,10 +533,87 @@ console.log(team.getAgentStatus('coder')); // running
|
|
|
306
533
|
await team.stopAll();
|
|
307
534
|
```
|
|
308
535
|
|
|
536
|
+
The orchestrator also provides `restartAgent(id)`, `getRunningAgents()`, `getPeerSpecs(workspace, excludeId?)`, and `on(event, listener)` for event subscription.
|
|
537
|
+
|
|
538
|
+
## Adding a New Coding Agent
|
|
539
|
+
|
|
540
|
+
To add support for a new coding agent (e.g. `amp`), follow these steps:
|
|
541
|
+
|
|
542
|
+
### 1. Add the shim
|
|
543
|
+
|
|
544
|
+
Edit `src/agents/shims.ts`. Add a new constant implementing the `AgentShim` interface:
|
|
545
|
+
|
|
546
|
+
```ts
|
|
547
|
+
const ampShim: AgentShim = {
|
|
548
|
+
name: 'amp',
|
|
549
|
+
command: 'amp',
|
|
550
|
+
tier: 1, // or 2 if gateway/TUI-constrained
|
|
551
|
+
getHelpCommand: () => ({ command: 'amp', args: ['--help'] }),
|
|
552
|
+
getVersionCommand: () => ({ command: 'amp', args: ['--version'] }),
|
|
553
|
+
getPromptCommand: (options) => {
|
|
554
|
+
const args = ['run', options.input ?? ''];
|
|
555
|
+
if (options.continue === true) args.push('--resume');
|
|
556
|
+
if (options.model !== undefined) args.push('--model', options.model);
|
|
557
|
+
if ((options.mode ?? 'text') === 'json') args.push('--json');
|
|
558
|
+
return { command: 'amp', args };
|
|
559
|
+
},
|
|
560
|
+
getAuthCommand: () => ({ command: 'amp', args: ['auth', 'status'] }),
|
|
561
|
+
};
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
### 2. Register the shim
|
|
565
|
+
|
|
566
|
+
In the same file, add the new agent to these three places:
|
|
567
|
+
|
|
568
|
+
```ts
|
|
569
|
+
// 1. The AgentName union type
|
|
570
|
+
export type AgentName = 'claude' | 'codex' | 'gemini' | 'pi' | 'opencode' | 'antigravity' | 'openclaw' | 'amp';
|
|
571
|
+
|
|
572
|
+
// 2. The AGENT_SHIMS registry
|
|
573
|
+
export const AGENT_SHIMS: Readonly<Record<AgentName, AgentShim>> = {
|
|
574
|
+
// ...existing entries...
|
|
575
|
+
amp: ampShim,
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
// 3. DISPLAY_ORDER (determines doctor/detector output order)
|
|
579
|
+
export const DISPLAY_ORDER: readonly AgentName[] = [
|
|
580
|
+
'claude', 'codex', 'gemini', 'pi', 'opencode', 'antigravity', 'openclaw', 'amp',
|
|
581
|
+
];
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
If the agent is tier 1, add it to `TIER1_PRIORITY`. If tier 2 (gateway/TUI-constrained), add it to `TIER2_AGENTS`.
|
|
585
|
+
|
|
586
|
+
### 3. Add slash-command dialect (if needed)
|
|
587
|
+
|
|
588
|
+
If the agent uses a different slash-command syntax than the default `/{plugin}-{command} args` mapping, add a case to `translateSlashCommand()` in `src/slash-command.ts`.
|
|
589
|
+
|
|
590
|
+
### 4. Add auth detection (if applicable)
|
|
591
|
+
|
|
592
|
+
If the agent supports an auth-status command, `getAuthCommand()` already returns it and `DoctorRunner` will probe it automatically. For env-only or file-based auth, add a pattern to `AUTH_PATTERNS` in `src/doctor-runner.ts`.
|
|
593
|
+
|
|
594
|
+
### 5. Add tests
|
|
595
|
+
|
|
596
|
+
- **Shim tests**: add cases to `tests/agents/` verifying command construction for help, version, prompt (with and without options), and auth.
|
|
597
|
+
- **Detector tests**: verify `detectOne('amp')` handles version output and error cases.
|
|
598
|
+
- **Doctor tests**: verify auth probe for the new agent.
|
|
599
|
+
|
|
600
|
+
### Summary checklist
|
|
601
|
+
|
|
602
|
+
| Step | File | What to change |
|
|
603
|
+
|------|------|----------------|
|
|
604
|
+
| Shim | `src/agents/shims.ts` | Add `AgentShim` impl, update `AgentName`, `AGENT_SHIMS`, `DISPLAY_ORDER` |
|
|
605
|
+
| Tier | `src/agents/shims.ts` | Add to `TIER1_PRIORITY` or `TIER2_AGENTS` |
|
|
606
|
+
| Slash | `src/slash-command.ts` | Add case if dialect differs from default |
|
|
607
|
+
| Auth | `src/doctor-runner.ts` | Add `AUTH_PATTERNS` entry if file/env-based |
|
|
608
|
+
| Tests | `tests/` | Shim, detector, and doctor coverage |
|
|
609
|
+
|
|
610
|
+
After these changes, the new agent is automatically available to `AiRunner`, `AgentDetector`, `DoctorRunner`, `TeamOrchestrator`, and all downstream consumers — no further registration needed.
|
|
611
|
+
|
|
309
612
|
## Boundary Notes
|
|
310
613
|
|
|
311
614
|
- This package is a command adapter, not an agent orchestration framework.
|
|
312
615
|
- It does not install agent CLIs or manage credentials.
|
|
313
616
|
- It does not parse agent responses beyond process result capture.
|
|
314
|
-
- It keeps subprocess launching behind `ProcessExecutor` / `
|
|
617
|
+
- It keeps subprocess launching behind `ProcessExecutor` / `PipeProcess`, so tests can stay deterministic.
|
|
315
618
|
- Team-mode persistence is delegated to `@gobing-ai/ts-db/inbox`; host apps own migrations and adapter lifecycle.
|
|
619
|
+
- Platform APIs (`node:fs`, `node:path`, `Bun.spawn`, etc.) are confined to `@gobing-ai/ts-runtime` per ADR-011. This package accesses them through the runtime's `FileSystem`, `ProcessExecutor`, and path utilities.
|
package/dist/agent-detector.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-detector.d.ts","sourceRoot":"","sources":["../src/agent-detector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAA4C,MAAM,gBAAgB,CAAC;AAC1F,OAAO,EAAuB,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5D,8DAA8D;AAC9D,MAAM,WAAW,aAAa;IAC1B,wBAAwB;IACxB,IAAI,EAAE,SAAS,GAAG,MAAM,CAAC;IACzB,wEAAwE;IACxE,SAAS,EAAE,OAAO,CAAC;IACnB,gDAAgD;IAChD,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,wDAAwD;IACxD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,wCAAwC;IACxC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,6CAA6C;AAC7C,MAAM,WAAW,oBAAoB;IACjC,gCAAgC;IAChC,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAKD,6EAA6E;AAC7E,qBAAa,aAAa;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAW;IAClC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAErB,OAAO,GAAE,oBAAyB;IAK9C,iDAAiD;IAC3C,SAAS,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;IAI3C,+BAA+B;IACzB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAStD,OAAO,CAAC,WAAW;
|
|
1
|
+
{"version":3,"file":"agent-detector.d.ts","sourceRoot":"","sources":["../src/agent-detector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAA4C,MAAM,gBAAgB,CAAC;AAC1F,OAAO,EAAuB,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5D,8DAA8D;AAC9D,MAAM,WAAW,aAAa;IAC1B,wBAAwB;IACxB,IAAI,EAAE,SAAS,GAAG,MAAM,CAAC;IACzB,wEAAwE;IACxE,SAAS,EAAE,OAAO,CAAC;IACnB,gDAAgD;IAChD,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,wDAAwD;IACxD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,wCAAwC;IACxC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,6CAA6C;AAC7C,MAAM,WAAW,oBAAoB;IACjC,gCAAgC;IAChC,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAKD,6EAA6E;AAC7E,qBAAa,aAAa;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAW;IAClC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAErB,OAAO,GAAE,oBAAyB;IAK9C,iDAAiD;IAC3C,SAAS,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;IAI3C,+BAA+B;IACzB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAStD,OAAO,CAAC,WAAW;IA8BnB,OAAO,CAAC,cAAc;CAIzB"}
|
package/dist/agent-detector.js
CHANGED
|
@@ -32,24 +32,32 @@ export class AgentDetector {
|
|
|
32
32
|
if (lower.includes('command not found') || lower.includes('enoent') || lower.includes('not recognized')) {
|
|
33
33
|
return unavailable(agent, `${command}: command not found`);
|
|
34
34
|
}
|
|
35
|
-
if (result.signal !== undefined
|
|
36
|
-
return unavailable(agent, result.signal
|
|
35
|
+
if (result.signal !== undefined) {
|
|
36
|
+
return unavailable(agent, `Terminated by signal: ${result.signal}`);
|
|
37
|
+
}
|
|
38
|
+
if (result.exitCode === null) {
|
|
39
|
+
return unavailable(agent, 'Process did not produce an exit code');
|
|
37
40
|
}
|
|
38
41
|
if (result.exitCode !== 0) {
|
|
39
|
-
return unavailable(agent, `Non-zero exit code ${result.exitCode}: ${result.stderr.slice(0, 200)}`);
|
|
42
|
+
return unavailable(agent, `Non-zero exit code: ${result.exitCode}. stderr: ${result.stderr.slice(0, 200)}`);
|
|
40
43
|
}
|
|
41
44
|
const match = VERSION_PATTERN.exec(output);
|
|
42
45
|
if (match?.groups?.version === undefined) {
|
|
43
46
|
return unavailable(agent, 'Could not parse version output');
|
|
44
47
|
}
|
|
48
|
+
const version = output.split('\n')[0]?.trim() || match.groups.version;
|
|
45
49
|
return {
|
|
46
50
|
name: agent,
|
|
47
51
|
installed: true,
|
|
48
|
-
version
|
|
49
|
-
channels:
|
|
52
|
+
version,
|
|
53
|
+
channels: this.detectChannels(agent, output),
|
|
50
54
|
error: null,
|
|
51
55
|
};
|
|
52
56
|
}
|
|
57
|
+
detectChannels(_agent, _output) {
|
|
58
|
+
// Phase 2 hook: parse per-agent channel/model output here when shims expose it.
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
53
61
|
}
|
|
54
62
|
/** Build an "unavailable" detection result for an agent with the given error. */
|
|
55
63
|
function unavailable(name, error) {
|
package/dist/agent-spec.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type SyncFileSystem } from '@gobing-ai/ts-runtime';
|
|
2
|
+
/** Specification for an AI agent defined in the configuration directory. */
|
|
2
3
|
export interface AgentSpec {
|
|
3
4
|
id: string;
|
|
4
5
|
name: string;
|
|
@@ -9,11 +10,16 @@ export interface AgentSpec {
|
|
|
9
10
|
config: Record<string, unknown>;
|
|
10
11
|
autoStart?: boolean;
|
|
11
12
|
}
|
|
13
|
+
/** Validation error thrown when agent spec data is malformed or invalid. */
|
|
12
14
|
export declare class ValueError extends Error {
|
|
13
15
|
constructor(message: string);
|
|
14
16
|
}
|
|
17
|
+
/** Validate that `id` matches the agent ID format: 2-64 chars, lowercase alphanumeric with `_` or `-`. Returns the id on success, throws `ValueError` otherwise. */
|
|
15
18
|
export declare function validateAgentId(id: string): string;
|
|
19
|
+
/** Load and validate all YAML agent spec files from `configDir`. Throws `ValueError` on parse failures or duplicate IDs. */
|
|
16
20
|
export declare function loadAgentSpecs(configDir: string, fs?: SyncFileSystem): AgentSpec[];
|
|
21
|
+
/** Serialize `spec` to YAML and write it to `configDir/<id>.yaml`. Creates the directory if missing. */
|
|
17
22
|
export declare function saveAgentSpec(spec: AgentSpec, configDir: string, fs?: SyncFileSystem): Promise<void>;
|
|
23
|
+
/** Remove the YAML file for agent `id` from `configDir`. Does nothing if the file doesn't exist. */
|
|
18
24
|
export declare function deleteAgentSpec(id: string, configDir: string, fs?: SyncFileSystem): Promise<void>;
|
|
19
25
|
//# sourceMappingURL=agent-spec.d.ts.map
|
package/dist/agent-spec.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-spec.d.ts","sourceRoot":"","sources":["../src/agent-spec.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"agent-spec.d.ts","sourceRoot":"","sources":["../src/agent-spec.ts"],"names":[],"mappings":"AAAA,OAAO,EAKH,KAAK,cAAc,EAEtB,MAAM,uBAAuB,CAAC;AAE/B,4EAA4E;AAC5E,MAAM,WAAW,SAAS;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,SAAS,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,4EAA4E;AAC5E,qBAAa,UAAW,SAAQ,KAAK;gBACrB,OAAO,EAAE,MAAM;CAI9B;AAED,oKAAoK;AACpK,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAKlD;AAED,4HAA4H;AAC5H,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,GAAE,cAAyC,GAAG,SAAS,EAAE,CAY5G;AAED,wGAAwG;AACxG,wBAAsB,aAAa,CAC/B,IAAI,EAAE,SAAS,EACf,SAAS,EAAE,MAAM,EACjB,EAAE,GAAE,cAAyC,GAC9C,OAAO,CAAC,IAAI,CAAC,CAIf;AAED,oGAAoG;AACpG,wBAAsB,eAAe,CACjC,EAAE,EAAE,MAAM,EACV,SAAS,EAAE,MAAM,EACjB,EAAE,GAAE,cAAyC,GAC9C,OAAO,CAAC,IAAI,CAAC,CAGf"}
|
package/dist/agent-spec.js
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { basenamePath, joinPath, NodeSyncFileSystem, parseYamlObject, stringifyYamlObject, } from '@gobing-ai/ts-runtime';
|
|
2
|
+
/** Validation error thrown when agent spec data is malformed or invalid. */
|
|
3
3
|
export class ValueError extends Error {
|
|
4
4
|
constructor(message) {
|
|
5
5
|
super(message);
|
|
6
6
|
this.name = 'ValueError';
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
|
+
/** Validate that `id` matches the agent ID format: 2-64 chars, lowercase alphanumeric with `_` or `-`. Returns the id on success, throws `ValueError` otherwise. */
|
|
9
10
|
export function validateAgentId(id) {
|
|
10
11
|
if (!/^[a-z][a-z0-9_-]{1,63}$/.test(id)) {
|
|
11
12
|
throw new ValueError(`Invalid agent id "${id}": expected 2-64 chars, lowercase alphanumeric, "_" or "-"`);
|
|
12
13
|
}
|
|
13
14
|
return id;
|
|
14
15
|
}
|
|
16
|
+
/** Load and validate all YAML agent spec files from `configDir`. Throws `ValueError` on parse failures or duplicate IDs. */
|
|
15
17
|
export function loadAgentSpecs(configDir, fs = new NodeSyncFileSystem()) {
|
|
16
18
|
const entries = safeReadDir(configDir, fs)
|
|
17
19
|
.filter((entry) => entry.endsWith('.yaml') || entry.endsWith('.yml'))
|
|
18
20
|
.sort();
|
|
19
|
-
const specs = entries.map((entry) => parseAgentSpec(fs.readFile(
|
|
21
|
+
const specs = entries.map((entry) => parseAgentSpec(fs.readFile(joinPath(configDir, entry)), entry));
|
|
20
22
|
const seen = new Set();
|
|
21
23
|
for (const spec of specs) {
|
|
22
24
|
validateAgentId(spec.id);
|
|
@@ -26,14 +28,16 @@ export function loadAgentSpecs(configDir, fs = new NodeSyncFileSystem()) {
|
|
|
26
28
|
}
|
|
27
29
|
return specs;
|
|
28
30
|
}
|
|
31
|
+
/** Serialize `spec` to YAML and write it to `configDir/<id>.yaml`. Creates the directory if missing. */
|
|
29
32
|
export async function saveAgentSpec(spec, configDir, fs = new NodeSyncFileSystem()) {
|
|
30
33
|
validateAgentId(spec.id);
|
|
31
34
|
fs.mkdir(configDir);
|
|
32
|
-
fs.writeFile(
|
|
35
|
+
fs.writeFile(joinPath(configDir, `${spec.id}.yaml`), serializeAgentSpec(spec));
|
|
33
36
|
}
|
|
37
|
+
/** Remove the YAML file for agent `id` from `configDir`. Does nothing if the file doesn't exist. */
|
|
34
38
|
export async function deleteAgentSpec(id, configDir, fs = new NodeSyncFileSystem()) {
|
|
35
39
|
validateAgentId(id);
|
|
36
|
-
fs.unlink(
|
|
40
|
+
fs.unlink(joinPath(configDir, `${id}.yaml`));
|
|
37
41
|
}
|
|
38
42
|
function safeReadDir(configDir, fs = new NodeSyncFileSystem()) {
|
|
39
43
|
try {
|
|
@@ -76,21 +80,21 @@ function serializeAgentSpec(spec) {
|
|
|
76
80
|
function requireString(source, key, fileName) {
|
|
77
81
|
const value = source[key];
|
|
78
82
|
if (typeof value !== 'string' || value.trim() === '') {
|
|
79
|
-
throw new ValueError(`${
|
|
83
|
+
throw new ValueError(`${basenamePath(fileName)}: "${key}" must be a non-empty string`);
|
|
80
84
|
}
|
|
81
85
|
return value;
|
|
82
86
|
}
|
|
83
87
|
function requireStringArray(source, key, fileName) {
|
|
84
88
|
const value = source[key];
|
|
85
89
|
if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) {
|
|
86
|
-
throw new ValueError(`${
|
|
90
|
+
throw new ValueError(`${basenamePath(fileName)}: "${key}" must be a string array`);
|
|
87
91
|
}
|
|
88
92
|
return value;
|
|
89
93
|
}
|
|
90
94
|
function requireRecord(source, key, fileName) {
|
|
91
95
|
const value = source[key];
|
|
92
96
|
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
93
|
-
throw new ValueError(`${
|
|
97
|
+
throw new ValueError(`${basenamePath(fileName)}: "${key}" must be an object`);
|
|
94
98
|
}
|
|
95
99
|
return value;
|
|
96
100
|
}
|