@yaag/runtime 0.1.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.
Files changed (71) hide show
  1. package/package.json +25 -0
  2. package/src/agent-names.ts +20 -0
  3. package/src/agent-usage.ts +72 -0
  4. package/src/agent.ts +130 -0
  5. package/src/args-validation.ts +11 -0
  6. package/src/ask-activity.ts +84 -0
  7. package/src/ask-contract-identity.ts +96 -0
  8. package/src/ask-exchange-events.ts +60 -0
  9. package/src/ask-exchange-options.ts +32 -0
  10. package/src/ask-exchange.ts +291 -0
  11. package/src/ask-hash.ts +86 -0
  12. package/src/ask-limit.ts +189 -0
  13. package/src/ask-output-steering.ts +69 -0
  14. package/src/ask-output-tail.ts +166 -0
  15. package/src/ask-output.ts +109 -0
  16. package/src/ask-settlement.ts +37 -0
  17. package/src/ask-turn.ts +70 -0
  18. package/src/cassette-loader.ts +131 -0
  19. package/src/cassette-publish.ts +55 -0
  20. package/src/cassette-replay.ts +178 -0
  21. package/src/cassette-schema.ts +152 -0
  22. package/src/cassette.ts +275 -0
  23. package/src/checkpoint-dir.ts +89 -0
  24. package/src/connection.ts +123 -0
  25. package/src/define-agent.ts +83 -0
  26. package/src/define-run.ts +69 -0
  27. package/src/errors.ts +115 -0
  28. package/src/events.ts +143 -0
  29. package/src/extension-package.ts +88 -0
  30. package/src/extension-paths.ts +66 -0
  31. package/src/extension-source.ts +60 -0
  32. package/src/fake-transport.ts +240 -0
  33. package/src/frame-gap.ts +41 -0
  34. package/src/frame-queue.ts +52 -0
  35. package/src/git-facts.ts +32 -0
  36. package/src/idle-watch.ts +154 -0
  37. package/src/index.ts +96 -0
  38. package/src/jsonl.ts +42 -0
  39. package/src/live-transport.ts +210 -0
  40. package/src/node-decoder-subagent.ts +67 -0
  41. package/src/node-decoder-workflow.ts +74 -0
  42. package/src/node-decoder.ts +23 -0
  43. package/src/node-decoders.ts +9 -0
  44. package/src/node-details.ts +70 -0
  45. package/src/node-path.ts +36 -0
  46. package/src/node-tracker.ts +143 -0
  47. package/src/pi-state.ts +108 -0
  48. package/src/prompt-gist.ts +13 -0
  49. package/src/prompt.ts +80 -0
  50. package/src/reap.ts +59 -0
  51. package/src/recording-transport.ts +97 -0
  52. package/src/replay-divergence.ts +155 -0
  53. package/src/replay-transport.ts +72 -0
  54. package/src/resume-preconditions.ts +59 -0
  55. package/src/resume-transport.ts +165 -0
  56. package/src/run-checkpoint.ts +93 -0
  57. package/src/run-context.ts +19 -0
  58. package/src/run.ts +274 -0
  59. package/src/skill-probe.ts +247 -0
  60. package/src/skill-restriction-transport.ts +76 -0
  61. package/src/spawn.ts +241 -0
  62. package/src/summary-agent.ts +310 -0
  63. package/src/summary-nodes.ts +77 -0
  64. package/src/summary.ts +213 -0
  65. package/src/tool-probe-extension.ts +17 -0
  66. package/src/tool-probe.ts +141 -0
  67. package/src/transport.ts +178 -0
  68. package/src/types.ts +130 -0
  69. package/src/validation-errors.ts +70 -0
  70. package/src/wire-constants.ts +24 -0
  71. package/src/worktree-transport.ts +125 -0
@@ -0,0 +1,247 @@
1
+ /** One enabled skill reported by pi's `get_commands` response. */
2
+ export interface DiscoveredSkill {
3
+ readonly name: string;
4
+ readonly path: string;
5
+ /** True when an extension injects the skill after `--no-skills` is processed. */
6
+ readonly extensionInjected: boolean;
7
+ }
8
+
9
+ /** Cwd-scoped skill discovery seam, separate from long-lived Agent transports. */
10
+ export interface SkillProbeFactory {
11
+ probe(cwd: string): Promise<readonly DiscoveredSkill[]>;
12
+ }
13
+
14
+ /** The stdin operations owned by one short-lived skill discovery process. */
15
+ export interface SkillProbeStdin {
16
+ write(input: string): void;
17
+ flush(): void;
18
+ end(): void;
19
+ }
20
+
21
+ /**
22
+ * The short-lived process boundary used only while discovering skills.
23
+ *
24
+ * `probe()` always ends stdin, escalates to SIGKILL when needed, and awaits
25
+ * `exited`; implementations must make `exited` settle once the child is reaped.
26
+ */
27
+ export interface SkillProbeProcess {
28
+ readonly stdin: SkillProbeStdin;
29
+ readonly stdout: ReadableStream<Uint8Array>;
30
+ readonly exited: Promise<number>;
31
+ kill(signal: "SIGKILL"): void;
32
+ }
33
+
34
+ /** Creates a short-lived discovery process in the supplied child cwd. */
35
+ export interface SkillProbeProcessFactory {
36
+ create(cwd: string): SkillProbeProcess;
37
+ }
38
+
39
+ /** Injectable timings for a short-lived skill discovery process. */
40
+ export interface SkillProbeTimings {
41
+ readonly responseTimeoutMs?: number;
42
+ readonly cleanupGraceMs?: number;
43
+ }
44
+
45
+ /** The fixed process configuration used by pi's short-lived discovery adapter. */
46
+ export interface SkillProbeSpawnOptions {
47
+ readonly cmd: readonly string[];
48
+ readonly cwd: string;
49
+ readonly stdin: "pipe";
50
+ readonly stdout: "pipe";
51
+ readonly stderr: "ignore";
52
+ }
53
+
54
+ /**
55
+ * Parses pi's successful `get_commands` response into pi-compatible skill names.
56
+ *
57
+ * Rejects malformed skill entries instead of claiming a restriction was enforced.
58
+ */
59
+ export function parseSkillCommands(response: unknown): readonly DiscoveredSkill[] {
60
+ if (!isRecord(response) || response.type !== "response" || response.command !== "get_commands") {
61
+ throw new Error("malformed get_commands response");
62
+ }
63
+ if (
64
+ response.success !== true ||
65
+ !isRecord(response.data) ||
66
+ !Array.isArray(response.data.commands)
67
+ ) {
68
+ throw new Error("get_commands did not return commands");
69
+ }
70
+ const discovered: DiscoveredSkill[] = [];
71
+ const names = new Set<string>();
72
+ for (const command of response.data.commands) {
73
+ if (!isRecord(command) || typeof command.source !== "string") {
74
+ throw new Error("malformed get_commands command");
75
+ }
76
+ if (command.source !== "skill") continue;
77
+ if (typeof command.name !== "string" || !command.name.startsWith("skill:")) {
78
+ throw new Error("malformed skill command name");
79
+ }
80
+ if (!isRecord(command.sourceInfo) || typeof command.sourceInfo.path !== "string") {
81
+ throw new Error(`skill command "${command.name}" has no sourceInfo.path`);
82
+ }
83
+ if (typeof command.sourceInfo.source !== "string") {
84
+ throw new Error(`skill command "${command.name}" has no sourceInfo.source`);
85
+ }
86
+ const name = command.name.slice("skill:".length);
87
+ if (name === "") throw new Error("malformed skill command name");
88
+ if (names.has(name)) continue;
89
+ names.add(name);
90
+ discovered.push({
91
+ name,
92
+ path: command.sourceInfo.path,
93
+ extensionInjected: command.sourceInfo.source.startsWith("extension:"),
94
+ });
95
+ }
96
+ return discovered;
97
+ }
98
+
99
+ const RESPONSE_TIMEOUT_MS = 30_000;
100
+ const CLEANUP_GRACE_MS = 100;
101
+
102
+ /** Wraps a process constructor as the narrow discovery-process factory seam. */
103
+ export function createSkillProbeProcessFactory(
104
+ create: (cwd: string) => SkillProbeProcess,
105
+ ): SkillProbeProcessFactory {
106
+ return { create };
107
+ }
108
+
109
+ /**
110
+ * Adapts a pi process spawner to the discovery-process seam.
111
+ *
112
+ * It always starts `pi --mode rpc --no-session` with piped stdin/stdout so a
113
+ * probe is isolated from an Agent's long-lived transport.
114
+ */
115
+ export function createBunSkillProbeProcessFactory(
116
+ spawn: (options: SkillProbeSpawnOptions) => SkillProbeProcess,
117
+ ): SkillProbeProcessFactory {
118
+ return createSkillProbeProcessFactory((cwd) =>
119
+ spawn({
120
+ cmd: ["pi", "--mode", "rpc", "--no-session"],
121
+ cwd,
122
+ stdin: "pipe",
123
+ stdout: "pipe",
124
+ stderr: "ignore",
125
+ }),
126
+ );
127
+ }
128
+
129
+ /**
130
+ * Creates a short-lived pi skill discovery probe.
131
+ *
132
+ * Rejects when pi exits, emits malformed output, or misses the response timeout.
133
+ * Every outcome ends stdin, escalates to SIGKILL after the cleanup grace period,
134
+ * and waits for process reaping before this method settles.
135
+ */
136
+ export function createSkillProbe(
137
+ processFactory: SkillProbeProcessFactory,
138
+ timings: SkillProbeTimings = {},
139
+ ): SkillProbeFactory {
140
+ const responseTimeoutMs = timings.responseTimeoutMs ?? RESPONSE_TIMEOUT_MS;
141
+ const cleanupGraceMs = timings.cleanupGraceMs ?? CLEANUP_GRACE_MS;
142
+ return {
143
+ async probe(cwd: string): Promise<readonly DiscoveredSkill[]> {
144
+ const process = processFactory.create(cwd);
145
+ try {
146
+ const id = "yaag-skill-probe-0";
147
+ process.stdin.write(`${JSON.stringify({ type: "get_commands", id })}\n`);
148
+ process.stdin.flush();
149
+ return await awaitResponse(process.stdout, process.exited, id, responseTimeoutMs);
150
+ } finally {
151
+ await cleanupProcess(process, cleanupGraceMs);
152
+ }
153
+ },
154
+ };
155
+ }
156
+
157
+ /** The production skill probe, backed by Bun's short-lived pi process. */
158
+ export const liveSkillProbe = createSkillProbe(
159
+ createBunSkillProbeProcessFactory((options) => Bun.spawn({ ...options, cmd: [...options.cmd] })),
160
+ );
161
+
162
+ async function cleanupProcess(process: SkillProbeProcess, graceMs: number): Promise<void> {
163
+ process.stdin.end();
164
+ if (!(await exitsWithin(process.exited, graceMs))) process.kill("SIGKILL");
165
+ await process.exited;
166
+ }
167
+
168
+ async function exitsWithin(exited: Promise<number>, timeoutMs: number): Promise<boolean> {
169
+ let timer: ReturnType<typeof setTimeout> | undefined;
170
+ try {
171
+ return await Promise.race([
172
+ exited.then(() => true),
173
+ new Promise<boolean>((resolve) => {
174
+ timer = setTimeout(() => resolve(false), timeoutMs);
175
+ }),
176
+ ]);
177
+ } finally {
178
+ clearTimeout(timer);
179
+ }
180
+ }
181
+
182
+ async function awaitResponse(
183
+ stdout: ReadableStream<Uint8Array>,
184
+ exited: Promise<number>,
185
+ id: string,
186
+ timeoutMs: number,
187
+ ): Promise<readonly DiscoveredSkill[]> {
188
+ let timer: ReturnType<typeof setTimeout> | undefined;
189
+ try {
190
+ return await Promise.race([
191
+ readResponse(stdout, id),
192
+ exited.then(() => {
193
+ throw new Error("pi exited before answering get_commands");
194
+ }),
195
+ new Promise<never>((_resolve, reject) => {
196
+ timer = setTimeout(() => reject(new Error("pi did not answer get_commands")), timeoutMs);
197
+ }),
198
+ ]);
199
+ } finally {
200
+ clearTimeout(timer);
201
+ }
202
+ }
203
+
204
+ async function readResponse(
205
+ stdout: ReadableStream<Uint8Array>,
206
+ id: string,
207
+ ): Promise<readonly DiscoveredSkill[]> {
208
+ for await (const line of lines(stdout)) {
209
+ const frame = parseJson(line);
210
+ if (!isRecord(frame) || frame.type !== "response" || frame.id !== id) continue;
211
+ return parseSkillCommands(frame);
212
+ }
213
+ throw new Error("pi exited before answering get_commands");
214
+ }
215
+
216
+ async function* lines(stream: ReadableStream<Uint8Array>): AsyncGenerator<string> {
217
+ const reader = stream.getReader();
218
+ const decoder = new TextDecoder();
219
+ let pending = "";
220
+ try {
221
+ for (;;) {
222
+ const { done, value } = await reader.read();
223
+ pending += decoder.decode(value, { stream: !done });
224
+ let newline = pending.indexOf("\n");
225
+ while (newline !== -1) {
226
+ yield pending.slice(0, newline);
227
+ pending = pending.slice(newline + 1);
228
+ newline = pending.indexOf("\n");
229
+ }
230
+ if (done) return;
231
+ }
232
+ } finally {
233
+ reader.releaseLock();
234
+ }
235
+ }
236
+
237
+ function parseJson(line: string): unknown {
238
+ try {
239
+ return JSON.parse(line);
240
+ } catch {
241
+ throw new Error("pi emitted malformed JSON while discovering skills");
242
+ }
243
+ }
244
+
245
+ function isRecord(value: unknown): value is Record<string, unknown> {
246
+ return typeof value === "object" && value !== null && !Array.isArray(value);
247
+ }
@@ -0,0 +1,76 @@
1
+ import type { DiscoveredSkill, SkillProbeFactory } from "./skill-probe.ts";
2
+ import type {
3
+ AgentTransport,
4
+ OpenOptions,
5
+ TransportFactory,
6
+ TransportStartupObserver,
7
+ } from "./transport.ts";
8
+
9
+ /**
10
+ * Restricts live Agent skills by resolving pi's enabled names before startup.
11
+ *
12
+ * The cache belongs to one Run: concurrent requests for one cwd share its probe.
13
+ */
14
+ export function skillRestrictionTransport(
15
+ inner: TransportFactory,
16
+ probe: SkillProbeFactory,
17
+ ): TransportFactory {
18
+ const discoveries = new Map<string, Promise<readonly DiscoveredSkill[]>>();
19
+ return {
20
+ async open(
21
+ options: OpenOptions,
22
+ observeStartup?: TransportStartupObserver,
23
+ ): Promise<AgentTransport> {
24
+ if (options.skills === undefined) {
25
+ if (options.inherit !== true) return inner.open(options, observeStartup);
26
+ if (options.disallowedSkills === undefined) return inner.open(options, observeStartup);
27
+ }
28
+ if (options.skills?.length === 0) {
29
+ return inner.open({ ...options, resolvedSkillPaths: [] }, observeStartup);
30
+ }
31
+ const discovered = await cachedDiscovery(discoveries, probe, options.cwd);
32
+ const resolvedSkillPaths = restrict(discovered, options.skills, options.disallowedSkills);
33
+ return inner.open({ ...options, resolvedSkillPaths }, observeStartup);
34
+ },
35
+ };
36
+ }
37
+
38
+ function cachedDiscovery(
39
+ discoveries: Map<string, Promise<readonly DiscoveredSkill[]>>,
40
+ probe: SkillProbeFactory,
41
+ cwd: string,
42
+ ): Promise<readonly DiscoveredSkill[]> {
43
+ const cached = discoveries.get(cwd);
44
+ if (cached) return cached;
45
+ const pending = probe.probe(cwd);
46
+ discoveries.set(cwd, pending);
47
+ return pending;
48
+ }
49
+
50
+ function restrict(
51
+ discovered: readonly DiscoveredSkill[],
52
+ allowlist: readonly string[] | undefined,
53
+ denylist: readonly string[] | undefined,
54
+ ): readonly string[] {
55
+ const byName = new Map<string, DiscoveredSkill>();
56
+ for (const skill of discovered) {
57
+ if (!byName.has(skill.name)) byName.set(skill.name, skill);
58
+ }
59
+ const unknown = [...(allowlist ?? []), ...(denylist ?? [])].filter((name) => !byName.has(name));
60
+ if (unknown.length > 0) throw new Error(`unknown skills: ${[...new Set(unknown)].join(", ")}`);
61
+ const undeniable = (denylist ?? []).filter((name) => byName.get(name)?.extensionInjected);
62
+ if (undeniable.length > 0) {
63
+ throw new Error(
64
+ `cannot deny extension-runtime-injected skills: ${[...new Set(undeniable)].join(", ")}`,
65
+ );
66
+ }
67
+ const denied = new Set(denylist);
68
+ const surviving = allowlist === undefined ? discovered.map((skill) => skill.name) : allowlist;
69
+ return surviving
70
+ .filter((name) => !denied.has(name))
71
+ .map((name) => {
72
+ const skill = byName.get(name);
73
+ if (!skill) throw new Error(`unknown skill: ${name}`);
74
+ return skill.path;
75
+ });
76
+ }
package/src/spawn.ts ADDED
@@ -0,0 +1,241 @@
1
+ import { resolve } from "node:path";
2
+ import { Agent } from "./agent.ts";
3
+ import { uniqueAgentName } from "./agent-names.ts";
4
+ import { type AgentDefinition, agentDefinitionConfig, isAgentDefinition } from "./define-agent.ts";
5
+ import { YaagError } from "./errors.ts";
6
+ import type { EventSink } from "./events.ts";
7
+ import { resolveExtensionPaths } from "./extension-paths.ts";
8
+ import type { RunContext } from "./run-context.ts";
9
+ import type { AgentTransport, TransportFactory, TransportStartup } from "./transport.ts";
10
+ import type { AskOptions, Handle, SpawnOptions, SpawnOverrides } from "./types.ts";
11
+
12
+ /** Dependencies for one Run's Agent-spawn gate. */
13
+ export interface SpawnDependencies {
14
+ readonly factory: TransportFactory;
15
+ readonly agents: Agent[];
16
+ readonly emit: EventSink;
17
+ readonly sessionDir: string | undefined;
18
+ readonly programFile: string | undefined;
19
+ }
20
+
21
+ /** A RunContext spawn function that can synchronously stop accepting new Agents. */
22
+ export interface SpawnGate {
23
+ readonly spawn: RunContext["spawn"];
24
+ close(): void;
25
+ }
26
+
27
+ /**
28
+ * Builds the Run-scoped Agent spawn gate.
29
+ *
30
+ * Allocates unique names within one Run and rejects with RUN_CLOSED once closed.
31
+ * Successfully opened Agents are registered immediately so Run cleanup reaps them.
32
+ */
33
+ export function makeSpawn(deps: SpawnDependencies): SpawnGate {
34
+ const taken = new Set<string>();
35
+ let closed = false;
36
+ const spawn: RunContext["spawn"] = async (
37
+ definitionOrOptions: SpawnOptions | AgentDefinition = {},
38
+ overrides?: SpawnOverrides,
39
+ ): Promise<Handle> => {
40
+ if (closed) throw new YaagError("RUN_CLOSED", "run has already settled");
41
+ const request = resolveRequest(definitionOrOptions, overrides);
42
+ const cwd = resolve(request.spawnOptions.cwd ?? process.cwd());
43
+ const name = uniqueAgentName(request.spawnOptions.name, deps.agents.length, taken);
44
+ try {
45
+ const opened = await openTransport({
46
+ factory: deps.factory,
47
+ name,
48
+ cwd,
49
+ spawnOptions: request.spawnOptions,
50
+ sessionDir: deps.sessionDir,
51
+ programFile: deps.programFile,
52
+ });
53
+ const resolvedCwd = opened.startup.worktree?.cwd ?? cwd;
54
+ const branch = opened.startup.worktree?.branch;
55
+ const sessionFile = opened.startup.sessionFile;
56
+ const agent = new Agent({
57
+ name,
58
+ cwd: resolvedCwd,
59
+ branch,
60
+ transport: opened.transport,
61
+ emit: deps.emit,
62
+ spawnOptions: request.spawnOptions,
63
+ ...(request.askDefaults === undefined ? {} : { askDefaults: request.askDefaults }),
64
+ ...(request.definitionName === undefined ? {} : { definitionName: request.definitionName }),
65
+ });
66
+ deps.agents.push(agent);
67
+ deps.emit({
68
+ type: "agent_spawn",
69
+ agent: name,
70
+ model: agent.model,
71
+ cwd: resolvedCwd,
72
+ ...(branch === undefined ? {} : { branch }),
73
+ ...(sessionFile === undefined ? {} : { sessionFile }),
74
+ });
75
+ return agent;
76
+ } catch (error) {
77
+ if (request.definitionName !== undefined && isSpawnFailure(error)) {
78
+ throw new YaagError(
79
+ "SPAWN_FAILED",
80
+ `definition "${request.definitionName}": ${error.message}`,
81
+ name,
82
+ );
83
+ }
84
+ throw error;
85
+ }
86
+ };
87
+ return {
88
+ spawn,
89
+ close: (): void => {
90
+ closed = true;
91
+ },
92
+ };
93
+ }
94
+
95
+ type MutableSpawnOverrides = { -readonly [Key in keyof SpawnOverrides]: SpawnOverrides[Key] };
96
+
97
+ interface SpawnRequest {
98
+ readonly spawnOptions: SpawnOptions;
99
+ readonly askDefaults: AskOptions | undefined;
100
+ /** Definition identity stays separate from a topology-overridden Agent name. */
101
+ readonly definitionName: string | undefined;
102
+ }
103
+
104
+ function resolveRequest(
105
+ definitionOrOptions: SpawnOptions | AgentDefinition,
106
+ overrides: SpawnOverrides | undefined,
107
+ ): SpawnRequest {
108
+ if (!isAgentDefinition(definitionOrOptions)) {
109
+ return { spawnOptions: definitionOrOptions, askDefaults: undefined, definitionName: undefined };
110
+ }
111
+ const topology = validateOverrides(overrides);
112
+ const config = agentDefinitionConfig(definitionOrOptions);
113
+ return {
114
+ spawnOptions: {
115
+ name: topology.name ?? config.name,
116
+ ...(topology.cwd === undefined ? {} : { cwd: topology.cwd }),
117
+ ...(topology.worktree === undefined ? {} : { worktree: topology.worktree }),
118
+ ...(config.model === undefined ? {} : { model: config.model }),
119
+ ...(config.thinking === undefined ? {} : { thinking: config.thinking }),
120
+ ...(config.tools === undefined ? {} : { tools: config.tools }),
121
+ ...(config.disallowedTools === undefined ? {} : { disallowedTools: config.disallowedTools }),
122
+ ...(config.skills === undefined ? {} : { skills: config.skills }),
123
+ ...(config.disallowedSkills === undefined
124
+ ? {}
125
+ : { disallowedSkills: config.disallowedSkills }),
126
+ ...(config.prompt === undefined
127
+ ? {}
128
+ : config.overrideSystemPrompt === true
129
+ ? { systemPrompt: config.prompt }
130
+ : { appendSystemPrompt: config.prompt }),
131
+ },
132
+ askDefaults: config.askDefaults,
133
+ definitionName: config.name,
134
+ };
135
+ }
136
+
137
+ function validateOverrides(overrides: unknown): SpawnOverrides {
138
+ if (overrides === undefined) return {};
139
+ if (typeof overrides !== "object" || overrides === null || Array.isArray(overrides)) {
140
+ throw new TypeError(
141
+ "spawn overrides must be an object: definitions own policy and spawn overrides own topology",
142
+ );
143
+ }
144
+ for (const key of Object.keys(overrides)) {
145
+ if (key !== "name" && key !== "cwd" && key !== "worktree") {
146
+ throw new TypeError(
147
+ `spawn override "${key}" is not allowed: definitions own policy and spawn overrides own topology`,
148
+ );
149
+ }
150
+ }
151
+ const topology: MutableSpawnOverrides = {};
152
+ for (const [key, value] of Object.entries(overrides)) {
153
+ if (key === "name") {
154
+ if (value !== undefined && typeof value !== "string") {
155
+ throw new TypeError('spawn override "name" must be a string when present');
156
+ }
157
+ topology.name = value;
158
+ }
159
+ if (key === "cwd") {
160
+ if (value !== undefined && typeof value !== "string") {
161
+ throw new TypeError('spawn override "cwd" must be a string when present');
162
+ }
163
+ topology.cwd = value;
164
+ }
165
+ if (key === "worktree") {
166
+ if (value !== undefined && typeof value !== "boolean") {
167
+ throw new TypeError('spawn override "worktree" must be a boolean when present');
168
+ }
169
+ topology.worktree = value;
170
+ }
171
+ }
172
+ return topology;
173
+ }
174
+
175
+ function isSpawnFailure(error: unknown): error is YaagError {
176
+ return error instanceof YaagError && error.code === "SPAWN_FAILED";
177
+ }
178
+
179
+ interface OpenTransportOptions {
180
+ readonly factory: TransportFactory;
181
+ readonly name: string;
182
+ readonly cwd: string;
183
+ readonly spawnOptions: SpawnOptions;
184
+ readonly sessionDir: string | undefined;
185
+ readonly programFile: string | undefined;
186
+ }
187
+
188
+ interface OpenedTransport {
189
+ readonly transport: AgentTransport;
190
+ readonly startup: TransportStartup;
191
+ }
192
+
193
+ async function openTransport(options: OpenTransportOptions): Promise<OpenedTransport> {
194
+ const startup: TransportStartup = {};
195
+ try {
196
+ const resolvedExtensionPaths =
197
+ options.spawnOptions.extensions === undefined
198
+ ? undefined
199
+ : await resolveExtensionPaths(options.spawnOptions.extensions, {
200
+ ...(options.programFile === undefined ? {} : { programFile: options.programFile }),
201
+ projectRoot: options.cwd,
202
+ });
203
+ const transport = await options.factory.open(
204
+ {
205
+ cwd: options.cwd,
206
+ name: options.name,
207
+ ...(options.spawnOptions.model === undefined ? {} : { model: options.spawnOptions.model }),
208
+ ...(options.spawnOptions.systemPrompt === undefined
209
+ ? {}
210
+ : { systemPrompt: options.spawnOptions.systemPrompt }),
211
+ ...(options.spawnOptions.thinking === undefined
212
+ ? {}
213
+ : { thinking: options.spawnOptions.thinking }),
214
+ ...(options.spawnOptions.appendSystemPrompt === undefined
215
+ ? {}
216
+ : { appendSystemPrompt: options.spawnOptions.appendSystemPrompt }),
217
+ ...(options.spawnOptions.inherit === undefined
218
+ ? {}
219
+ : { inherit: options.spawnOptions.inherit }),
220
+ ...(options.spawnOptions.tools === undefined ? {} : { tools: options.spawnOptions.tools }),
221
+ ...(options.spawnOptions.disallowedTools === undefined
222
+ ? {}
223
+ : { disallowedTools: options.spawnOptions.disallowedTools }),
224
+ ...(options.spawnOptions.skills === undefined
225
+ ? {}
226
+ : { skills: options.spawnOptions.skills }),
227
+ ...(options.spawnOptions.disallowedSkills === undefined
228
+ ? {}
229
+ : { disallowedSkills: options.spawnOptions.disallowedSkills }),
230
+ ...(resolvedExtensionPaths === undefined ? {} : { resolvedExtensionPaths }),
231
+ ...(options.spawnOptions.worktree === true ? { worktree: true } : {}),
232
+ ...(options.sessionDir === undefined ? {} : { sessionDir: options.sessionDir }),
233
+ },
234
+ (report) => Object.assign(startup, report),
235
+ );
236
+ return { transport, startup };
237
+ } catch (error) {
238
+ if (error instanceof YaagError) throw error;
239
+ throw new YaagError("SPAWN_FAILED", `agent "${options.name}": ${String(error)}`, options.name);
240
+ }
241
+ }