@cabane/companion 0.6.0 → 0.6.1
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 +15 -7
- package/dist/cli.js +523 -1696
- package/dist/pairing-config.js +29 -53
- package/dist/runtime.js +441 -1546
- package/dist/static/index.html +2 -3
- package/package.json +2 -2
package/dist/runtime.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "fs";
|
|
14
14
|
import { homedir, userInfo } from "os";
|
|
15
15
|
import { dirname, join } from "path";
|
|
16
|
-
import { z as
|
|
16
|
+
import { z as z2 } from "zod";
|
|
17
17
|
|
|
18
18
|
// src/errors.ts
|
|
19
19
|
var CompanionError = class extends Error {
|
|
@@ -46,9 +46,6 @@ var PrepareHookError = class extends CompanionError {
|
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
// src/pairing.ts
|
|
49
|
-
import { z } from "zod";
|
|
50
|
-
var PAIRING_VERSION = 1;
|
|
51
|
-
var DEVICE_TOKEN_PREFIX = "cabdev_";
|
|
52
49
|
function isAllowedBaseUrl(raw) {
|
|
53
50
|
let url;
|
|
54
51
|
try {
|
|
@@ -63,45 +60,24 @@ function isAllowedBaseUrl(raw) {
|
|
|
63
60
|
}
|
|
64
61
|
return false;
|
|
65
62
|
}
|
|
66
|
-
var pairingSchema = z.object({
|
|
67
|
-
// Bumped if the wire shape changes incompatibly. We only accept v1.
|
|
68
|
-
v: z.literal(PAIRING_VERSION),
|
|
69
|
-
baseUrl: z.string().url().refine(isAllowedBaseUrl, {
|
|
70
|
-
message: "baseUrl must be https (loopback http is allowed for local dev only)"
|
|
71
|
-
}),
|
|
72
|
-
// The `cabdev_` device token plaintext — the companion's one durable credential.
|
|
73
|
-
deviceToken: z.string().min(1).startsWith(DEVICE_TOKEN_PREFIX, {
|
|
74
|
-
message: 'deviceToken must be a cabane device token (starts with "cabdev_")'
|
|
75
|
-
}),
|
|
76
|
-
// Optional identity hints the app may include for nicer local display. The
|
|
77
|
-
// companion also learns these from the first assignments pull, so they're not
|
|
78
|
-
// required.
|
|
79
|
-
deviceId: z.string().min(1).optional(),
|
|
80
|
-
deviceLabel: z.string().min(1).optional()
|
|
81
|
-
});
|
|
82
63
|
|
|
83
64
|
// src/prepare-hook.ts
|
|
84
65
|
import { spawn } from "child_process";
|
|
85
|
-
import { z
|
|
86
|
-
var prepareHookSchema =
|
|
87
|
-
command:
|
|
88
|
-
args:
|
|
66
|
+
import { z } from "zod";
|
|
67
|
+
var prepareHookSchema = z.object({
|
|
68
|
+
command: z.string().min(1),
|
|
69
|
+
args: z.array(z.string()).optional(),
|
|
89
70
|
// Extra env handed to the hook process itself (merged over process.env).
|
|
90
|
-
env:
|
|
71
|
+
env: z.record(z.string(), z.string()).optional(),
|
|
91
72
|
// Wall-clock cap for the hook. Provisioning is slow (minutes), so the
|
|
92
73
|
// default is generous; a hook that hangs past this is killed and the turn
|
|
93
74
|
// fails with a clear timeout message rather than pinning the companion.
|
|
94
|
-
timeoutMs:
|
|
75
|
+
timeoutMs: z.number().int().positive().optional()
|
|
95
76
|
}).strict();
|
|
96
77
|
var DEFAULT_TIMEOUT_MS = 10 * 6e4;
|
|
97
|
-
var prepareResultSchema =
|
|
98
|
-
cwd:
|
|
99
|
-
env:
|
|
100
|
-
nativeWorkAssignment: z2.object({
|
|
101
|
-
itemId: z2.string(),
|
|
102
|
-
executionId: z2.string(),
|
|
103
|
-
activationEpoch: z2.number().int().nonnegative()
|
|
104
|
-
}).strict().optional()
|
|
78
|
+
var prepareResultSchema = z.object({
|
|
79
|
+
cwd: z.string().min(1),
|
|
80
|
+
env: z.record(z.string(), z.string()).optional()
|
|
105
81
|
});
|
|
106
82
|
function parsePrepareOutput(stdout) {
|
|
107
83
|
const last = stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0).at(-1);
|
|
@@ -120,13 +96,12 @@ function parsePrepareOutput(stdout) {
|
|
|
120
96
|
const r = prepareResultSchema.safeParse(parsed);
|
|
121
97
|
if (!r.success) {
|
|
122
98
|
throw new PrepareHookError(
|
|
123
|
-
'prepare hook JSON must carry a non-empty string "cwd" (plus optional "env"
|
|
99
|
+
'prepare hook JSON must carry a non-empty string "cwd" (plus optional "env")'
|
|
124
100
|
);
|
|
125
101
|
}
|
|
126
102
|
return {
|
|
127
103
|
cwd: r.data.cwd,
|
|
128
|
-
...r.data.env ? { env: r.data.env } : {}
|
|
129
|
-
...r.data.nativeWorkAssignment ? { nativeWorkAssignment: r.data.nativeWorkAssignment } : {}
|
|
104
|
+
...r.data.env ? { env: r.data.env } : {}
|
|
130
105
|
};
|
|
131
106
|
}
|
|
132
107
|
return { cwd: last };
|
|
@@ -164,10 +139,10 @@ var runPrepareHook = (hook, input) => {
|
|
|
164
139
|
CABANE_CONVERSATION_TITLE: input.title ?? ""
|
|
165
140
|
}
|
|
166
141
|
});
|
|
167
|
-
} catch (
|
|
142
|
+
} catch (err) {
|
|
168
143
|
reject(
|
|
169
144
|
new PrepareHookError(
|
|
170
|
-
`prepare hook failed to start: ${
|
|
145
|
+
`prepare hook failed to start: ${err instanceof Error ? err.message : String(err)}`
|
|
171
146
|
)
|
|
172
147
|
);
|
|
173
148
|
return;
|
|
@@ -187,8 +162,8 @@ var runPrepareHook = (hook, input) => {
|
|
|
187
162
|
child.stderr?.on("data", (d) => {
|
|
188
163
|
stderr += d.toString();
|
|
189
164
|
});
|
|
190
|
-
child.on("error", (
|
|
191
|
-
finish(() => reject(new PrepareHookError(`prepare hook failed to start: ${
|
|
165
|
+
child.on("error", (err) => {
|
|
166
|
+
finish(() => reject(new PrepareHookError(`prepare hook failed to start: ${err.message}`)));
|
|
192
167
|
});
|
|
193
168
|
child.on("close", (code) => {
|
|
194
169
|
finish(() => {
|
|
@@ -201,8 +176,8 @@ var runPrepareHook = (hook, input) => {
|
|
|
201
176
|
}
|
|
202
177
|
try {
|
|
203
178
|
resolve(parsePrepareOutput(stdout));
|
|
204
|
-
} catch (
|
|
205
|
-
reject(
|
|
179
|
+
} catch (err) {
|
|
180
|
+
reject(err instanceof PrepareHookError ? err : new PrepareHookError(String(err)));
|
|
206
181
|
}
|
|
207
182
|
});
|
|
208
183
|
});
|
|
@@ -241,8 +216,8 @@ function cabaneDir() {
|
|
|
241
216
|
function configPath() {
|
|
242
217
|
return join(cabaneDir(), "config.json");
|
|
243
218
|
}
|
|
244
|
-
var localAgentConfigSchema =
|
|
245
|
-
cwd:
|
|
219
|
+
var localAgentConfigSchema = z2.object({
|
|
220
|
+
cwd: z2.string().optional(),
|
|
246
221
|
prepareHook: prepareHookSchema.optional(),
|
|
247
222
|
// CT289: the Claude Code auto-memory escape hatch. Auto-memory is forced OFF
|
|
248
223
|
// by default on every companion (memory belongs in the Cabane workspace, and a
|
|
@@ -251,13 +226,13 @@ var localAgentConfigSchema = z3.object({
|
|
|
251
226
|
// auto-memory back to your own `~/.claude/settings.json` — Cabane then stops
|
|
252
227
|
// injecting the off switch and your normal Claude Code memory workflow applies
|
|
253
228
|
// (in coding mode, where the checkout's project settings are read).
|
|
254
|
-
claudeCode:
|
|
229
|
+
claudeCode: z2.object({ autoMemory: z2.boolean().optional() }).strict().optional()
|
|
255
230
|
}).strict();
|
|
256
|
-
var companionConfigSchema =
|
|
231
|
+
var companionConfigSchema = z2.object({
|
|
257
232
|
// The cabane instance this device is paired with. SJ515: https-enforced
|
|
258
233
|
// (loopback exempt) so a hand-edited config can't smuggle a plaintext-http
|
|
259
234
|
// base URL onto the MITM-able channel the device token + prompt ride.
|
|
260
|
-
baseUrl:
|
|
235
|
+
baseUrl: z2.string().url().refine(isAllowedBaseUrl, {
|
|
261
236
|
message: "baseUrl must be https (loopback http is allowed for local dev only)"
|
|
262
237
|
}),
|
|
263
238
|
// The `cabdev_` device token plaintext — the companion's one durable credential,
|
|
@@ -265,22 +240,22 @@ var companionConfigSchema = z3.object({
|
|
|
265
240
|
// heartbeat endpoints. Optional so `cabane-companion logout` can strip it (a
|
|
266
241
|
// "paired but logged out" state the supervisor refuses to run) while keeping
|
|
267
242
|
// the rest of the config; `pair` always writes one.
|
|
268
|
-
deviceToken:
|
|
269
|
-
// Identity hints, learned from the
|
|
243
|
+
deviceToken: z2.string().optional(),
|
|
244
|
+
// Identity hints, learned from the device flow and refreshed on the first
|
|
270
245
|
// assignments pull. Cosmetic — used for `status`/dashboard display only.
|
|
271
|
-
deviceId:
|
|
272
|
-
deviceLabel:
|
|
246
|
+
deviceId: z2.string().optional(),
|
|
247
|
+
deviceLabel: z2.string().optional(),
|
|
273
248
|
// Optional per-agent machine-local overrides (cwd / prepareHook), keyed by
|
|
274
249
|
// agentId / username / `slug/username`. Hand-added by the operator; the companion
|
|
275
250
|
// never writes this (it only persists credentials + the device, elsewhere).
|
|
276
|
-
agents:
|
|
251
|
+
agents: z2.record(z2.string(), localAgentConfigSchema).optional(),
|
|
277
252
|
// Dashboard settings (all optional). dashboardPort: preferred bind port (next
|
|
278
253
|
// free one if taken); autoOpen: whether `start` opens the browser (the
|
|
279
254
|
// `--no-open` flag / `COMPANION_NO_OPEN=1` override per-run); logLevel: pino
|
|
280
255
|
// level, live-editable from the dashboard settings panel.
|
|
281
|
-
dashboardPort:
|
|
282
|
-
autoOpen:
|
|
283
|
-
logLevel:
|
|
256
|
+
dashboardPort: z2.number().int().min(1).max(65535).optional(),
|
|
257
|
+
autoOpen: z2.boolean().optional(),
|
|
258
|
+
logLevel: z2.enum(["warn", "info", "debug"]).optional(),
|
|
284
259
|
// CT270: the opencode runtime, when the operator runs one on this machine. The
|
|
285
260
|
// operator installs opencode, starts `opencode serve` (auth via opencode's own
|
|
286
261
|
// `/connect` — Cabane never sees provider keys), and points the companion at it
|
|
@@ -288,8 +263,8 @@ var companionConfigSchema = z3.object({
|
|
|
288
263
|
// heartbeat manifest (so the server offers DeepSeek/opencode models here and
|
|
289
264
|
// routes those turns to this device) AND registers the opencode adapter in the
|
|
290
265
|
// dispatcher. Absent → the device is claude-code-only, exactly as before.
|
|
291
|
-
opencode:
|
|
292
|
-
serverUrl:
|
|
266
|
+
opencode: z2.object({
|
|
267
|
+
serverUrl: z2.string().url()
|
|
293
268
|
}).strict().optional(),
|
|
294
269
|
// CT481: the codex runtime, when the operator runs Codex on this machine. Unlike
|
|
295
270
|
// opencode (a long-lived `opencode serve` addressed by URL), Codex is a local CLI
|
|
@@ -300,20 +275,13 @@ var companionConfigSchema = z3.object({
|
|
|
300
275
|
// keeps the block but turns it off). Enabling makes the device advertise the
|
|
301
276
|
// `codex` runtime on its heartbeat manifest AND registers the codex adapter in
|
|
302
277
|
// the dispatcher. Absent → the device doesn't offer codex, exactly as before.
|
|
303
|
-
codex:
|
|
304
|
-
enabled:
|
|
278
|
+
codex: z2.object({
|
|
279
|
+
enabled: z2.boolean().optional()
|
|
305
280
|
}).strict().optional()
|
|
306
281
|
});
|
|
307
282
|
function isCodexEnabled(cfg) {
|
|
308
283
|
return !!cfg.codex && cfg.codex.enabled !== false;
|
|
309
284
|
}
|
|
310
|
-
function cabaneNativeApiKey() {
|
|
311
|
-
const key = process.env.OPENROUTER_API_KEY?.trim();
|
|
312
|
-
return key ? key : void 0;
|
|
313
|
-
}
|
|
314
|
-
function isCabaneNativeEnabled() {
|
|
315
|
-
return cabaneNativeApiKey() !== void 0;
|
|
316
|
-
}
|
|
317
285
|
function localAgentConfig(cfg, agent) {
|
|
318
286
|
const map = cfg.agents ?? {};
|
|
319
287
|
return map[agent.agentId] ?? map[agent.agentUsername] ?? map[`${agent.workspaceSlug}/${agent.agentUsername}`] ?? {};
|
|
@@ -324,18 +292,18 @@ function loadConfig() {
|
|
|
324
292
|
let raw;
|
|
325
293
|
try {
|
|
326
294
|
raw = readFileSync(path3, "utf8");
|
|
327
|
-
} catch (
|
|
295
|
+
} catch (err) {
|
|
328
296
|
throw new ConfigError(
|
|
329
|
-
`couldn't read ${path3}: ${
|
|
297
|
+
`couldn't read ${path3}: ${err instanceof Error ? err.message : String(err)}`
|
|
330
298
|
);
|
|
331
299
|
}
|
|
332
300
|
if (raw.trim().length === 0) return null;
|
|
333
301
|
let parsed;
|
|
334
302
|
try {
|
|
335
303
|
parsed = JSON.parse(raw);
|
|
336
|
-
} catch (
|
|
304
|
+
} catch (err) {
|
|
337
305
|
throw new ConfigError(
|
|
338
|
-
`${path3} is not valid JSON: ${
|
|
306
|
+
`${path3} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
339
307
|
);
|
|
340
308
|
}
|
|
341
309
|
const result = companionConfigSchema.safeParse(parsed);
|
|
@@ -367,12 +335,12 @@ function saveConfig(cfg) {
|
|
|
367
335
|
} catch {
|
|
368
336
|
}
|
|
369
337
|
renameSync(tmp, path3);
|
|
370
|
-
} catch (
|
|
338
|
+
} catch (err) {
|
|
371
339
|
try {
|
|
372
340
|
rmSync(tmp, { force: true });
|
|
373
341
|
} catch {
|
|
374
342
|
}
|
|
375
|
-
throw
|
|
343
|
+
throw err;
|
|
376
344
|
}
|
|
377
345
|
}
|
|
378
346
|
function requireConfig() {
|
|
@@ -845,15 +813,15 @@ var DEFAULT_PORT = 7474;
|
|
|
845
813
|
var PORT_FALLBACK_SPAN = 10;
|
|
846
814
|
function buildDashboardApp(deps) {
|
|
847
815
|
const app = new Hono();
|
|
848
|
-
app.onError((
|
|
849
|
-
if (
|
|
850
|
-
const status =
|
|
851
|
-
return c.json({ error:
|
|
816
|
+
app.onError((err, c) => {
|
|
817
|
+
if (err instanceof ApiError) {
|
|
818
|
+
const status = err.status >= 400 && err.status < 600 ? err.status : 502;
|
|
819
|
+
return c.json({ error: err.message }, status);
|
|
852
820
|
}
|
|
853
|
-
if (
|
|
854
|
-
return c.json({ error:
|
|
821
|
+
if (err instanceof CompanionError) {
|
|
822
|
+
return c.json({ error: err.message }, 400);
|
|
855
823
|
}
|
|
856
|
-
return c.json({ error:
|
|
824
|
+
return c.json({ error: err instanceof Error ? err.message : "internal error" }, 500);
|
|
857
825
|
});
|
|
858
826
|
registerRoutes(app, { ...deps, staticDir: resolveStaticDir() });
|
|
859
827
|
return app;
|
|
@@ -874,12 +842,12 @@ async function startDashboard(opts) {
|
|
|
874
842
|
server.closeAllConnections?.();
|
|
875
843
|
})
|
|
876
844
|
};
|
|
877
|
-
} catch (
|
|
878
|
-
if (isAddrInUse(
|
|
879
|
-
lastErr =
|
|
845
|
+
} catch (err) {
|
|
846
|
+
if (isAddrInUse(err)) {
|
|
847
|
+
lastErr = err;
|
|
880
848
|
continue;
|
|
881
849
|
}
|
|
882
|
-
throw
|
|
850
|
+
throw err;
|
|
883
851
|
}
|
|
884
852
|
}
|
|
885
853
|
throw new CompanionError(
|
|
@@ -895,16 +863,16 @@ function listen(app, port) {
|
|
|
895
863
|
resolve(server);
|
|
896
864
|
}
|
|
897
865
|
});
|
|
898
|
-
server.on("error", (
|
|
866
|
+
server.on("error", (err) => {
|
|
899
867
|
if (!settled) {
|
|
900
868
|
settled = true;
|
|
901
|
-
reject(
|
|
869
|
+
reject(err);
|
|
902
870
|
}
|
|
903
871
|
});
|
|
904
872
|
});
|
|
905
873
|
}
|
|
906
|
-
function isAddrInUse(
|
|
907
|
-
return Boolean(
|
|
874
|
+
function isAddrInUse(err) {
|
|
875
|
+
return Boolean(err && typeof err === "object" && "code" in err && err.code === "EADDRINUSE");
|
|
908
876
|
}
|
|
909
877
|
function resolveStaticDir() {
|
|
910
878
|
return join4(dirname3(fileURLToPath(import.meta.url)), "static");
|
|
@@ -1101,8 +1069,8 @@ async function verifyRuntime(state, fetchImpl = fetch) {
|
|
|
1101
1069
|
let res;
|
|
1102
1070
|
try {
|
|
1103
1071
|
res = await fetchImpl(url, { signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) });
|
|
1104
|
-
} catch (
|
|
1105
|
-
return isConnRefused(
|
|
1072
|
+
} catch (err) {
|
|
1073
|
+
return isConnRefused(err) ? "stale" : "unknown";
|
|
1106
1074
|
}
|
|
1107
1075
|
if (!res.ok) return "unknown";
|
|
1108
1076
|
let body;
|
|
@@ -1114,9 +1082,9 @@ async function verifyRuntime(state, fetchImpl = fetch) {
|
|
|
1114
1082
|
if (typeof body.instance_id !== "string") return "unknown";
|
|
1115
1083
|
return body.instance_id === state.instanceId ? "ours" : "stale";
|
|
1116
1084
|
}
|
|
1117
|
-
function isConnRefused(
|
|
1118
|
-
if (!
|
|
1119
|
-
const cause =
|
|
1085
|
+
function isConnRefused(err) {
|
|
1086
|
+
if (!err || typeof err !== "object") return false;
|
|
1087
|
+
const cause = err.cause;
|
|
1120
1088
|
return !!cause && typeof cause === "object" && cause.code === "ECONNREFUSED";
|
|
1121
1089
|
}
|
|
1122
1090
|
function trimSlash(s) {
|
|
@@ -1179,10 +1147,10 @@ var CabaneApi = class {
|
|
|
1179
1147
|
for (let attempt = 1; ; attempt++) {
|
|
1180
1148
|
try {
|
|
1181
1149
|
return await this.attempt(method, path3, body, signal);
|
|
1182
|
-
} catch (
|
|
1183
|
-
if (attempt >= maxAttempts || signal?.aborted || !isRetryable(
|
|
1150
|
+
} catch (err) {
|
|
1151
|
+
if (attempt >= maxAttempts || signal?.aborted || !isRetryable(err)) throw err;
|
|
1184
1152
|
await sleep(RETRY_BACKOFF_MS[attempt - 1], signal);
|
|
1185
|
-
if (signal?.aborted) throw
|
|
1153
|
+
if (signal?.aborted) throw err;
|
|
1186
1154
|
}
|
|
1187
1155
|
}
|
|
1188
1156
|
}
|
|
@@ -1210,14 +1178,14 @@ var CabaneApi = class {
|
|
|
1210
1178
|
retry: true,
|
|
1211
1179
|
...signal ? { signal } : {}
|
|
1212
1180
|
});
|
|
1213
|
-
} catch (
|
|
1181
|
+
} catch (err) {
|
|
1214
1182
|
const outbox = this.opts.outbox;
|
|
1215
|
-
if (!outbox) throw
|
|
1216
|
-
if (signal?.aborted || isAbortError(
|
|
1217
|
-
if (!isRetryable(
|
|
1183
|
+
if (!outbox) throw err;
|
|
1184
|
+
if (signal?.aborted || isAbortError(err)) throw err;
|
|
1185
|
+
if (!isRetryable(err)) throw err;
|
|
1218
1186
|
outbox.persist({ enqueuedAt: Date.now(), turnId, seq, method: "POST", path: path3, body, kind });
|
|
1219
1187
|
this.opts.log?.warn(
|
|
1220
|
-
{ kind, turnId, seq, err:
|
|
1188
|
+
{ kind, turnId, seq, err: err instanceof Error ? err.message : String(err) },
|
|
1221
1189
|
"companion: commit queued to outbox after transient failure (will drain when the API returns)"
|
|
1222
1190
|
);
|
|
1223
1191
|
}
|
|
@@ -1241,10 +1209,10 @@ var CabaneApi = class {
|
|
|
1241
1209
|
await this.request(entry.method, entry.path, entry.body, { retry: true });
|
|
1242
1210
|
outbox.remove(entry.turnId, entry.seq);
|
|
1243
1211
|
progressed = true;
|
|
1244
|
-
} catch (
|
|
1245
|
-
if (
|
|
1212
|
+
} catch (err) {
|
|
1213
|
+
if (err instanceof ApiError && err.status >= 400 && err.status < 500) {
|
|
1246
1214
|
this.opts.log?.warn(
|
|
1247
|
-
{ kind: entry.kind, turnId: entry.turnId, seq: entry.seq, status:
|
|
1215
|
+
{ kind: entry.kind, turnId: entry.turnId, seq: entry.seq, status: err.status },
|
|
1248
1216
|
"companion outbox: discarding entry on terminal 4xx (will never land)"
|
|
1249
1217
|
);
|
|
1250
1218
|
outbox.remove(entry.turnId, entry.seq);
|
|
@@ -1392,11 +1360,11 @@ var CabaneApi = class {
|
|
|
1392
1360
|
try {
|
|
1393
1361
|
await this.request("PATCH", path3, body, { retry: true });
|
|
1394
1362
|
outbox?.remove(key, ACTIVE_RUN_OUTBOX_SEQ);
|
|
1395
|
-
} catch (
|
|
1396
|
-
if (!outbox) throw
|
|
1397
|
-
if (!isRetryable(
|
|
1363
|
+
} catch (err) {
|
|
1364
|
+
if (!outbox) throw err;
|
|
1365
|
+
if (!isRetryable(err)) {
|
|
1398
1366
|
outbox.remove(key, ACTIVE_RUN_OUTBOX_SEQ);
|
|
1399
|
-
throw
|
|
1367
|
+
throw err;
|
|
1400
1368
|
}
|
|
1401
1369
|
outbox.persist({
|
|
1402
1370
|
enqueuedAt: Date.now(),
|
|
@@ -1408,7 +1376,7 @@ var CabaneApi = class {
|
|
|
1408
1376
|
kind: "active-run"
|
|
1409
1377
|
});
|
|
1410
1378
|
this.opts.log?.warn(
|
|
1411
|
-
{ conversationId, agentId, err:
|
|
1379
|
+
{ conversationId, agentId, err: err instanceof Error ? err.message : String(err) },
|
|
1412
1380
|
"companion: active-run write queued to outbox after transient failure (will drain when the API returns)"
|
|
1413
1381
|
);
|
|
1414
1382
|
}
|
|
@@ -1507,13 +1475,13 @@ var CabaneApi = class {
|
|
|
1507
1475
|
return res.messages.find((m) => m.id === messageId2) ?? null;
|
|
1508
1476
|
}
|
|
1509
1477
|
};
|
|
1510
|
-
function isRetryable(
|
|
1511
|
-
if (
|
|
1512
|
-
if (isAbortError(
|
|
1478
|
+
function isRetryable(err) {
|
|
1479
|
+
if (err instanceof ApiError) return err.status >= 500;
|
|
1480
|
+
if (isAbortError(err)) return false;
|
|
1513
1481
|
return true;
|
|
1514
1482
|
}
|
|
1515
|
-
function isAbortError(
|
|
1516
|
-
return
|
|
1483
|
+
function isAbortError(err) {
|
|
1484
|
+
return err instanceof Error && err.name === "AbortError";
|
|
1517
1485
|
}
|
|
1518
1486
|
function sleep(ms, signal) {
|
|
1519
1487
|
return new Promise((resolve) => {
|
|
@@ -1531,8 +1499,8 @@ function sleep(ms, signal) {
|
|
|
1531
1499
|
}
|
|
1532
1500
|
function errorMessage(status, body) {
|
|
1533
1501
|
if (body && typeof body === "object" && "error" in body) {
|
|
1534
|
-
const
|
|
1535
|
-
if (typeof
|
|
1502
|
+
const err = body.error;
|
|
1503
|
+
if (typeof err === "string") return `${status} ${err}`;
|
|
1536
1504
|
}
|
|
1537
1505
|
if (typeof body === "string" && body.length > 0) return `${status} ${body.slice(0, 200)}`;
|
|
1538
1506
|
return `${status} error`;
|
|
@@ -1588,8 +1556,8 @@ var DeviceApi = class {
|
|
|
1588
1556
|
};
|
|
1589
1557
|
function errorMessage2(status, body) {
|
|
1590
1558
|
if (body && typeof body === "object" && "error" in body) {
|
|
1591
|
-
const
|
|
1592
|
-
if (typeof
|
|
1559
|
+
const err = body.error;
|
|
1560
|
+
if (typeof err === "string") return `${status} ${err}`;
|
|
1593
1561
|
}
|
|
1594
1562
|
if (typeof body === "string" && body.length > 0) return `${status} ${body.slice(0, 200)}`;
|
|
1595
1563
|
return `${status} error`;
|
|
@@ -1606,11 +1574,11 @@ import {
|
|
|
1606
1574
|
writeFileSync as writeFileSync3
|
|
1607
1575
|
} from "fs";
|
|
1608
1576
|
import { dirname as dirname4, join as join6 } from "path";
|
|
1609
|
-
import { z as
|
|
1577
|
+
import { z as z3 } from "zod";
|
|
1610
1578
|
function credentialsPath() {
|
|
1611
1579
|
return join6(cabaneDir(), "credentials.json");
|
|
1612
1580
|
}
|
|
1613
|
-
var credentialStoreSchema =
|
|
1581
|
+
var credentialStoreSchema = z3.record(z3.string(), z3.string());
|
|
1614
1582
|
function load() {
|
|
1615
1583
|
const path3 = credentialsPath();
|
|
1616
1584
|
if (!existsSync4(path3)) return {};
|
|
@@ -1643,12 +1611,12 @@ function save(map) {
|
|
|
1643
1611
|
} catch {
|
|
1644
1612
|
}
|
|
1645
1613
|
renameSync2(tmp, path3);
|
|
1646
|
-
} catch (
|
|
1614
|
+
} catch (err) {
|
|
1647
1615
|
try {
|
|
1648
1616
|
rmSync3(tmp, { force: true });
|
|
1649
1617
|
} catch {
|
|
1650
1618
|
}
|
|
1651
|
-
throw
|
|
1619
|
+
throw err;
|
|
1652
1620
|
}
|
|
1653
1621
|
}
|
|
1654
1622
|
function getCredential(agentId) {
|
|
@@ -1821,44 +1789,44 @@ function noResume() {
|
|
|
1821
1789
|
var TURN_PROTOCOL_VERSION = 1;
|
|
1822
1790
|
|
|
1823
1791
|
// packages/agent-runtime/src/host-policy.ts
|
|
1824
|
-
import { z as
|
|
1825
|
-
var hostPolicySchema =
|
|
1792
|
+
import { z as z4 } from "zod";
|
|
1793
|
+
var hostPolicySchema = z4.object({
|
|
1826
1794
|
// Host filesystem + shell: `Bash`/`Read`/`Write`/`Edit`/`Glob`/`Grep`, the
|
|
1827
1795
|
// notebook read/write pair, git-worktree tools, and the `REPL` host code-exec
|
|
1828
1796
|
// tool. Off under the locked assistant surface (today's `DISALLOWED_TOOLS`);
|
|
1829
1797
|
// on under `coding` mode.
|
|
1830
|
-
hostFs:
|
|
1798
|
+
hostFs: z4.boolean(),
|
|
1831
1799
|
// Public web (today's `WebSearch` / `WebFetch`, the `DEFAULT_WEB_TOOLS`). Public
|
|
1832
1800
|
// web, not host reach — granted by default today, but expressible as a grant.
|
|
1833
|
-
web:
|
|
1801
|
+
web: z4.boolean(),
|
|
1834
1802
|
// Browser automation (the Playwright MCP surface). Varies by host: a companion has
|
|
1835
1803
|
// it, the house executor does not (CT230).
|
|
1836
|
-
browser:
|
|
1804
|
+
browser: z4.boolean(),
|
|
1837
1805
|
// User-configured MCP servers permitted. False for the house executor
|
|
1838
1806
|
// (CT227: Cabane agents run no user MCP servers), true for a personal companion.
|
|
1839
|
-
userMcp:
|
|
1807
|
+
userMcp: z4.boolean(),
|
|
1840
1808
|
// Subagent / sub-dispatch (`Task` / `Agent`). A per-host GRANT (see the CT261
|
|
1841
1809
|
// amendment above): `false` on the locked assistant/house surface (banned via
|
|
1842
1810
|
// `DISALLOWED_TOOLS`), `true` under coding / custom where the fan-out is left to
|
|
1843
1811
|
// the coding allow-hook (which reaches sub-agent tool calls, CT680) or the user
|
|
1844
1812
|
// allowlist. The subagent completes within the turn, so
|
|
1845
1813
|
// it's not the turn-model invariant `scheduling` is.
|
|
1846
|
-
subagents:
|
|
1814
|
+
subagents: z4.boolean(),
|
|
1847
1815
|
// ── Hard platform invariants — always denied, never granted ────────────────
|
|
1848
1816
|
// Deferred re-invocation / scheduling (`ScheduleWakeup`, the `Cron*` / `Task*`
|
|
1849
1817
|
// families, `Monitor`, `Workflow`, …). A turn is one query that resolves when
|
|
1850
1818
|
// `result` fires; a scheduled callback fires after the reply window has closed
|
|
1851
1819
|
// and strands the agent (the CT155/CT156 rule).
|
|
1852
|
-
scheduling:
|
|
1820
|
+
scheduling: z4.literal("never"),
|
|
1853
1821
|
// Human-facing UI prompts (`AskUserQuestion`). A Cabane conversation has no
|
|
1854
1822
|
// handler to answer a structured prompt, so the call hangs the turn
|
|
1855
1823
|
// (`UNSUPPORTED_TOOLS`). The agent asks in its reply instead.
|
|
1856
|
-
uiPrompts:
|
|
1824
|
+
uiPrompts: z4.literal("never")
|
|
1857
1825
|
});
|
|
1858
1826
|
|
|
1859
1827
|
// packages/agent-runtime/src/turn-event.ts
|
|
1860
|
-
import { z as
|
|
1861
|
-
var turnEventSchema =
|
|
1828
|
+
import { z as z5 } from "zod";
|
|
1829
|
+
var turnEventSchema = z5.discriminatedUnion("type", [
|
|
1862
1830
|
// The runtime's opaque session state, emitted when the adapter learns it (e.g.
|
|
1863
1831
|
// the SDK `system/init` frame). The platform stores `state` verbatim per
|
|
1864
1832
|
// (conversation, agent) and hands it back on the next turn; only the adapter
|
|
@@ -1878,19 +1846,19 @@ var turnEventSchema = z6.discriminatedUnion("type", [
|
|
|
1878
1846
|
// on the companion, after the server committed the manifest). Runtime-neutral: a
|
|
1879
1847
|
// plain boolean, not a runtime-specific reason string (that stays in the
|
|
1880
1848
|
// adapter's `onWarn` log). Additive + optional — an old receiver ignores it.
|
|
1881
|
-
|
|
1882
|
-
type:
|
|
1883
|
-
state:
|
|
1884
|
-
degraded:
|
|
1849
|
+
z5.object({
|
|
1850
|
+
type: z5.literal("session"),
|
|
1851
|
+
state: z5.string(),
|
|
1852
|
+
degraded: z5.boolean().optional()
|
|
1885
1853
|
}),
|
|
1886
1854
|
// One readable thinking summary. Maps `onThinking({ text })`. Transient —
|
|
1887
1855
|
// surfaced live, never persisted as durable content.
|
|
1888
|
-
|
|
1856
|
+
z5.object({ type: z5.literal("thinking"), text: z5.string() }),
|
|
1889
1857
|
// Assistant text. Maps `onAssistantText({ text, final })` — `text`→`body`,
|
|
1890
1858
|
// `final`→`terminal`. `terminal: false` is interim narration (commits as a
|
|
1891
1859
|
// `progress` row); `terminal: true` is the turn's closing reply (commits as
|
|
1892
1860
|
// the `final` row).
|
|
1893
|
-
|
|
1861
|
+
z5.object({ type: z5.literal("text"), body: z5.string(), terminal: z5.boolean() }),
|
|
1894
1862
|
// A tool-activity transition. Maps `onToolActivity` — `toolUseId`→`id`,
|
|
1895
1863
|
// `toolName`→`name` (already prefix-stripped: `cabane_read`, not
|
|
1896
1864
|
// `mcp__cabane__cabane_read`), `summary` is the short card label. `phase`
|
|
@@ -1905,15 +1873,15 @@ var turnEventSchema = z6.discriminatedUnion("type", [
|
|
|
1905
1873
|
// dropped the prefix; null for a host / built-in tool. The client tags Cabane
|
|
1906
1874
|
// MCP calls (`=== 'cabane'`) with a subtle glyph. Nullable + optional so a
|
|
1907
1875
|
// pre-CT496 producer that never sets it is unaffected (treated as null).
|
|
1908
|
-
|
|
1909
|
-
type:
|
|
1910
|
-
id:
|
|
1911
|
-
name:
|
|
1912
|
-
phase:
|
|
1913
|
-
summary:
|
|
1914
|
-
input:
|
|
1915
|
-
result:
|
|
1916
|
-
mcpServer:
|
|
1876
|
+
z5.object({
|
|
1877
|
+
type: z5.literal("tool"),
|
|
1878
|
+
id: z5.string(),
|
|
1879
|
+
name: z5.string(),
|
|
1880
|
+
phase: z5.enum(["start", "done", "error"]),
|
|
1881
|
+
summary: z5.string(),
|
|
1882
|
+
input: z5.unknown().optional(),
|
|
1883
|
+
result: z5.unknown().optional(),
|
|
1884
|
+
mcpServer: z5.string().nullable().optional()
|
|
1917
1885
|
}),
|
|
1918
1886
|
// The turn's terminal outcome. Maps the `{ ok, reason }` both loops return
|
|
1919
1887
|
// inline. `ok:false` carries a machine reason (`no_session`, an error code);
|
|
@@ -1957,34 +1925,34 @@ var turnEventSchema = z6.discriminatedUnion("type", [
|
|
|
1957
1925
|
// `message_turn_metadata.resolved_model` / `resolved_config`. Optional ⇒
|
|
1958
1926
|
// backward-compatible: an old adapter/companion omits them, a cancel has no result
|
|
1959
1927
|
// event at all, and the columns stay null → the UI shows `—`.
|
|
1960
|
-
|
|
1961
|
-
type:
|
|
1962
|
-
ok:
|
|
1963
|
-
reason:
|
|
1964
|
-
usage:
|
|
1965
|
-
inputTokens:
|
|
1966
|
-
outputTokens:
|
|
1967
|
-
cacheReadTokens:
|
|
1968
|
-
cacheCreationTokens:
|
|
1969
|
-
contextTokens:
|
|
1970
|
-
contextWindow:
|
|
1928
|
+
z5.object({
|
|
1929
|
+
type: z5.literal("result"),
|
|
1930
|
+
ok: z5.boolean(),
|
|
1931
|
+
reason: z5.string().optional(),
|
|
1932
|
+
usage: z5.object({
|
|
1933
|
+
inputTokens: z5.number(),
|
|
1934
|
+
outputTokens: z5.number(),
|
|
1935
|
+
cacheReadTokens: z5.number().optional(),
|
|
1936
|
+
cacheCreationTokens: z5.number().optional(),
|
|
1937
|
+
contextTokens: z5.number().optional(),
|
|
1938
|
+
contextWindow: z5.number().optional()
|
|
1971
1939
|
}).optional(),
|
|
1972
|
-
resolvedModel:
|
|
1973
|
-
resolvedConfig:
|
|
1974
|
-
effort:
|
|
1975
|
-
thinking:
|
|
1976
|
-
reasoningEffort:
|
|
1940
|
+
resolvedModel: z5.string().optional(),
|
|
1941
|
+
resolvedConfig: z5.object({
|
|
1942
|
+
effort: z5.string().optional(),
|
|
1943
|
+
thinking: z5.string().optional(),
|
|
1944
|
+
reasoningEffort: z5.string().optional()
|
|
1977
1945
|
}).optional()
|
|
1978
1946
|
})
|
|
1979
1947
|
]);
|
|
1980
1948
|
|
|
1981
1949
|
// packages/agent-runtime/src/failure.ts
|
|
1982
|
-
import { z as
|
|
1983
|
-
var turnFailureSchema =
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1950
|
+
import { z as z6 } from "zod";
|
|
1951
|
+
var turnFailureSchema = z6.discriminatedUnion("kind", [
|
|
1952
|
+
z6.object({ kind: z6.literal("usage_capped"), resetsAt: z6.string().optional() }),
|
|
1953
|
+
z6.object({ kind: z6.literal("rate_limited") }),
|
|
1954
|
+
z6.object({ kind: z6.literal("server_error") }),
|
|
1955
|
+
z6.object({ kind: z6.literal("auth_expired") })
|
|
1988
1956
|
]);
|
|
1989
1957
|
var USAGE_CAPPED = "usage_capped";
|
|
1990
1958
|
var RATE_LIMITED = "rate_limited";
|
|
@@ -2090,63 +2058,63 @@ var RATE_PATTERNS = [/rate[_ ]?limit/, /\b429\b/, /too many requests/];
|
|
|
2090
2058
|
var BARE_LIMIT = /\blimit (reached|exceeded)\b/;
|
|
2091
2059
|
|
|
2092
2060
|
// packages/agent-runtime/src/turn-request.ts
|
|
2093
|
-
import { z as
|
|
2094
|
-
var contentBlockSchema =
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
type:
|
|
2098
|
-
source:
|
|
2061
|
+
import { z as z7 } from "zod";
|
|
2062
|
+
var contentBlockSchema = z7.discriminatedUnion("type", [
|
|
2063
|
+
z7.object({ type: z7.literal("text"), text: z7.string() }),
|
|
2064
|
+
z7.object({
|
|
2065
|
+
type: z7.literal("image"),
|
|
2066
|
+
source: z7.object({ type: z7.literal("url"), url: z7.string() })
|
|
2099
2067
|
}),
|
|
2100
|
-
|
|
2101
|
-
type:
|
|
2102
|
-
source:
|
|
2068
|
+
z7.object({
|
|
2069
|
+
type: z7.literal("document"),
|
|
2070
|
+
source: z7.object({ type: z7.literal("url"), url: z7.string() })
|
|
2103
2071
|
})
|
|
2104
2072
|
]);
|
|
2105
|
-
var effortLevelSchema =
|
|
2106
|
-
var resolvedRunConfigSchema =
|
|
2107
|
-
model:
|
|
2073
|
+
var effortLevelSchema = z7.enum(["low", "medium", "high", "xhigh", "max"]);
|
|
2074
|
+
var resolvedRunConfigSchema = z7.object({
|
|
2075
|
+
model: z7.string().nullable(),
|
|
2108
2076
|
effort: effortLevelSchema.optional(),
|
|
2109
|
-
runtimeOptions:
|
|
2077
|
+
runtimeOptions: z7.record(z7.string(), z7.unknown()).optional()
|
|
2110
2078
|
});
|
|
2111
|
-
var resolvedMcpServerSchema =
|
|
2112
|
-
|
|
2113
|
-
type:
|
|
2114
|
-
command:
|
|
2115
|
-
args:
|
|
2116
|
-
env:
|
|
2079
|
+
var resolvedMcpServerSchema = z7.union([
|
|
2080
|
+
z7.object({
|
|
2081
|
+
type: z7.literal("stdio").optional(),
|
|
2082
|
+
command: z7.string(),
|
|
2083
|
+
args: z7.array(z7.string()).optional(),
|
|
2084
|
+
env: z7.record(z7.string(), z7.string()).optional()
|
|
2117
2085
|
}),
|
|
2118
|
-
|
|
2119
|
-
type:
|
|
2120
|
-
url:
|
|
2121
|
-
headers:
|
|
2086
|
+
z7.object({
|
|
2087
|
+
type: z7.literal("http"),
|
|
2088
|
+
url: z7.string(),
|
|
2089
|
+
headers: z7.record(z7.string(), z7.string()).optional()
|
|
2122
2090
|
}),
|
|
2123
|
-
|
|
2124
|
-
type:
|
|
2125
|
-
url:
|
|
2126
|
-
headers:
|
|
2091
|
+
z7.object({
|
|
2092
|
+
type: z7.literal("sse"),
|
|
2093
|
+
url: z7.string(),
|
|
2094
|
+
headers: z7.record(z7.string(), z7.string()).optional()
|
|
2127
2095
|
})
|
|
2128
2096
|
]);
|
|
2129
|
-
var resolvedMcpServersSchema =
|
|
2130
|
-
var hostInjectedServersSchema =
|
|
2131
|
-
var turnRequestSchema =
|
|
2097
|
+
var resolvedMcpServersSchema = z7.record(z7.string(), resolvedMcpServerSchema);
|
|
2098
|
+
var hostInjectedServersSchema = z7.record(z7.string(), z7.unknown());
|
|
2099
|
+
var turnRequestSchema = z7.object({
|
|
2132
2100
|
// Server-composed system prompt (core + capability prose + adapter addendum +
|
|
2133
2101
|
// charter). One string to the adapter.
|
|
2134
|
-
systemPrompt:
|
|
2102
|
+
systemPrompt: z7.string(),
|
|
2135
2103
|
// Server-composed per-turn user text (anchor reminder + the triggering message).
|
|
2136
|
-
prompt:
|
|
2104
|
+
prompt: z7.string(),
|
|
2137
2105
|
// The multi-block user-message body (text + vision).
|
|
2138
|
-
content:
|
|
2106
|
+
content: z7.array(contentBlockSchema),
|
|
2139
2107
|
// Portable-or-dialect run-config (above).
|
|
2140
2108
|
config: resolvedRunConfigSchema,
|
|
2141
2109
|
// Abstract capability grants; the adapter maps them to tool names.
|
|
2142
2110
|
policy: hostPolicySchema,
|
|
2143
2111
|
// Prior opaque session state, or null for a fresh session.
|
|
2144
|
-
session:
|
|
2112
|
+
session: z7.string().nullable(),
|
|
2145
2113
|
// The cabane control-plane coordinates for this turn's MCP + post-back.
|
|
2146
|
-
cabane:
|
|
2147
|
-
mcpUrl:
|
|
2148
|
-
bearer:
|
|
2149
|
-
activeConversationId:
|
|
2114
|
+
cabane: z7.object({
|
|
2115
|
+
mcpUrl: z7.string(),
|
|
2116
|
+
bearer: z7.string(),
|
|
2117
|
+
activeConversationId: z7.string(),
|
|
2150
2118
|
// CT714: the scoped TURN-CONTROL MCP endpoint (`/api/turn-control`). The
|
|
2151
2119
|
// EXTERNAL adapters (Codex / opencode) mount it by URL under the key
|
|
2152
2120
|
// `cabane_companion` — using the same `bearer` (the turn token) and the same
|
|
@@ -2156,7 +2124,7 @@ var turnRequestSchema = z8.object({
|
|
|
2156
2124
|
// claude-code ignores it (it mounts the in-process instance instead), and
|
|
2157
2125
|
// every existing `cabane`-block fixture keeps parsing unchanged; the
|
|
2158
2126
|
// companion always populates it (`build-options.ts`).
|
|
2159
|
-
turnControlUrl:
|
|
2127
|
+
turnControlUrl: z7.string().optional(),
|
|
2160
2128
|
// CT598: the workspace this turn runs in. The claude-code/opencode/codex
|
|
2161
2129
|
// adapters never need it (they reach Cabane through the `cabane` MCP server,
|
|
2162
2130
|
// which takes `workspaceId` as a per-tool arg the model supplies); the
|
|
@@ -2166,53 +2134,39 @@ var turnRequestSchema = z8.object({
|
|
|
2166
2134
|
// adapters' conformance fixtures, tests) keeps parsing unchanged — the companion
|
|
2167
2135
|
// always populates it (`build-options.ts`), and the native adapter fails the
|
|
2168
2136
|
// turn loudly when it is somehow absent rather than guessing.
|
|
2169
|
-
workspaceId:
|
|
2137
|
+
workspaceId: z7.string().optional(),
|
|
2170
2138
|
// CT752: the server-resolved workspace surface this credential exposes.
|
|
2171
2139
|
// Readiness uses this explicit fact to require `sdk` for code mode and the
|
|
2172
2140
|
// granular floor for classic mode; inventory contents alone cannot infer it
|
|
2173
2141
|
// because `sdk` is intentionally also available on the classic surface.
|
|
2174
|
-
workspaceToolSurface:
|
|
2142
|
+
workspaceToolSurface: z7.enum(["code", "classic"]).optional()
|
|
2175
2143
|
}),
|
|
2176
2144
|
// Machine-local resolution (host-filled): the checkout cwd, extra env from a
|
|
2177
2145
|
// prepare hook, and the resolved user MCP servers.
|
|
2178
|
-
local:
|
|
2179
|
-
cwd:
|
|
2180
|
-
env:
|
|
2181
|
-
nativeWorkAssignment: z8.object({
|
|
2182
|
-
itemId: z8.string(),
|
|
2183
|
-
executionId: z8.string(),
|
|
2184
|
-
activationEpoch: z8.number().int().nonnegative()
|
|
2185
|
-
}).strict().optional(),
|
|
2146
|
+
local: z7.object({
|
|
2147
|
+
cwd: z7.string().optional(),
|
|
2148
|
+
env: z7.record(z7.string(), z7.string()).optional(),
|
|
2186
2149
|
mcpServers: resolvedMcpServersSchema.optional(),
|
|
2187
2150
|
// CT289: machine-local claude-code adapter knobs the operator sets on a
|
|
2188
2151
|
// companion they run themselves — the auto-memory escape hatch. `autoMemory:
|
|
2189
2152
|
// true` opts back into Claude Code's auto-memory (governed by the operator's
|
|
2190
2153
|
// own `.claude/settings.json`); absent/false leaves the adapter's force-off
|
|
2191
|
-
// default in place (see `buildClaudeCodeOptions`).
|
|
2192
|
-
|
|
2193
|
-
claudeCode: z8.object({ autoMemory: z8.boolean().optional() }).optional()
|
|
2154
|
+
// default in place (see `buildClaudeCodeOptions`).
|
|
2155
|
+
claudeCode: z7.object({ autoMemory: z7.boolean().optional() }).optional()
|
|
2194
2156
|
}),
|
|
2195
2157
|
// Host-owned injected servers (host-filled) — e.g. the summon server.
|
|
2196
|
-
extra:
|
|
2197
|
-
mcpServers: hostInjectedServersSchema
|
|
2198
|
-
// CT666: the per-turn turn-control HANDLER for the in-process cabane-native
|
|
2199
|
-
// runtime — the seam that gives it ask/wake_me/summon_agent/sub_agent/skip_turn
|
|
2200
|
-
// without a subprocess `cabane_companion` MCP server. Typed `unknown` for the same
|
|
2201
|
-
// reason as `mcpServers`: it's a live host object (a `NativeTurnControl` whose
|
|
2202
|
-
// methods close over the dispatcher's per-turn state), passed to the native
|
|
2203
|
-
// adapter WITHOUT the contract package inspecting it. The subprocess adapters
|
|
2204
|
-
// ignore it (they get the same verbs from the injected SDK server instead).
|
|
2205
|
-
turnControl: z8.unknown().optional()
|
|
2158
|
+
extra: z7.object({
|
|
2159
|
+
mcpServers: hostInjectedServersSchema
|
|
2206
2160
|
})
|
|
2207
2161
|
});
|
|
2208
2162
|
|
|
2209
2163
|
// packages/agent-runtime/src/conformance.ts
|
|
2210
|
-
import { z as
|
|
2211
|
-
var conformanceFixtureSchema =
|
|
2212
|
-
name:
|
|
2164
|
+
import { z as z8 } from "zod";
|
|
2165
|
+
var conformanceFixtureSchema = z8.object({
|
|
2166
|
+
name: z8.string(),
|
|
2213
2167
|
request: turnRequestSchema,
|
|
2214
|
-
nativeStream:
|
|
2215
|
-
expected:
|
|
2168
|
+
nativeStream: z8.array(z8.unknown()),
|
|
2169
|
+
expected: z8.array(turnEventSchema)
|
|
2216
2170
|
});
|
|
2217
2171
|
|
|
2218
2172
|
// packages/agent-runtime/src/transcript.ts
|
|
@@ -2222,8 +2176,8 @@ function createTerminalTextBuffer() {
|
|
|
2222
2176
|
async function safeEmit(emit, event, onError) {
|
|
2223
2177
|
try {
|
|
2224
2178
|
await emit(event);
|
|
2225
|
-
} catch (
|
|
2226
|
-
onError?.(
|
|
2179
|
+
} catch (err) {
|
|
2180
|
+
onError?.(err, event.type);
|
|
2227
2181
|
}
|
|
2228
2182
|
}
|
|
2229
2183
|
async function processAssistantMessage(msg, emit, pending, buffer, onError) {
|
|
@@ -2484,16 +2438,16 @@ var TurnPump = class {
|
|
|
2484
2438
|
// minimal note. Skipped when cancelled or already final. The held-text flush
|
|
2485
2439
|
// that precedes it is a classification concern, driven by the caller before
|
|
2486
2440
|
// this runs.
|
|
2487
|
-
async finalize(
|
|
2488
|
-
if (!
|
|
2441
|
+
async finalize(ok) {
|
|
2442
|
+
if (!ok || this.opts.signal.aborted || this.emittedFinal) return;
|
|
2489
2443
|
const body = this.lastProgressBody ?? this.opts.emptyFinalBody;
|
|
2490
2444
|
const seq = this.opts.nextSeq();
|
|
2491
2445
|
try {
|
|
2492
2446
|
await this.opts.commit.commitMessage({ body, kind: "final", seq });
|
|
2493
2447
|
this.emittedFinal = true;
|
|
2494
2448
|
this.finalReplyBody = body;
|
|
2495
|
-
} catch (
|
|
2496
|
-
this.opts.onError?.(
|
|
2449
|
+
} catch (err) {
|
|
2450
|
+
this.opts.onError?.(err, "empty-final");
|
|
2497
2451
|
}
|
|
2498
2452
|
}
|
|
2499
2453
|
// Whether the turn has committed its `final` row — read by the host to decide
|
|
@@ -2517,7 +2471,7 @@ import {
|
|
|
2517
2471
|
var CLAUDE_CODE_ADDENDUM = "";
|
|
2518
2472
|
|
|
2519
2473
|
// packages/agent-runtime/src/claude-code/policy.ts
|
|
2520
|
-
import { z as
|
|
2474
|
+
import { z as z9 } from "zod";
|
|
2521
2475
|
var HOST_FS_TOOLS = [
|
|
2522
2476
|
// shell + local filesystem
|
|
2523
2477
|
"Bash",
|
|
@@ -2567,30 +2521,20 @@ function withThinkingSummaries(thinking) {
|
|
|
2567
2521
|
if (thinking.type === "disabled") return thinking;
|
|
2568
2522
|
return { display: "summarized", ...thinking };
|
|
2569
2523
|
}
|
|
2570
|
-
var claudeCodeDialectSchema =
|
|
2571
|
-
thinking:
|
|
2572
|
-
|
|
2573
|
-
type:
|
|
2574
|
-
display:
|
|
2524
|
+
var claudeCodeDialectSchema = z9.object({
|
|
2525
|
+
thinking: z9.discriminatedUnion("type", [
|
|
2526
|
+
z9.object({
|
|
2527
|
+
type: z9.literal("adaptive"),
|
|
2528
|
+
display: z9.enum(["summarized", "omitted"]).optional()
|
|
2575
2529
|
}),
|
|
2576
|
-
|
|
2577
|
-
type:
|
|
2578
|
-
budgetTokens:
|
|
2579
|
-
display:
|
|
2530
|
+
z9.object({
|
|
2531
|
+
type: z9.literal("enabled"),
|
|
2532
|
+
budgetTokens: z9.number().int().positive().optional(),
|
|
2533
|
+
display: z9.enum(["summarized", "omitted"]).optional()
|
|
2580
2534
|
}),
|
|
2581
|
-
|
|
2535
|
+
z9.object({ type: z9.literal("disabled") })
|
|
2582
2536
|
]).optional(),
|
|
2583
|
-
|
|
2584
|
-
disallowedTools: z10.array(z10.string()).optional(),
|
|
2585
|
-
// Which claude-code harness shape to run. `coding` switches to the
|
|
2586
|
-
// `claude_code` preset + project settings + always-allow `PreToolUse` hook;
|
|
2587
|
-
// `custom`/`assistant` (or absent) use a plain-string prompt + bypass. This
|
|
2588
|
-
// is the claude-code-specific PRESET selector — kept distinct from
|
|
2589
|
-
// `policy.hostFs` (the host-fs BLOCK), because companion `custom` mode wants host
|
|
2590
|
-
// fs available (via its own allowlist) WITHOUT the coding harness, and in-app
|
|
2591
|
-
// `custom` wants host fs blocked — neither of which a single `hostFs` boolean
|
|
2592
|
-
// can express alongside the preset choice.
|
|
2593
|
-
mode: z10.enum(["assistant", "coding", "custom"]).optional()
|
|
2537
|
+
hostAccess: z9.boolean().optional()
|
|
2594
2538
|
}).loose();
|
|
2595
2539
|
function readThinking(runtimeOptions) {
|
|
2596
2540
|
const dialect = runtimeOptions?.["claude-code"];
|
|
@@ -2669,18 +2613,15 @@ function buildClaudeCodeOptions(req, augment) {
|
|
|
2669
2613
|
};
|
|
2670
2614
|
}
|
|
2671
2615
|
const dialect = claudeCodeDialectSchema.safeParse(config.runtimeOptions?.["claude-code"] ?? {});
|
|
2672
|
-
const
|
|
2673
|
-
const customDisallowed = dialect.success ? dialect.data.disallowedTools ?? [] : [];
|
|
2674
|
-
const useCodingPreset = (dialect.success ? dialect.data.mode : void 0) === "coding";
|
|
2616
|
+
const useCodingPreset = dialect.success ? dialect.data.hostAccess ?? false : false;
|
|
2675
2617
|
const cabaneGlob = `mcp__${CABANE_MCP_SERVER}__*`;
|
|
2676
2618
|
const extraServerGlobs = Object.keys(req.extra.mcpServers).map((name) => `mcp__${name}__*`);
|
|
2677
2619
|
const allowedTools = dedupe([
|
|
2678
2620
|
cabaneGlob,
|
|
2679
2621
|
...extraServerGlobs,
|
|
2680
|
-
...policy.web ? DEFAULT_WEB_TOOLS : []
|
|
2681
|
-
...customAllowed
|
|
2622
|
+
...policy.web ? DEFAULT_WEB_TOOLS : []
|
|
2682
2623
|
]);
|
|
2683
|
-
const disallowedTools = dedupe([...disallowedToolsFor(policy)
|
|
2624
|
+
const disallowedTools = dedupe([...disallowedToolsFor(policy)]);
|
|
2684
2625
|
const resumeDecision = decideResume(req.session, cwd);
|
|
2685
2626
|
const resume = "resume" in resumeDecision ? resumeDecision.resume : null;
|
|
2686
2627
|
const freshReason = "fresh" in resumeDecision ? resumeDecision.reason : void 0;
|
|
@@ -2746,7 +2687,7 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
2746
2687
|
out.push(event);
|
|
2747
2688
|
};
|
|
2748
2689
|
let sessionEmitted = false;
|
|
2749
|
-
let
|
|
2690
|
+
let ok = false;
|
|
2750
2691
|
let resultReason;
|
|
2751
2692
|
let sawResult = false;
|
|
2752
2693
|
let usage;
|
|
@@ -2792,8 +2733,8 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
2792
2733
|
if (typeof info.rateLimitType === "string") rateLimitType = info.rateLimitType;
|
|
2793
2734
|
}
|
|
2794
2735
|
} else if (msg.type === "auth_status") {
|
|
2795
|
-
const
|
|
2796
|
-
if (typeof
|
|
2736
|
+
const err = msg.error;
|
|
2737
|
+
if (typeof err === "string" && err.length > 0) authError = err;
|
|
2797
2738
|
} else if (msg.type === "result") {
|
|
2798
2739
|
sawResult = true;
|
|
2799
2740
|
usage = readSdkUsage(msg);
|
|
@@ -2805,7 +2746,7 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
2805
2746
|
}
|
|
2806
2747
|
const isError = msg.is_error === true;
|
|
2807
2748
|
if (msg.subtype === "success" && !isError) {
|
|
2808
|
-
|
|
2749
|
+
ok = true;
|
|
2809
2750
|
} else {
|
|
2810
2751
|
const resultText = msg.result ?? "";
|
|
2811
2752
|
const terminalReason = msg.terminal_reason;
|
|
@@ -2819,26 +2760,26 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
2819
2760
|
...rateLimitResetIso ? { resetsAt: rateLimitResetIso } : {}
|
|
2820
2761
|
} : classifyAssistantError(lastAssistantError) ?? classifyErrorText([authError, errorText].filter(Boolean).join(" "));
|
|
2821
2762
|
resultReason = failure ? encodeFailureReason(failure) : isError ? `error:${resultText.slice(0, 200) || "unknown"}` : `result_error:${msg.subtype}`;
|
|
2822
|
-
|
|
2763
|
+
ok = false;
|
|
2823
2764
|
}
|
|
2824
2765
|
break;
|
|
2825
2766
|
}
|
|
2826
2767
|
}
|
|
2827
|
-
} catch (
|
|
2828
|
-
if (ctx.signal.aborted) throw
|
|
2829
|
-
const failure = classifyErrorText(
|
|
2830
|
-
if (!failure) throw
|
|
2831
|
-
|
|
2768
|
+
} catch (err) {
|
|
2769
|
+
if (ctx.signal.aborted) throw err;
|
|
2770
|
+
const failure = classifyErrorText(err instanceof Error ? err.message : String(err));
|
|
2771
|
+
if (!failure) throw err;
|
|
2772
|
+
ok = false;
|
|
2832
2773
|
resultReason = encodeFailureReason(failure);
|
|
2833
2774
|
sawResult = true;
|
|
2834
2775
|
}
|
|
2835
2776
|
if (ctx.signal.aborted) return;
|
|
2836
|
-
await flushHeldText(buffer, emit,
|
|
2777
|
+
await flushHeldText(buffer, emit, ok);
|
|
2837
2778
|
yield* drain(out);
|
|
2838
|
-
if (!
|
|
2779
|
+
if (!ok && !resultReason && !sawResult) resultReason = "no_result";
|
|
2839
2780
|
yield {
|
|
2840
2781
|
type: "result",
|
|
2841
|
-
ok
|
|
2782
|
+
ok,
|
|
2842
2783
|
...resultReason ? { reason: resultReason } : {},
|
|
2843
2784
|
...usage ? { usage } : {},
|
|
2844
2785
|
...resolvedModel ? { resolvedModel } : {}
|
|
@@ -3401,9 +3342,9 @@ function readSessionId(properties) {
|
|
|
3401
3342
|
}
|
|
3402
3343
|
function readSessionError(properties) {
|
|
3403
3344
|
const props = asRecord(properties);
|
|
3404
|
-
const
|
|
3405
|
-
if (typeof
|
|
3406
|
-
const rec = asRecord(
|
|
3345
|
+
const err = props?.error;
|
|
3346
|
+
if (typeof err === "string") return err;
|
|
3347
|
+
const rec = asRecord(err);
|
|
3407
3348
|
if (!rec) return "unknown";
|
|
3408
3349
|
const { name, message } = deepestError(rec);
|
|
3409
3350
|
if (message && name && !isGenericErrorName(name)) return `${name}: ${message}`;
|
|
@@ -3439,7 +3380,7 @@ async function* decodeOpencodeStream(events, ctx) {
|
|
|
3439
3380
|
const pending = /* @__PURE__ */ new Map();
|
|
3440
3381
|
const startedTools = /* @__PURE__ */ new Set();
|
|
3441
3382
|
const finishedTools = /* @__PURE__ */ new Set();
|
|
3442
|
-
let
|
|
3383
|
+
let ok = false;
|
|
3443
3384
|
let reason;
|
|
3444
3385
|
let settled = false;
|
|
3445
3386
|
const userMessageIds = /* @__PURE__ */ new Set();
|
|
@@ -3502,7 +3443,7 @@ async function* decodeOpencodeStream(events, ctx) {
|
|
|
3502
3443
|
const sealed = sealHeld(held, true);
|
|
3503
3444
|
held = null;
|
|
3504
3445
|
if (sealed) yield sealed;
|
|
3505
|
-
|
|
3446
|
+
ok = true;
|
|
3506
3447
|
settled = true;
|
|
3507
3448
|
break;
|
|
3508
3449
|
} else if (ev.type === "session.error") {
|
|
@@ -3511,7 +3452,7 @@ async function* decodeOpencodeStream(events, ctx) {
|
|
|
3511
3452
|
const sealed = sealHeld(held, false);
|
|
3512
3453
|
held = null;
|
|
3513
3454
|
if (sealed) yield sealed;
|
|
3514
|
-
|
|
3455
|
+
ok = false;
|
|
3515
3456
|
const errorText = readSessionError(ev.properties);
|
|
3516
3457
|
const failure = classifyErrorText(errorText);
|
|
3517
3458
|
reason = failure ? encodeFailureReason(failure) : `error:${errorText.slice(0, 200)}`;
|
|
@@ -3526,7 +3467,7 @@ async function* decodeOpencodeStream(events, ctx) {
|
|
|
3526
3467
|
if (sealed) yield sealed;
|
|
3527
3468
|
reason = "no_terminal";
|
|
3528
3469
|
}
|
|
3529
|
-
yield { type: "result", ok
|
|
3470
|
+
yield { type: "result", ok, ...reason ? { reason } : {} };
|
|
3530
3471
|
}
|
|
3531
3472
|
function hasToolInput(input) {
|
|
3532
3473
|
return !!input && typeof input === "object" && Object.keys(input).length > 0;
|
|
@@ -3540,7 +3481,7 @@ function sealHeld(held, terminal) {
|
|
|
3540
3481
|
}
|
|
3541
3482
|
|
|
3542
3483
|
// packages/agent-runtime/src/opencode/policy.ts
|
|
3543
|
-
import { z as
|
|
3484
|
+
import { z as z10 } from "zod";
|
|
3544
3485
|
var OPENCODE_HOST_TOOLS = [
|
|
3545
3486
|
"bash",
|
|
3546
3487
|
"edit",
|
|
@@ -3567,8 +3508,8 @@ function opencodeToolPolicy(policy) {
|
|
|
3567
3508
|
deny(OPENCODE_UI_PROMPT_TOOLS);
|
|
3568
3509
|
return { tools, allowAllHostTools: policy.hostFs };
|
|
3569
3510
|
}
|
|
3570
|
-
var opencodeDialectSchema =
|
|
3571
|
-
agent:
|
|
3511
|
+
var opencodeDialectSchema = z10.object({
|
|
3512
|
+
agent: z10.string().min(1).optional()
|
|
3572
3513
|
}).loose();
|
|
3573
3514
|
function readOpencodeDialect(runtimeOptions) {
|
|
3574
3515
|
const parsed = opencodeDialectSchema.safeParse(runtimeOptions?.["opencode"] ?? {});
|
|
@@ -3789,9 +3730,9 @@ function createHttpOpencodeTransport(opts) {
|
|
|
3789
3730
|
// The lock is released when this stream finishes draining.
|
|
3790
3731
|
events: releaseAfter(parseSseStream(eventRes.body, sessionId, signal), release)
|
|
3791
3732
|
};
|
|
3792
|
-
} catch (
|
|
3733
|
+
} catch (err) {
|
|
3793
3734
|
release();
|
|
3794
|
-
throw
|
|
3735
|
+
throw err;
|
|
3795
3736
|
}
|
|
3796
3737
|
}
|
|
3797
3738
|
};
|
|
@@ -4384,9 +4325,9 @@ function readItemType(item) {
|
|
|
4384
4325
|
function readErrorMessage(ev) {
|
|
4385
4326
|
const direct = str(ev.message);
|
|
4386
4327
|
if (direct) return direct;
|
|
4387
|
-
const
|
|
4388
|
-
if (
|
|
4389
|
-
const m = str(
|
|
4328
|
+
const err = asRecord2(ev.error);
|
|
4329
|
+
if (err) {
|
|
4330
|
+
const m = str(err.message);
|
|
4390
4331
|
if (m) return m;
|
|
4391
4332
|
}
|
|
4392
4333
|
return "unknown";
|
|
@@ -4442,7 +4383,7 @@ async function* decodeCodexStream(events, ctx) {
|
|
|
4442
4383
|
const startedTools = /* @__PURE__ */ new Set();
|
|
4443
4384
|
const finishedTools = /* @__PURE__ */ new Set();
|
|
4444
4385
|
let sessionEmitted = false;
|
|
4445
|
-
let
|
|
4386
|
+
let ok = false;
|
|
4446
4387
|
let reason;
|
|
4447
4388
|
let usage;
|
|
4448
4389
|
let settled = false;
|
|
@@ -4473,7 +4414,7 @@ async function* decodeCodexStream(events, ctx) {
|
|
|
4473
4414
|
const message = readItemMessage(item);
|
|
4474
4415
|
if (isModelMetadataError(message)) {
|
|
4475
4416
|
yield* flushInterim();
|
|
4476
|
-
|
|
4417
|
+
ok = false;
|
|
4477
4418
|
reason = `model_unavailable:${message.slice(0, 200)}`;
|
|
4478
4419
|
settled = true;
|
|
4479
4420
|
break;
|
|
@@ -4528,13 +4469,13 @@ async function* decodeCodexStream(events, ctx) {
|
|
|
4528
4469
|
held = null;
|
|
4529
4470
|
if (sealed) yield sealed;
|
|
4530
4471
|
usage = readUsage(ev);
|
|
4531
|
-
|
|
4472
|
+
ok = true;
|
|
4532
4473
|
settled = true;
|
|
4533
4474
|
break;
|
|
4534
4475
|
}
|
|
4535
4476
|
if (ev.type === "turn.failed" || ev.type === "error") {
|
|
4536
4477
|
yield* flushInterim();
|
|
4537
|
-
|
|
4478
|
+
ok = false;
|
|
4538
4479
|
const text = readErrorMessage(ev);
|
|
4539
4480
|
const failure = classifyErrorText(text);
|
|
4540
4481
|
reason = failure ? encodeFailureReason(failure) : `error:${text.slice(0, 200)}`;
|
|
@@ -4552,7 +4493,7 @@ async function* decodeCodexStream(events, ctx) {
|
|
|
4552
4493
|
const resolvedConfig = ctx.resolvedReasoningEffort ? { reasoningEffort: ctx.resolvedReasoningEffort } : void 0;
|
|
4553
4494
|
yield {
|
|
4554
4495
|
type: "result",
|
|
4555
|
-
ok
|
|
4496
|
+
ok,
|
|
4556
4497
|
...reason ? { reason } : {},
|
|
4557
4498
|
...usage ? { usage } : {},
|
|
4558
4499
|
...ctx.resolvedModel ? { resolvedModel: ctx.resolvedModel } : {},
|
|
@@ -4570,7 +4511,7 @@ function sealHeld2(held, terminal) {
|
|
|
4570
4511
|
}
|
|
4571
4512
|
|
|
4572
4513
|
// packages/agent-runtime/src/codex/policy.ts
|
|
4573
|
-
import { z as
|
|
4514
|
+
import { z as z11 } from "zod";
|
|
4574
4515
|
function codexToolPolicy(policy) {
|
|
4575
4516
|
return policy.hostFs ? {
|
|
4576
4517
|
permissionProfile: "cabane-coding",
|
|
@@ -4585,8 +4526,8 @@ function codexToolPolicy(policy) {
|
|
|
4585
4526
|
};
|
|
4586
4527
|
}
|
|
4587
4528
|
var CODEX_REASONING_EFFORTS = ["minimal", "low", "medium", "high", "xhigh"];
|
|
4588
|
-
var codexDialectSchema =
|
|
4589
|
-
modelReasoningEffort:
|
|
4529
|
+
var codexDialectSchema = z11.object({
|
|
4530
|
+
modelReasoningEffort: z11.enum(CODEX_REASONING_EFFORTS).optional()
|
|
4590
4531
|
}).loose();
|
|
4591
4532
|
function readCodexDialect(runtimeOptions) {
|
|
4592
4533
|
const parsed = codexDialectSchema.safeParse(runtimeOptions?.["codex"] ?? {});
|
|
@@ -4594,7 +4535,7 @@ function readCodexDialect(runtimeOptions) {
|
|
|
4594
4535
|
}
|
|
4595
4536
|
|
|
4596
4537
|
// packages/agent-runtime/src/codex/model.ts
|
|
4597
|
-
var CODEX_DEFAULT_MODEL_BARES = /* @__PURE__ */ new Set(["default"
|
|
4538
|
+
var CODEX_DEFAULT_MODEL_BARES = /* @__PURE__ */ new Set(["default"]);
|
|
4598
4539
|
function parseCodexModel(model) {
|
|
4599
4540
|
const sep = model.indexOf("/");
|
|
4600
4541
|
const bare = sep === -1 || model.slice(0, sep) !== "openai" ? model : model.slice(sep + 1);
|
|
@@ -4681,9 +4622,11 @@ function buildConfig(req) {
|
|
|
4681
4622
|
};
|
|
4682
4623
|
}
|
|
4683
4624
|
const policy = codexToolPolicy(req.policy);
|
|
4625
|
+
const tmpDir = req.local.env?.TMPDIR;
|
|
4684
4626
|
return {
|
|
4685
4627
|
mcp_servers,
|
|
4686
4628
|
experimental_use_rmcp_client: true,
|
|
4629
|
+
...tmpDir ? { shell_environment_policy: { set: { TMPDIR: tmpDir } } } : {},
|
|
4687
4630
|
...policy.permissionProfile ? {
|
|
4688
4631
|
// CT733: named permission profiles are Codex's split-filesystem path.
|
|
4689
4632
|
// `:root = read` preserves coding-mode host reads; the one explicit
|
|
@@ -4961,7 +4904,7 @@ var CodexExec = class {
|
|
|
4961
4904
|
signal: args.signal
|
|
4962
4905
|
});
|
|
4963
4906
|
let spawnError = null;
|
|
4964
|
-
child.once("error", (
|
|
4907
|
+
child.once("error", (err) => spawnError = err);
|
|
4965
4908
|
if (!child.stdin) {
|
|
4966
4909
|
child.kill();
|
|
4967
4910
|
throw new Error("Child process has no stdin");
|
|
@@ -5865,1027 +5808,6 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
5865
5808
|
}
|
|
5866
5809
|
];
|
|
5867
5810
|
|
|
5868
|
-
// packages/agent-runtime/src/cabane-native/addendum.ts
|
|
5869
|
-
var CABANE_NATIVE_ADDENDUM = `## Your tools (native runtime)
|
|
5870
|
-
|
|
5871
|
-
You are running on Cabane's own agent runtime. You have a small, curated set of workspace tools, all prefixed \`cabane_\`:
|
|
5872
|
-
|
|
5873
|
-
- \`cabane_list\` \u2014 list a folder's files and subfolders.
|
|
5874
|
-
- \`cabane_read\` \u2014 read one file's contents by path.
|
|
5875
|
-
- \`cabane_search\` \u2014 substring search across file/folder names, file contents, and conversations.
|
|
5876
|
-
- \`cabane_write\` \u2014 create or overwrite a file (pass \`overwrite: true\` to replace).
|
|
5877
|
-
- \`cabane_edit\` \u2014 find/replace inside an existing file.
|
|
5878
|
-
- \`cabane_mkdir\` \u2014 create a folder (pass \`recursive: true\` to also make missing parents).
|
|
5879
|
-
- \`cabane_move\` \u2014 move or rename a file or folder.
|
|
5880
|
-
- \`cabane_delete\` \u2014 delete a file or folder (folders need \`recursive: true\`; soft-delete, recoverable).
|
|
5881
|
-
- \`cabane_context\` \u2014 gather an object plus its related context (files, conversations, people) in one call \u2014 your first move to understand what surrounds something.
|
|
5882
|
-
- \`cabane_messages\` \u2014 read a conversation's messages by id (from \`cabane_search\` / \`cabane_context\`).
|
|
5883
|
-
|
|
5884
|
-
Paths are workspace-relative with a leading slash (\`/notes/todo.md\`). This is a deliberately curated tool set \u2014 if a job needs an operation that isn't here, say so plainly in your reply rather than inventing a tool. Your reply text is streamed straight into the conversation; there is no separate send step.`;
|
|
5885
|
-
var CABANE_NATIVE_ADDENDUM_CODE_MODE = `## Your tools (native runtime \u2014 code mode)
|
|
5886
|
-
|
|
5887
|
-
You are running on Cabane's own agent runtime, in **code mode**. Your one workspace tool is \`sdk\`: you act on the workspace by writing a short TypeScript program against the ambient \`cabane\` object and submitting it as the \`code\` argument. Only what your program \`return\`s (plus \`console.log\`) comes back \u2014 so gather, filter, and summarize *inside* the program and return the small answer, not the raw material.
|
|
5888
|
-
|
|
5889
|
-
There is **no** \`cabane_read\` / \`cabane_write\` / \`cabane_search\` / \`cabane_edit\` tool \u2014 those are \`cabane\` SDK calls *inside* your program (\`cabane.files.read(path)\`, \`cabane.find.search(q)\`, \u2026), not tools you call directly. The full SDK surface \u2014 every method and its options \u2014 is documented below this note in your prompt.
|
|
5890
|
-
|
|
5891
|
-
Beside \`sdk\` you also have a few **plain-named** tools that act on the TURN itself, not the workspace (they can't be a program's \`return\` value):
|
|
5892
|
-
- \`ask\` \u2014 put a structured question to a human and END your turn (they may answer in days).
|
|
5893
|
-
- \`wake_me\` \u2014 end this turn now and be re-dispatched later to check a condition (a PR merging, a reply landing).
|
|
5894
|
-
- \`summon_agent\` \u2014 pull a peer agent into THIS conversation to reply on your turn.
|
|
5895
|
-
- \`sub_agent\` \u2014 spawn a private worker with a fresh context window; its result comes back here later (don't wait \u2014 end your turn).
|
|
5896
|
-
- \`skip_turn\` \u2014 end your turn with NO reply, when the message doesn't need one from you.
|
|
5897
|
-
- \`mint_render_token\` \u2014 mint a short-lived credential to load a workspace HTML page in a browser.
|
|
5898
|
-
|
|
5899
|
-
Your reply text is streamed straight into the conversation; there is no separate send step.`;
|
|
5900
|
-
|
|
5901
|
-
// packages/agent-runtime/src/cabane-native/context.ts
|
|
5902
|
-
var DEFAULT_HISTORY_LIMIT = 20;
|
|
5903
|
-
var DEFAULT_MAX_HISTORY_CHARS = 24e3;
|
|
5904
|
-
async function assembleMessages(systemPrompt, content, fallbackPrompt, opts) {
|
|
5905
|
-
const messages = [{ role: "system", content: systemPrompt }];
|
|
5906
|
-
const history = await fetchRecentHistory(opts);
|
|
5907
|
-
for (const m of history) messages.push(m);
|
|
5908
|
-
messages.push({ role: "user", content: currentUserText(content, fallbackPrompt) });
|
|
5909
|
-
return messages;
|
|
5910
|
-
}
|
|
5911
|
-
function currentUserText(content, fallbackPrompt) {
|
|
5912
|
-
const text = content.filter((b) => b.type === "text").map((b) => b.text).join("\n").trim();
|
|
5913
|
-
return text.length > 0 ? text : fallbackPrompt;
|
|
5914
|
-
}
|
|
5915
|
-
async function fetchRecentHistory(opts) {
|
|
5916
|
-
const doFetch = opts.fetchImpl ?? fetch;
|
|
5917
|
-
const limit = opts.historyLimit ?? DEFAULT_HISTORY_LIMIT;
|
|
5918
|
-
const url = new URL(
|
|
5919
|
-
`${opts.apiRoot}/workspaces/${opts.workspaceId}/conversations/${opts.conversationId}/messages`
|
|
5920
|
-
);
|
|
5921
|
-
url.searchParams.set("limit", String(limit));
|
|
5922
|
-
url.searchParams.set("order", "desc");
|
|
5923
|
-
let rows;
|
|
5924
|
-
try {
|
|
5925
|
-
const res = await doFetch(url.toString(), {
|
|
5926
|
-
headers: { Authorization: `Bearer ${opts.bearer}` }
|
|
5927
|
-
});
|
|
5928
|
-
if (!res.ok) return [];
|
|
5929
|
-
const body = await res.json();
|
|
5930
|
-
rows = body.messages ?? [];
|
|
5931
|
-
} catch {
|
|
5932
|
-
return [];
|
|
5933
|
-
}
|
|
5934
|
-
const chronological = [...rows].reverse();
|
|
5935
|
-
while (chronological.length > 0 && chronological[chronological.length - 1].role === "user") {
|
|
5936
|
-
chronological.pop();
|
|
5937
|
-
}
|
|
5938
|
-
const mapped = [];
|
|
5939
|
-
for (const r of chronological) {
|
|
5940
|
-
const role = r.role === "agent" ? "assistant" : r.role === "user" ? "user" : null;
|
|
5941
|
-
if (!role) continue;
|
|
5942
|
-
const body = (r.body ?? "").trim();
|
|
5943
|
-
if (body.length === 0) continue;
|
|
5944
|
-
mapped.push({ role, content: body });
|
|
5945
|
-
}
|
|
5946
|
-
return capHistory(mapped, opts.maxHistoryChars ?? DEFAULT_MAX_HISTORY_CHARS);
|
|
5947
|
-
}
|
|
5948
|
-
function capHistory(messages, maxChars) {
|
|
5949
|
-
let total = messages.reduce((n, m) => n + m.content.length, 0);
|
|
5950
|
-
let start = 0;
|
|
5951
|
-
while (total > maxChars && start < messages.length) {
|
|
5952
|
-
total -= messages[start].content.length;
|
|
5953
|
-
start += 1;
|
|
5954
|
-
}
|
|
5955
|
-
return messages.slice(start);
|
|
5956
|
-
}
|
|
5957
|
-
|
|
5958
|
-
// packages/agent-runtime/src/cabane-native/model.ts
|
|
5959
|
-
var CABANE_NATIVE_MODEL_PREFIX = "cabane-native/";
|
|
5960
|
-
function parseCabaneNativeModel(model) {
|
|
5961
|
-
return model.startsWith(CABANE_NATIVE_MODEL_PREFIX) ? model.slice(CABANE_NATIVE_MODEL_PREFIX.length) : model;
|
|
5962
|
-
}
|
|
5963
|
-
|
|
5964
|
-
// packages/agent-runtime/src/cabane-native/policy.ts
|
|
5965
|
-
import { z as z13 } from "zod";
|
|
5966
|
-
var NATIVE_SURFACES = ["code", "classic"];
|
|
5967
|
-
var cabaneNativeDialectSchema = z13.object({ surface: z13.enum(NATIVE_SURFACES).optional() }).loose();
|
|
5968
|
-
function readCabaneNativeDialect(runtimeOptions) {
|
|
5969
|
-
const parsed = cabaneNativeDialectSchema.safeParse(runtimeOptions?.["cabane-native"] ?? {});
|
|
5970
|
-
return parsed.success ? parsed.data : {};
|
|
5971
|
-
}
|
|
5972
|
-
function cabaneNativeSurface(runtimeOptions) {
|
|
5973
|
-
return readCabaneNativeDialect(runtimeOptions).surface ?? "code";
|
|
5974
|
-
}
|
|
5975
|
-
|
|
5976
|
-
// packages/agent-runtime/src/cabane-native/tools.ts
|
|
5977
|
-
var TOOL_RESULT_MAX_CHARS = 8e3;
|
|
5978
|
-
var CABANE_NATIVE_TOOLS = [
|
|
5979
|
-
{
|
|
5980
|
-
type: "function",
|
|
5981
|
-
function: {
|
|
5982
|
-
name: "cabane_list",
|
|
5983
|
-
description: "List the files and subfolders at a workspace folder path. Omit `path` for the root.",
|
|
5984
|
-
parameters: {
|
|
5985
|
-
type: "object",
|
|
5986
|
-
properties: {
|
|
5987
|
-
path: {
|
|
5988
|
-
type: "string",
|
|
5989
|
-
description: "Workspace folder path, e.g. /notes. Defaults to /."
|
|
5990
|
-
}
|
|
5991
|
-
}
|
|
5992
|
-
}
|
|
5993
|
-
}
|
|
5994
|
-
},
|
|
5995
|
-
{
|
|
5996
|
-
type: "function",
|
|
5997
|
-
function: {
|
|
5998
|
-
name: "cabane_read",
|
|
5999
|
-
description: "Read the contents of one file at a workspace path.",
|
|
6000
|
-
parameters: {
|
|
6001
|
-
type: "object",
|
|
6002
|
-
properties: {
|
|
6003
|
-
path: { type: "string", description: "Workspace file path, e.g. /notes/todo.md." }
|
|
6004
|
-
},
|
|
6005
|
-
required: ["path"]
|
|
6006
|
-
}
|
|
6007
|
-
}
|
|
6008
|
-
},
|
|
6009
|
-
{
|
|
6010
|
-
type: "function",
|
|
6011
|
-
function: {
|
|
6012
|
-
name: "cabane_search",
|
|
6013
|
-
description: "Case-insensitive substring search across file names and file contents. Optionally scope to a subtree with `path`.",
|
|
6014
|
-
parameters: {
|
|
6015
|
-
type: "object",
|
|
6016
|
-
properties: {
|
|
6017
|
-
q: { type: "string", description: "The search string." },
|
|
6018
|
-
path: {
|
|
6019
|
-
type: "string",
|
|
6020
|
-
description: "Optional workspace subtree to scope the search to."
|
|
6021
|
-
}
|
|
6022
|
-
},
|
|
6023
|
-
required: ["q"]
|
|
6024
|
-
}
|
|
6025
|
-
}
|
|
6026
|
-
},
|
|
6027
|
-
{
|
|
6028
|
-
type: "function",
|
|
6029
|
-
function: {
|
|
6030
|
-
name: "cabane_write",
|
|
6031
|
-
description: "Create a file at a workspace path (missing parent folders are created). Pass `overwrite: true` to replace an existing file instead of failing on a name conflict.",
|
|
6032
|
-
parameters: {
|
|
6033
|
-
type: "object",
|
|
6034
|
-
properties: {
|
|
6035
|
-
path: { type: "string", description: "Workspace file path, e.g. /notes/new.md." },
|
|
6036
|
-
content: { type: "string", description: "The file contents." },
|
|
6037
|
-
overwrite: { type: "boolean", description: "Replace an existing file (default false)." }
|
|
6038
|
-
},
|
|
6039
|
-
required: ["path", "content"]
|
|
6040
|
-
}
|
|
6041
|
-
}
|
|
6042
|
-
},
|
|
6043
|
-
{
|
|
6044
|
-
type: "function",
|
|
6045
|
-
function: {
|
|
6046
|
-
name: "cabane_edit",
|
|
6047
|
-
description: "Modify an existing file with a single find/replace. By default `find` must occur exactly once; set `replaceAll: true` to replace every occurrence.",
|
|
6048
|
-
parameters: {
|
|
6049
|
-
type: "object",
|
|
6050
|
-
properties: {
|
|
6051
|
-
path: { type: "string", description: "Workspace file path to edit." },
|
|
6052
|
-
find: { type: "string", description: "The substring to find." },
|
|
6053
|
-
replace: { type: "string", description: "The replacement." },
|
|
6054
|
-
replaceAll: { type: "boolean", description: "Replace every occurrence (default false)." }
|
|
6055
|
-
},
|
|
6056
|
-
required: ["path", "find", "replace"]
|
|
6057
|
-
}
|
|
6058
|
-
}
|
|
6059
|
-
},
|
|
6060
|
-
{
|
|
6061
|
-
type: "function",
|
|
6062
|
-
function: {
|
|
6063
|
-
name: "cabane_mkdir",
|
|
6064
|
-
description: "Create a folder at a workspace path. Pass `recursive: true` to also create any missing parent folders (like `mkdir -p`); otherwise the parent must already exist.",
|
|
6065
|
-
parameters: {
|
|
6066
|
-
type: "object",
|
|
6067
|
-
properties: {
|
|
6068
|
-
path: { type: "string", description: "Workspace folder path, e.g. /notes/archive." },
|
|
6069
|
-
recursive: {
|
|
6070
|
-
type: "boolean",
|
|
6071
|
-
description: "Create missing parent folders too (default false)."
|
|
6072
|
-
}
|
|
6073
|
-
},
|
|
6074
|
-
required: ["path"]
|
|
6075
|
-
}
|
|
6076
|
-
}
|
|
6077
|
-
},
|
|
6078
|
-
{
|
|
6079
|
-
type: "function",
|
|
6080
|
-
function: {
|
|
6081
|
-
name: "cabane_move",
|
|
6082
|
-
description: "Move and/or rename a file or folder. `fromPath` is the current path; `toPath` is the new full path (its parent must already exist). Same parent + new name renames in place; a different parent moves it.",
|
|
6083
|
-
parameters: {
|
|
6084
|
-
type: "object",
|
|
6085
|
-
properties: {
|
|
6086
|
-
fromPath: { type: "string", description: "The current file/folder path." },
|
|
6087
|
-
toPath: { type: "string", description: "The new full path (parent + name)." }
|
|
6088
|
-
},
|
|
6089
|
-
required: ["fromPath", "toPath"]
|
|
6090
|
-
}
|
|
6091
|
-
}
|
|
6092
|
-
},
|
|
6093
|
-
{
|
|
6094
|
-
type: "function",
|
|
6095
|
-
function: {
|
|
6096
|
-
name: "cabane_delete",
|
|
6097
|
-
description: "Delete the file or folder at a workspace path. Folders require `recursive: true` (deletes everything inside). Soft delete \u2014 the entry is recoverable from the trash, not erased.",
|
|
6098
|
-
parameters: {
|
|
6099
|
-
type: "object",
|
|
6100
|
-
properties: {
|
|
6101
|
-
path: { type: "string", description: "Workspace file/folder path to delete." },
|
|
6102
|
-
recursive: {
|
|
6103
|
-
type: "boolean",
|
|
6104
|
-
description: "Required to delete a non-empty folder (default false)."
|
|
6105
|
-
}
|
|
6106
|
-
},
|
|
6107
|
-
required: ["path"]
|
|
6108
|
-
}
|
|
6109
|
-
}
|
|
6110
|
-
},
|
|
6111
|
-
{
|
|
6112
|
-
type: "function",
|
|
6113
|
-
function: {
|
|
6114
|
-
name: "cabane_context",
|
|
6115
|
-
description: "Gather a workspace object plus its most relevant connected context \u2014 related files, conversations, people \u2014 in one call. Your first move to understand what surrounds something before acting. `ref` is the object id, or its workspace path when `kind` is `file` or `folder`.",
|
|
6116
|
-
parameters: {
|
|
6117
|
-
type: "object",
|
|
6118
|
-
properties: {
|
|
6119
|
-
kind: {
|
|
6120
|
-
type: "string",
|
|
6121
|
-
enum: ["conversation", "file", "folder", "user", "agent", "channel"],
|
|
6122
|
-
description: "The kind of object to orient around."
|
|
6123
|
-
},
|
|
6124
|
-
ref: {
|
|
6125
|
-
type: "string",
|
|
6126
|
-
description: "The object id (or the workspace path for a file/folder)."
|
|
6127
|
-
}
|
|
6128
|
-
},
|
|
6129
|
-
required: ["kind", "ref"]
|
|
6130
|
-
}
|
|
6131
|
-
}
|
|
6132
|
-
},
|
|
6133
|
-
{
|
|
6134
|
-
type: "function",
|
|
6135
|
-
function: {
|
|
6136
|
-
name: "cabane_messages",
|
|
6137
|
-
description: 'Read a page of messages from a conversation by id (get ids from `cabane_search` or `cabane_context`). `order: "desc"` (default) returns newest-first \u2014 where a thread is now; `order: "asc"` returns oldest-first \u2014 how it began. Your own current thread is already in context; use this to catch up on other conversations or to page further back.',
|
|
6138
|
-
parameters: {
|
|
6139
|
-
type: "object",
|
|
6140
|
-
properties: {
|
|
6141
|
-
conversationId: { type: "string", description: "The conversation id to read." },
|
|
6142
|
-
order: {
|
|
6143
|
-
type: "string",
|
|
6144
|
-
enum: ["asc", "desc"],
|
|
6145
|
-
description: "newest-first (desc, default) or oldest-first (asc)."
|
|
6146
|
-
},
|
|
6147
|
-
limit: {
|
|
6148
|
-
type: "number",
|
|
6149
|
-
description: "Max messages to return (default 50, max 100)."
|
|
6150
|
-
}
|
|
6151
|
-
},
|
|
6152
|
-
required: ["conversationId"]
|
|
6153
|
-
}
|
|
6154
|
-
}
|
|
6155
|
-
}
|
|
6156
|
-
];
|
|
6157
|
-
var CABANE_NATIVE_CODE_TOOL = {
|
|
6158
|
-
type: "function",
|
|
6159
|
-
function: {
|
|
6160
|
-
name: "sdk",
|
|
6161
|
-
description: "Run a TypeScript program against the workspace. `code` is the program \u2014 the body of an async function, so top-level `await` works and `return <value>` produces the result. Write against the ambient `cabane` object (no imports); only what you `return` plus `console.log` comes back, so gather/filter/summarize inside the program and return the small answer. The full `cabane` SDK surface \u2014 every method and its options \u2014 is documented in your system prompt above.",
|
|
6162
|
-
parameters: {
|
|
6163
|
-
type: "object",
|
|
6164
|
-
properties: {
|
|
6165
|
-
code: {
|
|
6166
|
-
type: "string",
|
|
6167
|
-
description: "The TypeScript program to run (an async function body)."
|
|
6168
|
-
}
|
|
6169
|
-
},
|
|
6170
|
-
required: ["code"]
|
|
6171
|
-
}
|
|
6172
|
-
}
|
|
6173
|
-
};
|
|
6174
|
-
var CABANE_NATIVE_RENDER_TOKEN_TOOL = {
|
|
6175
|
-
type: "function",
|
|
6176
|
-
function: {
|
|
6177
|
-
name: "mint_render_token",
|
|
6178
|
-
description: "Mint a short-lived, scoped credential that lets a browser load a workspace HTML page at the content-isolation origin. Returns `{ url, cookieName, cookieValue, expiresAt, pathPrefix }`. `pathPrefix` (default `/`) narrows the grant to a folder or single file; `ttlSeconds` (default 30 min, max 86400) overrides the TTL.",
|
|
6179
|
-
parameters: {
|
|
6180
|
-
type: "object",
|
|
6181
|
-
properties: {
|
|
6182
|
-
pathPrefix: {
|
|
6183
|
-
type: "string",
|
|
6184
|
-
description: "Workspace-rooted path-prefix the token authorizes; `/` for whole-workspace."
|
|
6185
|
-
},
|
|
6186
|
-
ttlSeconds: {
|
|
6187
|
-
type: "number",
|
|
6188
|
-
description: "Override the default 30-minute TTL; capped at 24h (86400s)."
|
|
6189
|
-
}
|
|
6190
|
-
}
|
|
6191
|
-
}
|
|
6192
|
-
}
|
|
6193
|
-
};
|
|
6194
|
-
function summarizeCabaneToolArgs(name, args) {
|
|
6195
|
-
if (name === "mint_render_token") return str2(args.pathPrefix) ?? "/";
|
|
6196
|
-
if (name === "sdk") {
|
|
6197
|
-
const code = str2(args.code) ?? "";
|
|
6198
|
-
const firstLine = code.split("\n").find((l) => l.trim().length > 0)?.trim() ?? "";
|
|
6199
|
-
return firstLine.length > 80 ? `${firstLine.slice(0, 80)}\u2026` : firstLine;
|
|
6200
|
-
}
|
|
6201
|
-
if (name === "cabane_search") return str2(args.q) ?? "";
|
|
6202
|
-
if (name === "cabane_move") {
|
|
6203
|
-
const from = str2(args.fromPath) ?? "";
|
|
6204
|
-
const to = str2(args.toPath) ?? "";
|
|
6205
|
-
return from && to ? `${from} \u2192 ${to}` : from || to;
|
|
6206
|
-
}
|
|
6207
|
-
if (name === "cabane_context") return str2(args.ref) ?? "";
|
|
6208
|
-
if (name === "cabane_messages") return str2(args.conversationId) ?? "";
|
|
6209
|
-
return str2(args.path) ?? "";
|
|
6210
|
-
}
|
|
6211
|
-
async function executeCabaneNativeTool(name, args, ctx, signal) {
|
|
6212
|
-
const doFetch = ctx.fetchImpl ?? fetch;
|
|
6213
|
-
const wsBase = `${ctx.apiRoot}/workspaces/${ctx.workspaceId}`;
|
|
6214
|
-
const headers = { Authorization: `Bearer ${ctx.bearer}`, "Content-Type": "application/json" };
|
|
6215
|
-
const call = async (method, path3, init2) => {
|
|
6216
|
-
const url = new URL(`${wsBase}${path3}`);
|
|
6217
|
-
for (const [k, v] of Object.entries(init2?.query ?? {})) {
|
|
6218
|
-
if (v !== void 0) url.searchParams.set(k, v);
|
|
6219
|
-
}
|
|
6220
|
-
let res;
|
|
6221
|
-
try {
|
|
6222
|
-
res = await doFetch(url.toString(), {
|
|
6223
|
-
method,
|
|
6224
|
-
headers,
|
|
6225
|
-
...init2?.body !== void 0 ? { body: JSON.stringify(init2.body) } : {},
|
|
6226
|
-
signal
|
|
6227
|
-
});
|
|
6228
|
-
} catch (err2) {
|
|
6229
|
-
return {
|
|
6230
|
-
ok: false,
|
|
6231
|
-
result: `error: request failed: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
6232
|
-
};
|
|
6233
|
-
}
|
|
6234
|
-
const contentType = res.headers.get("content-type") ?? "";
|
|
6235
|
-
const text = contentType.includes("application/json") ? JSON.stringify(await res.json().catch(() => ({}))) : await res.text().catch(() => "");
|
|
6236
|
-
if (!res.ok) return { ok: false, result: truncate2(`error ${res.status}: ${text}`) };
|
|
6237
|
-
return { ok: true, result: truncate2(text) };
|
|
6238
|
-
};
|
|
6239
|
-
switch (name) {
|
|
6240
|
-
case "sdk":
|
|
6241
|
-
return call("POST", "/sdk", {
|
|
6242
|
-
body: {
|
|
6243
|
-
program: str2(args.code) ?? "",
|
|
6244
|
-
...ctx.conversationId ? { activeConversationId: ctx.conversationId } : {}
|
|
6245
|
-
}
|
|
6246
|
-
});
|
|
6247
|
-
case "mint_render_token":
|
|
6248
|
-
return call("POST", "/render-tokens", {
|
|
6249
|
-
body: {
|
|
6250
|
-
pathPrefix: str2(args.pathPrefix) ?? "/",
|
|
6251
|
-
...typeof args.ttlSeconds === "number" ? { ttlSeconds: args.ttlSeconds } : {}
|
|
6252
|
-
}
|
|
6253
|
-
});
|
|
6254
|
-
case "cabane_list":
|
|
6255
|
-
return call("GET", "/files/tree", { query: { path: str2(args.path) ?? "/" } });
|
|
6256
|
-
case "cabane_read":
|
|
6257
|
-
return call("GET", "/files/content", { query: { path: str2(args.path) } });
|
|
6258
|
-
case "cabane_search":
|
|
6259
|
-
return call("GET", "/search", { query: { q: str2(args.q), path: str2(args.path) } });
|
|
6260
|
-
case "cabane_write": {
|
|
6261
|
-
const overwrite = args.overwrite === true;
|
|
6262
|
-
return overwrite ? call("PUT", "/files/content", {
|
|
6263
|
-
body: { path: str2(args.path), content: str2(args.content) }
|
|
6264
|
-
}) : call("POST", "/files", {
|
|
6265
|
-
body: { path: str2(args.path), content: str2(args.content), mkdirs: true }
|
|
6266
|
-
});
|
|
6267
|
-
}
|
|
6268
|
-
case "cabane_edit":
|
|
6269
|
-
return call("PATCH", "/files", {
|
|
6270
|
-
body: {
|
|
6271
|
-
path: str2(args.path),
|
|
6272
|
-
find: str2(args.find),
|
|
6273
|
-
replace: str2(args.replace) ?? "",
|
|
6274
|
-
...args.replaceAll === true ? { replaceAll: true } : {}
|
|
6275
|
-
}
|
|
6276
|
-
});
|
|
6277
|
-
case "cabane_mkdir":
|
|
6278
|
-
return call("POST", "/folders", {
|
|
6279
|
-
body: {
|
|
6280
|
-
path: str2(args.path),
|
|
6281
|
-
...args.recursive === true ? { recursive: true } : {}
|
|
6282
|
-
}
|
|
6283
|
-
});
|
|
6284
|
-
case "cabane_move":
|
|
6285
|
-
return call("POST", "/entries/move", {
|
|
6286
|
-
body: { fromPath: str2(args.fromPath), toPath: str2(args.toPath) }
|
|
6287
|
-
});
|
|
6288
|
-
case "cabane_delete":
|
|
6289
|
-
return call("DELETE", "/entries", {
|
|
6290
|
-
query: {
|
|
6291
|
-
path: str2(args.path),
|
|
6292
|
-
recursive: args.recursive === true ? "true" : void 0
|
|
6293
|
-
}
|
|
6294
|
-
});
|
|
6295
|
-
case "cabane_context":
|
|
6296
|
-
return call("POST", "/graph/query", {
|
|
6297
|
-
body: { preset: "orient", args: { kind: str2(args.kind), ref: str2(args.ref) } }
|
|
6298
|
-
});
|
|
6299
|
-
case "cabane_messages":
|
|
6300
|
-
return call(
|
|
6301
|
-
"GET",
|
|
6302
|
-
`/conversations/${encodeURIComponent(str2(args.conversationId) ?? "")}/messages`,
|
|
6303
|
-
{
|
|
6304
|
-
query: {
|
|
6305
|
-
order: str2(args.order),
|
|
6306
|
-
limit: typeof args.limit === "number" ? String(args.limit) : void 0
|
|
6307
|
-
}
|
|
6308
|
-
}
|
|
6309
|
-
);
|
|
6310
|
-
default:
|
|
6311
|
-
return { ok: false, result: `error: unknown tool "${name}"` };
|
|
6312
|
-
}
|
|
6313
|
-
}
|
|
6314
|
-
function str2(v) {
|
|
6315
|
-
return typeof v === "string" ? v : void 0;
|
|
6316
|
-
}
|
|
6317
|
-
function truncate2(s) {
|
|
6318
|
-
return s.length > TOOL_RESULT_MAX_CHARS ? `${s.slice(0, TOOL_RESULT_MAX_CHARS)}
|
|
6319
|
-
\u2026 [truncated]` : s;
|
|
6320
|
-
}
|
|
6321
|
-
|
|
6322
|
-
// packages/agent-runtime/src/cabane-native/turn-control.ts
|
|
6323
|
-
function describeSubAgentError(status, body) {
|
|
6324
|
-
const code = body && typeof body === "object" && "error" in body ? String(body.error) : void 0;
|
|
6325
|
-
switch (code) {
|
|
6326
|
-
case "callout_cap_exceeded":
|
|
6327
|
-
return "sub_agent: you already have the maximum open sub-agents for this thread. Wait for some to return \u2014 you're woken once they're all back \u2014 before spawning more.";
|
|
6328
|
-
case "callout_depth_exceeded":
|
|
6329
|
-
return "sub_agent: this would nest sub-agents too deep (max 3 levels). Have the current worker report back rather than spawning another layer.";
|
|
6330
|
-
case "dispatch_agent_not_found":
|
|
6331
|
-
return "sub_agent: no live agent in this workspace matches that `agentId`. Check `list_agents`, or omit `agentId` to spawn yourself.";
|
|
6332
|
-
case "dispatch_return_requires_turn":
|
|
6333
|
-
case "dispatch_return_requires_agent":
|
|
6334
|
-
case "dispatch_return_requires_dispatch":
|
|
6335
|
-
return `sub_agent: the spawn was rejected (${code}). This is a turn-context problem, not something to retry blindly \u2014 report it rather than looping.`;
|
|
6336
|
-
default:
|
|
6337
|
-
return `sub_agent: the spawn failed (${code ?? `HTTP ${status}`}).`;
|
|
6338
|
-
}
|
|
6339
|
-
}
|
|
6340
|
-
var CABANE_NATIVE_TURN_CONTROL_TOOLS = [
|
|
6341
|
-
{
|
|
6342
|
-
type: "function",
|
|
6343
|
-
function: {
|
|
6344
|
-
name: "ask",
|
|
6345
|
-
description: "Ask a HUMAN a structured question you need answered to continue, then END your turn (do not wait \u2014 a reply may take days). Pass `targetUserId` (a workspace member id). Single form: a one-sentence `headline` (the question, capitalized, ending in `?`) + a short `question` body of framing, with 2\u20134 `options` when the answer is a bounded/yes-no choice. Or `questions`: 1\u20135 items each `{ headline, body?, options? }` when several decisions land at once. Provide EITHER `question` or `questions`, never both. Your surrounding context goes in your reply; the ask carries the question.",
|
|
6346
|
-
parameters: {
|
|
6347
|
-
type: "object",
|
|
6348
|
-
properties: {
|
|
6349
|
-
targetUserId: {
|
|
6350
|
-
type: "string",
|
|
6351
|
-
description: "The workspace member (human) to ask \u2014 a user id from the roster."
|
|
6352
|
-
},
|
|
6353
|
-
question: {
|
|
6354
|
-
type: "string",
|
|
6355
|
-
description: "Single-question form: a short body of framing (one or two sentences)."
|
|
6356
|
-
},
|
|
6357
|
-
headline: {
|
|
6358
|
-
type: "string",
|
|
6359
|
-
description: "Single-question form: the question itself as one clear capitalized sentence ending in `?`."
|
|
6360
|
-
},
|
|
6361
|
-
options: {
|
|
6362
|
-
type: "array",
|
|
6363
|
-
items: { type: "string" },
|
|
6364
|
-
description: "Single-question form: 2\u20134 suggested one-click answers."
|
|
6365
|
-
},
|
|
6366
|
-
questions: {
|
|
6367
|
-
type: "array",
|
|
6368
|
-
items: {
|
|
6369
|
-
type: "object",
|
|
6370
|
-
properties: {
|
|
6371
|
-
headline: { type: "string" },
|
|
6372
|
-
body: { type: "string" },
|
|
6373
|
-
options: { type: "array", items: { type: "string" } }
|
|
6374
|
-
},
|
|
6375
|
-
required: ["headline"]
|
|
6376
|
-
},
|
|
6377
|
-
description: "Multi-question form: 1\u20135 questions to ask at once."
|
|
6378
|
-
}
|
|
6379
|
-
},
|
|
6380
|
-
required: ["targetUserId"]
|
|
6381
|
-
}
|
|
6382
|
-
}
|
|
6383
|
-
},
|
|
6384
|
-
{
|
|
6385
|
-
type: "function",
|
|
6386
|
-
function: {
|
|
6387
|
-
name: "wake_me",
|
|
6388
|
-
description: 'Wake yourself later \u2014 end this turn now and be re-dispatched at a time you pick, to CHECK a condition that has not happened yet (a PR merging, a reply landing, "check back in five minutes"). Pass EXACTLY ONE of `afterSeconds` (a relative delay, \u226560) or `at` (an absolute ISO-8601 timestamp WITH a zone you compute yourself). `note` is the message to your future self \u2014 write the condition to re-check. It arms when the turn ends; one wake per turn.',
|
|
6389
|
-
parameters: {
|
|
6390
|
-
type: "object",
|
|
6391
|
-
properties: {
|
|
6392
|
-
afterSeconds: {
|
|
6393
|
-
type: "number",
|
|
6394
|
-
description: "Relative delay in seconds from when this turn ends (floor 60)."
|
|
6395
|
-
},
|
|
6396
|
-
at: {
|
|
6397
|
-
type: "string",
|
|
6398
|
-
description: "Absolute ISO-8601 timestamp with a zone (e.g. 2026-07-16T09:00:00-07:00)."
|
|
6399
|
-
},
|
|
6400
|
-
note: {
|
|
6401
|
-
type: "string",
|
|
6402
|
-
description: "A note to your future self \u2014 becomes the wake body (the condition to re-check)."
|
|
6403
|
-
}
|
|
6404
|
-
},
|
|
6405
|
-
required: ["note"]
|
|
6406
|
-
}
|
|
6407
|
-
}
|
|
6408
|
-
},
|
|
6409
|
-
{
|
|
6410
|
-
type: "function",
|
|
6411
|
-
function: {
|
|
6412
|
-
name: "summon_agent",
|
|
6413
|
-
description: "Summon another agent into THIS conversation \u2014 dispatch a peer to reply here on your turn. Pass the peer `agentId` (from the agent roster). The peer is dispatched on your final reply and picks up this conversation as context, so write the context/ask into your reply first. Writing `@handle` in prose summons no one \u2014 this tool is the only in-thread lever. Single target, last call wins; summoning yourself is a no-op.",
|
|
6414
|
-
parameters: {
|
|
6415
|
-
type: "object",
|
|
6416
|
-
properties: {
|
|
6417
|
-
agentId: {
|
|
6418
|
-
type: "string",
|
|
6419
|
-
description: "The peer agent to summon \u2014 a workspace agent id from the roster."
|
|
6420
|
-
}
|
|
6421
|
-
},
|
|
6422
|
-
required: ["agentId"]
|
|
6423
|
-
}
|
|
6424
|
-
}
|
|
6425
|
-
},
|
|
6426
|
-
{
|
|
6427
|
-
type: "function",
|
|
6428
|
-
function: {
|
|
6429
|
-
name: "sub_agent",
|
|
6430
|
-
description: "Spawn a sub-agent \u2014 hand a piece of work to a private worker with a fresh context window whose result comes back to you automatically. It does NOT return inline: this returns the child's id immediately and the outcome lands LATER as a message in this conversation (you're woken once every sub-agent you have out here has returned). So don't wait \u2014 finish what else this turn can do and end your turn. `prompt` is the child's self-contained opening instruction; `agentId` (optional) dispatches a peer instead of yourself; `title` (optional) names the child thread. Reach for it to isolate a big read or fan out N independent pieces in parallel.",
|
|
6431
|
-
parameters: {
|
|
6432
|
-
type: "object",
|
|
6433
|
-
properties: {
|
|
6434
|
-
prompt: {
|
|
6435
|
-
type: "string",
|
|
6436
|
-
description: "The sub-agent's self-contained opening instruction."
|
|
6437
|
-
},
|
|
6438
|
-
agentId: {
|
|
6439
|
-
type: "string",
|
|
6440
|
-
description: "Optional peer to run the sub-agent as; omit to spawn yourself."
|
|
6441
|
-
},
|
|
6442
|
-
title: { type: "string", description: "Optional title for the child thread." }
|
|
6443
|
-
},
|
|
6444
|
-
required: ["prompt"]
|
|
6445
|
-
}
|
|
6446
|
-
}
|
|
6447
|
-
},
|
|
6448
|
-
{
|
|
6449
|
-
type: "function",
|
|
6450
|
-
function: {
|
|
6451
|
-
name: "skip_turn",
|
|
6452
|
-
description: "End your current turn WITHOUT posting a reply. Call this when you've been dispatched but the message genuinely doesn't need a response from you \u2014 an aside, a question already answered, a pile-on someone else has. Your turn ends silently (no message bubble). `reason` is a short free-text note for telemetry. Prefer this over posting a low-value \"ok!\"; skipping IS the whole turn.",
|
|
6453
|
-
parameters: {
|
|
6454
|
-
type: "object",
|
|
6455
|
-
properties: {
|
|
6456
|
-
reason: {
|
|
6457
|
-
type: "string",
|
|
6458
|
-
description: "Short reason you are declining \u2014 used for telemetry."
|
|
6459
|
-
}
|
|
6460
|
-
},
|
|
6461
|
-
required: ["reason"]
|
|
6462
|
-
}
|
|
6463
|
-
}
|
|
6464
|
-
}
|
|
6465
|
-
];
|
|
6466
|
-
var CABANE_NATIVE_TURN_CONTROL_NAMES = new Set(
|
|
6467
|
-
CABANE_NATIVE_TURN_CONTROL_TOOLS.map((t) => t.function.name)
|
|
6468
|
-
);
|
|
6469
|
-
function summarizeTurnControlArgs(name, args) {
|
|
6470
|
-
if (name === "ask") return str3(args.headline) ?? str3(args.question) ?? "";
|
|
6471
|
-
if (name === "wake_me") return str3(args.note) ?? "";
|
|
6472
|
-
if (name === "summon_agent") return str3(args.agentId) ?? "";
|
|
6473
|
-
if (name === "sub_agent") return str3(args.title) ?? truncateHead(str3(args.prompt) ?? "", 60);
|
|
6474
|
-
if (name === "skip_turn") return str3(args.reason) ?? "";
|
|
6475
|
-
return "";
|
|
6476
|
-
}
|
|
6477
|
-
async function executeNativeTurnControl(name, args, tc) {
|
|
6478
|
-
switch (name) {
|
|
6479
|
-
case "summon_agent": {
|
|
6480
|
-
const agentId = str3(args.agentId);
|
|
6481
|
-
if (!agentId) return err("summon_agent: `agentId` is required.");
|
|
6482
|
-
tc.summon(agentId);
|
|
6483
|
-
return ok({ summoned: agentId });
|
|
6484
|
-
}
|
|
6485
|
-
case "skip_turn": {
|
|
6486
|
-
const reason = str3(args.reason);
|
|
6487
|
-
if (!reason) return err("skip_turn: `reason` is required.");
|
|
6488
|
-
tc.skip(reason);
|
|
6489
|
-
return ok({ skipped: true });
|
|
6490
|
-
}
|
|
6491
|
-
case "ask": {
|
|
6492
|
-
const targetUserId = str3(args.targetUserId);
|
|
6493
|
-
if (!targetUserId) return err("ask: `targetUserId` is required.");
|
|
6494
|
-
const hasSingle = args.question !== void 0;
|
|
6495
|
-
const hasArray = Array.isArray(args.questions) && args.questions.length > 0;
|
|
6496
|
-
if (hasSingle && hasArray)
|
|
6497
|
-
return err("ask: provide either `question` or `questions`, not both.");
|
|
6498
|
-
if (!hasSingle && !hasArray) return err("ask: provide `question` or `questions`.");
|
|
6499
|
-
const payload = hasArray ? { targetUserId, questions: args.questions } : {
|
|
6500
|
-
targetUserId,
|
|
6501
|
-
question: str3(args.question),
|
|
6502
|
-
...str3(args.headline) ? { headline: str3(args.headline) } : {},
|
|
6503
|
-
...Array.isArray(args.options) ? { options: args.options } : {}
|
|
6504
|
-
};
|
|
6505
|
-
tc.ask(payload);
|
|
6506
|
-
return ok({ asked: targetUserId });
|
|
6507
|
-
}
|
|
6508
|
-
case "wake_me": {
|
|
6509
|
-
const note = str3(args.note);
|
|
6510
|
-
if (!note) return err("wake_me: `note` is required.");
|
|
6511
|
-
const hasAfter = typeof args.afterSeconds === "number";
|
|
6512
|
-
const hasAt = typeof args.at === "string";
|
|
6513
|
-
if (hasAfter && hasAt)
|
|
6514
|
-
return err("wake_me: provide either `afterSeconds` or `at`, not both.");
|
|
6515
|
-
if (!hasAfter && !hasAt) return err("wake_me: provide `afterSeconds` or `at`.");
|
|
6516
|
-
tc.wake({
|
|
6517
|
-
...hasAfter ? { afterSeconds: args.afterSeconds } : {},
|
|
6518
|
-
...hasAt ? { at: args.at } : {},
|
|
6519
|
-
note
|
|
6520
|
-
});
|
|
6521
|
-
return ok({ armed: hasAt ? { at: args.at } : { afterSeconds: args.afterSeconds } });
|
|
6522
|
-
}
|
|
6523
|
-
case "sub_agent": {
|
|
6524
|
-
const prompt = str3(args.prompt);
|
|
6525
|
-
if (!prompt) return err("sub_agent: `prompt` is required.");
|
|
6526
|
-
const result = await tc.subAgent({
|
|
6527
|
-
prompt,
|
|
6528
|
-
...str3(args.agentId) ? { agentId: str3(args.agentId) } : {},
|
|
6529
|
-
...str3(args.title) ? { title: str3(args.title) } : {}
|
|
6530
|
-
});
|
|
6531
|
-
if (!result.ok) return err(result.error);
|
|
6532
|
-
return ok({
|
|
6533
|
-
conversationId: result.conversationId,
|
|
6534
|
-
note: "Spawned. Don't wait \u2014 finish what else this turn can do, then end your turn; the result posts back here."
|
|
6535
|
-
});
|
|
6536
|
-
}
|
|
6537
|
-
default:
|
|
6538
|
-
return err(`unknown turn-control tool "${name}"`);
|
|
6539
|
-
}
|
|
6540
|
-
}
|
|
6541
|
-
function ok(payload) {
|
|
6542
|
-
return { ok: true, result: JSON.stringify(payload) };
|
|
6543
|
-
}
|
|
6544
|
-
function err(message) {
|
|
6545
|
-
return { ok: false, result: `error: ${message}` };
|
|
6546
|
-
}
|
|
6547
|
-
function str3(v) {
|
|
6548
|
-
return typeof v === "string" ? v : void 0;
|
|
6549
|
-
}
|
|
6550
|
-
function truncateHead(s, n) {
|
|
6551
|
-
return s.length > n ? `${s.slice(0, n)}\u2026` : s;
|
|
6552
|
-
}
|
|
6553
|
-
|
|
6554
|
-
// packages/agent-runtime/src/cabane-native/loop.ts
|
|
6555
|
-
var DEFAULT_MAX_ITERATIONS = 12;
|
|
6556
|
-
async function* runCabaneNativeTurn(req, signal, deps) {
|
|
6557
|
-
if (!req.config.model) {
|
|
6558
|
-
yield { type: "result", ok: false, reason: "no_model" };
|
|
6559
|
-
return;
|
|
6560
|
-
}
|
|
6561
|
-
const model = parseCabaneNativeModel(req.config.model);
|
|
6562
|
-
const surface = cabaneNativeSurface(req.config.runtimeOptions);
|
|
6563
|
-
const turnControl = req.extra.turnControl;
|
|
6564
|
-
const tools = surface === "code" ? [
|
|
6565
|
-
CABANE_NATIVE_CODE_TOOL,
|
|
6566
|
-
CABANE_NATIVE_RENDER_TOKEN_TOOL,
|
|
6567
|
-
...turnControl ? CABANE_NATIVE_TURN_CONTROL_TOOLS : []
|
|
6568
|
-
] : CABANE_NATIVE_TOOLS;
|
|
6569
|
-
const toolCtx = {
|
|
6570
|
-
apiRoot: deps.apiRoot,
|
|
6571
|
-
workspaceId: deps.workspaceId,
|
|
6572
|
-
bearer: deps.bearer,
|
|
6573
|
-
// CT666: the origin conversation, forwarded by the `sdk` tool to the `/sdk`
|
|
6574
|
-
// endpoint so a code-mode program's returning callout binds to this turn.
|
|
6575
|
-
...deps.conversationId ? { conversationId: deps.conversationId } : {},
|
|
6576
|
-
...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
|
|
6577
|
-
};
|
|
6578
|
-
const messages = await assembleMessages(req.systemPrompt, req.content, req.prompt, {
|
|
6579
|
-
apiRoot: deps.apiRoot,
|
|
6580
|
-
workspaceId: deps.workspaceId,
|
|
6581
|
-
bearer: deps.bearer,
|
|
6582
|
-
conversationId: deps.conversationId,
|
|
6583
|
-
...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {},
|
|
6584
|
-
...deps.historyLimit !== void 0 ? { historyLimit: deps.historyLimit } : {}
|
|
6585
|
-
});
|
|
6586
|
-
if (signal.aborted) return;
|
|
6587
|
-
const maxIterations = deps.maxIterations ?? DEFAULT_MAX_ITERATIONS;
|
|
6588
|
-
let usage;
|
|
6589
|
-
let resolvedModel = model;
|
|
6590
|
-
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
6591
|
-
let text = "";
|
|
6592
|
-
const toolAcc = /* @__PURE__ */ new Map();
|
|
6593
|
-
let finishReason;
|
|
6594
|
-
let errored2;
|
|
6595
|
-
for await (const ev of deps.provider.stream({ model, messages, tools }, signal)) {
|
|
6596
|
-
if (signal.aborted) return;
|
|
6597
|
-
switch (ev.type) {
|
|
6598
|
-
case "text":
|
|
6599
|
-
text += ev.delta;
|
|
6600
|
-
break;
|
|
6601
|
-
case "tool_call": {
|
|
6602
|
-
const cur = toolAcc.get(ev.index) ?? { id: `call_${ev.index}`, name: "", args: "" };
|
|
6603
|
-
if (ev.id) cur.id = ev.id;
|
|
6604
|
-
if (ev.name) cur.name = ev.name;
|
|
6605
|
-
if (ev.argumentsDelta) cur.args += ev.argumentsDelta;
|
|
6606
|
-
toolAcc.set(ev.index, cur);
|
|
6607
|
-
break;
|
|
6608
|
-
}
|
|
6609
|
-
case "usage":
|
|
6610
|
-
usage = {
|
|
6611
|
-
inputTokens: ev.inputTokens,
|
|
6612
|
-
outputTokens: ev.outputTokens,
|
|
6613
|
-
contextTokens: ev.inputTokens
|
|
6614
|
-
};
|
|
6615
|
-
break;
|
|
6616
|
-
case "model":
|
|
6617
|
-
resolvedModel = ev.model;
|
|
6618
|
-
break;
|
|
6619
|
-
case "error":
|
|
6620
|
-
errored2 = ev.message;
|
|
6621
|
-
break;
|
|
6622
|
-
case "done":
|
|
6623
|
-
finishReason = ev.finishReason;
|
|
6624
|
-
break;
|
|
6625
|
-
}
|
|
6626
|
-
}
|
|
6627
|
-
if (signal.aborted) return;
|
|
6628
|
-
if (errored2 !== void 0) {
|
|
6629
|
-
const sealed = sealText(text, false);
|
|
6630
|
-
if (sealed) yield sealed;
|
|
6631
|
-
const failure = classifyErrorText(errored2);
|
|
6632
|
-
yield {
|
|
6633
|
-
type: "result",
|
|
6634
|
-
ok: false,
|
|
6635
|
-
reason: failure ? encodeFailureReason(failure) : `error:${errored2.slice(0, 200)}`,
|
|
6636
|
-
...usage ? { usage } : {},
|
|
6637
|
-
...resolvedModel ? { resolvedModel } : {}
|
|
6638
|
-
};
|
|
6639
|
-
return;
|
|
6640
|
-
}
|
|
6641
|
-
const toolCalls = [...toolAcc.entries()].sort((a, b) => a[0] - b[0]).map(([, v]) => v);
|
|
6642
|
-
if (toolCalls.length === 0) {
|
|
6643
|
-
const sealed = sealText(text, true);
|
|
6644
|
-
if (sealed) yield sealed;
|
|
6645
|
-
yield {
|
|
6646
|
-
type: "result",
|
|
6647
|
-
ok: true,
|
|
6648
|
-
...usage ? { usage } : {},
|
|
6649
|
-
...resolvedModel ? { resolvedModel } : {}
|
|
6650
|
-
};
|
|
6651
|
-
return;
|
|
6652
|
-
}
|
|
6653
|
-
const sealedInterim = sealText(text, false);
|
|
6654
|
-
if (sealedInterim) yield sealedInterim;
|
|
6655
|
-
const assistantToolCalls = toolCalls.map((t) => ({
|
|
6656
|
-
id: t.id,
|
|
6657
|
-
type: "function",
|
|
6658
|
-
function: { name: t.name, arguments: t.args || "{}" }
|
|
6659
|
-
}));
|
|
6660
|
-
messages.push({ role: "assistant", content: text, tool_calls: assistantToolCalls });
|
|
6661
|
-
for (const t of toolCalls) {
|
|
6662
|
-
if (signal.aborted) return;
|
|
6663
|
-
const args = parseArgs(t.args);
|
|
6664
|
-
const displayName = prettyToolName(t.name);
|
|
6665
|
-
const isTurnControl = CABANE_NATIVE_TURN_CONTROL_NAMES.has(t.name);
|
|
6666
|
-
const summary = isTurnControl ? summarizeTurnControlArgs(t.name, args) : summarizeCabaneToolArgs(t.name, args);
|
|
6667
|
-
yield { type: "tool", id: t.id, name: displayName, phase: "start", summary, input: args };
|
|
6668
|
-
const result = isTurnControl && turnControl ? await executeNativeTurnControl(t.name, args, turnControl) : await executeCabaneNativeTool(t.name, args, toolCtx, signal);
|
|
6669
|
-
if (signal.aborted) return;
|
|
6670
|
-
yield {
|
|
6671
|
-
type: "tool",
|
|
6672
|
-
id: t.id,
|
|
6673
|
-
name: displayName,
|
|
6674
|
-
phase: result.ok ? "done" : "error",
|
|
6675
|
-
summary,
|
|
6676
|
-
input: args,
|
|
6677
|
-
result: result.result
|
|
6678
|
-
};
|
|
6679
|
-
messages.push({ role: "tool", tool_call_id: t.id, content: result.result });
|
|
6680
|
-
}
|
|
6681
|
-
}
|
|
6682
|
-
deps.onWarn?.("cabane-native: turn hit the tool-iteration cap; force-settling", {
|
|
6683
|
-
maxIterations
|
|
6684
|
-
});
|
|
6685
|
-
yield {
|
|
6686
|
-
type: "text",
|
|
6687
|
-
body: `(Stopped after ${maxIterations} tool steps without a final answer.)`,
|
|
6688
|
-
terminal: true
|
|
6689
|
-
};
|
|
6690
|
-
yield {
|
|
6691
|
-
type: "result",
|
|
6692
|
-
ok: true,
|
|
6693
|
-
...usage ? { usage } : {},
|
|
6694
|
-
...resolvedModel ? { resolvedModel } : {}
|
|
6695
|
-
};
|
|
6696
|
-
}
|
|
6697
|
-
function sealText(text, terminal) {
|
|
6698
|
-
const body = text.trim();
|
|
6699
|
-
if (body.length === 0) return null;
|
|
6700
|
-
return { type: "text", body, terminal };
|
|
6701
|
-
}
|
|
6702
|
-
function parseArgs(raw) {
|
|
6703
|
-
if (!raw.trim()) return {};
|
|
6704
|
-
try {
|
|
6705
|
-
const parsed = JSON.parse(raw);
|
|
6706
|
-
return parsed && typeof parsed === "object" ? parsed : {};
|
|
6707
|
-
} catch {
|
|
6708
|
-
return {};
|
|
6709
|
-
}
|
|
6710
|
-
}
|
|
6711
|
-
|
|
6712
|
-
// packages/agent-runtime/src/cabane-native/provider.ts
|
|
6713
|
-
var DEFAULT_OPENROUTER_BASE = "https://openrouter.ai/api/v1";
|
|
6714
|
-
function createOpenRouterProvider(opts) {
|
|
6715
|
-
const base = (opts.baseUrl ?? DEFAULT_OPENROUTER_BASE).replace(/\/$/, "");
|
|
6716
|
-
const doFetch = opts.fetchImpl ?? fetch;
|
|
6717
|
-
return {
|
|
6718
|
-
async *stream(req, signal) {
|
|
6719
|
-
let res;
|
|
6720
|
-
try {
|
|
6721
|
-
res = await doFetch(`${base}/chat/completions`, {
|
|
6722
|
-
method: "POST",
|
|
6723
|
-
headers: {
|
|
6724
|
-
Authorization: `Bearer ${opts.apiKey}`,
|
|
6725
|
-
"Content-Type": "application/json",
|
|
6726
|
-
// OpenRouter attribution headers (optional, but polite + used for
|
|
6727
|
-
// routing/analytics on their side).
|
|
6728
|
-
"HTTP-Referer": "https://cabane.ai",
|
|
6729
|
-
"X-Title": "Cabane"
|
|
6730
|
-
},
|
|
6731
|
-
body: JSON.stringify({
|
|
6732
|
-
model: req.model,
|
|
6733
|
-
messages: req.messages,
|
|
6734
|
-
...req.tools.length > 0 ? { tools: req.tools } : {},
|
|
6735
|
-
stream: true,
|
|
6736
|
-
// Ask OpenRouter to append a trailing usage chunk to the stream.
|
|
6737
|
-
stream_options: { include_usage: true }
|
|
6738
|
-
}),
|
|
6739
|
-
signal
|
|
6740
|
-
});
|
|
6741
|
-
} catch (err2) {
|
|
6742
|
-
if (signal.aborted) return;
|
|
6743
|
-
yield { type: "error", message: `request failed: ${errText(err2)}` };
|
|
6744
|
-
return;
|
|
6745
|
-
}
|
|
6746
|
-
if (!res.ok || !res.body) {
|
|
6747
|
-
const bodyText = await res.text().catch(() => "");
|
|
6748
|
-
yield { type: "error", message: providerErrorMessage(res.status, bodyText) };
|
|
6749
|
-
return;
|
|
6750
|
-
}
|
|
6751
|
-
const decoder = new TextDecoder();
|
|
6752
|
-
const reader = res.body.getReader();
|
|
6753
|
-
let buffer = "";
|
|
6754
|
-
let finishReason;
|
|
6755
|
-
let modelEmitted = false;
|
|
6756
|
-
try {
|
|
6757
|
-
for (; ; ) {
|
|
6758
|
-
if (signal.aborted) return;
|
|
6759
|
-
const { value, done } = await reader.read();
|
|
6760
|
-
if (done) break;
|
|
6761
|
-
buffer += decoder.decode(value, { stream: true });
|
|
6762
|
-
let nl;
|
|
6763
|
-
while ((nl = buffer.indexOf("\n")) !== -1) {
|
|
6764
|
-
const line = buffer.slice(0, nl).trim();
|
|
6765
|
-
buffer = buffer.slice(nl + 1);
|
|
6766
|
-
if (!line || line.startsWith(":")) continue;
|
|
6767
|
-
if (!line.startsWith("data:")) continue;
|
|
6768
|
-
const data = line.slice("data:".length).trim();
|
|
6769
|
-
if (data === "[DONE]") {
|
|
6770
|
-
yield { type: "done", ...finishReason ? { finishReason } : {} };
|
|
6771
|
-
return;
|
|
6772
|
-
}
|
|
6773
|
-
let chunk;
|
|
6774
|
-
try {
|
|
6775
|
-
chunk = JSON.parse(data);
|
|
6776
|
-
} catch {
|
|
6777
|
-
continue;
|
|
6778
|
-
}
|
|
6779
|
-
if (chunk.error) {
|
|
6780
|
-
yield { type: "error", message: chunk.error.message ?? "provider error" };
|
|
6781
|
-
return;
|
|
6782
|
-
}
|
|
6783
|
-
if (!modelEmitted && chunk.model) {
|
|
6784
|
-
modelEmitted = true;
|
|
6785
|
-
yield { type: "model", model: chunk.model };
|
|
6786
|
-
}
|
|
6787
|
-
const choice = chunk.choices?.[0];
|
|
6788
|
-
if (choice) {
|
|
6789
|
-
const delta = choice.delta;
|
|
6790
|
-
if (delta?.content) yield { type: "text", delta: delta.content };
|
|
6791
|
-
if (delta?.tool_calls) {
|
|
6792
|
-
for (const tc of delta.tool_calls) {
|
|
6793
|
-
yield {
|
|
6794
|
-
type: "tool_call",
|
|
6795
|
-
index: tc.index,
|
|
6796
|
-
...tc.id ? { id: tc.id } : {},
|
|
6797
|
-
...tc.function?.name ? { name: tc.function.name } : {},
|
|
6798
|
-
...tc.function?.arguments !== void 0 ? { argumentsDelta: tc.function.arguments } : {}
|
|
6799
|
-
};
|
|
6800
|
-
}
|
|
6801
|
-
}
|
|
6802
|
-
if (choice.finish_reason) finishReason = choice.finish_reason;
|
|
6803
|
-
}
|
|
6804
|
-
if (chunk.usage) {
|
|
6805
|
-
yield {
|
|
6806
|
-
type: "usage",
|
|
6807
|
-
inputTokens: chunk.usage.prompt_tokens ?? 0,
|
|
6808
|
-
outputTokens: chunk.usage.completion_tokens ?? 0
|
|
6809
|
-
};
|
|
6810
|
-
}
|
|
6811
|
-
}
|
|
6812
|
-
}
|
|
6813
|
-
} catch (err2) {
|
|
6814
|
-
if (signal.aborted) return;
|
|
6815
|
-
yield { type: "error", message: `stream read failed: ${errText(err2)}` };
|
|
6816
|
-
return;
|
|
6817
|
-
}
|
|
6818
|
-
yield { type: "done", ...finishReason ? { finishReason } : {} };
|
|
6819
|
-
}
|
|
6820
|
-
};
|
|
6821
|
-
}
|
|
6822
|
-
function providerErrorMessage(status, body) {
|
|
6823
|
-
let detail = body.slice(0, 300);
|
|
6824
|
-
try {
|
|
6825
|
-
const parsed = JSON.parse(body);
|
|
6826
|
-
if (parsed.error?.message) detail = parsed.error.message;
|
|
6827
|
-
} catch {
|
|
6828
|
-
}
|
|
6829
|
-
return `HTTP ${status}: ${detail}`;
|
|
6830
|
-
}
|
|
6831
|
-
function errText(err2) {
|
|
6832
|
-
return err2 instanceof Error ? err2.message : String(err2);
|
|
6833
|
-
}
|
|
6834
|
-
|
|
6835
|
-
// packages/agent-runtime/src/cabane-native/index.ts
|
|
6836
|
-
var CABANE_NATIVE_RUNTIME_NAME = "cabane-native";
|
|
6837
|
-
function createCabaneNativeAdapter(deps = {}) {
|
|
6838
|
-
const provider = deps.provider ?? (deps.apiKey ? createOpenRouterProvider({
|
|
6839
|
-
apiKey: deps.apiKey,
|
|
6840
|
-
...deps.baseUrl ? { baseUrl: deps.baseUrl } : {},
|
|
6841
|
-
...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
|
|
6842
|
-
}) : null);
|
|
6843
|
-
return {
|
|
6844
|
-
name: CABANE_NATIVE_RUNTIME_NAME,
|
|
6845
|
-
// CT666: the native runtime's addendum is now SURFACE-aware. Code mode (the
|
|
6846
|
-
// default In-Cabane surface) mounts the single `sdk` tool, so it's taught the
|
|
6847
|
-
// code-mode addendum (act by writing a `cabane` SDK program; the `cabane_*`
|
|
6848
|
-
// names don't exist here); classic (the retained CT663 10-tool fallback) is
|
|
6849
|
-
// taught the granular `cabane_*` listing. The caller (turn-context) passes
|
|
6850
|
-
// `codeMode` off the SAME lifted surface value the loop mounts tools from, so
|
|
6851
|
-
// the prompt and the mounted surface can't drift. (Supersedes CT614's
|
|
6852
|
-
// flag-independent addendum: native no longer ignores this arg.)
|
|
6853
|
-
promptAddendum: (codeMode) => codeMode ? CABANE_NATIVE_ADDENDUM_CODE_MODE : CABANE_NATIVE_ADDENDUM,
|
|
6854
|
-
dialectSchema: cabaneNativeDialectSchema,
|
|
6855
|
-
async *runTurn(req, signal) {
|
|
6856
|
-
if (!provider) {
|
|
6857
|
-
yield {
|
|
6858
|
-
type: "result",
|
|
6859
|
-
ok: false,
|
|
6860
|
-
reason: "cabane_native_unavailable:no OPENROUTER_API_KEY configured on this device"
|
|
6861
|
-
};
|
|
6862
|
-
return;
|
|
6863
|
-
}
|
|
6864
|
-
const workspaceId = req.cabane.workspaceId;
|
|
6865
|
-
if (!workspaceId) {
|
|
6866
|
-
yield {
|
|
6867
|
-
type: "result",
|
|
6868
|
-
ok: false,
|
|
6869
|
-
reason: "cabane_native_unavailable:turn carried no workspaceId"
|
|
6870
|
-
};
|
|
6871
|
-
return;
|
|
6872
|
-
}
|
|
6873
|
-
const apiRoot = req.cabane.mcpUrl.replace(/\/mcp\/?$/, "");
|
|
6874
|
-
yield* runCabaneNativeTurn(req, signal, {
|
|
6875
|
-
provider,
|
|
6876
|
-
apiRoot,
|
|
6877
|
-
workspaceId,
|
|
6878
|
-
bearer: req.cabane.bearer,
|
|
6879
|
-
conversationId: req.cabane.activeConversationId,
|
|
6880
|
-
...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {},
|
|
6881
|
-
...deps.onWarn ? { onWarn: deps.onWarn } : {},
|
|
6882
|
-
...deps.maxIterations !== void 0 ? { maxIterations: deps.maxIterations } : {}
|
|
6883
|
-
});
|
|
6884
|
-
}
|
|
6885
|
-
};
|
|
6886
|
-
}
|
|
6887
|
-
var cabaneNativeAdapter = createCabaneNativeAdapter();
|
|
6888
|
-
|
|
6889
5811
|
// packages/agent-runtime/src/claude-code/sdk.ts
|
|
6890
5812
|
import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
6891
5813
|
|
|
@@ -6943,7 +5865,7 @@ import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync
|
|
|
6943
5865
|
import { join as join12 } from "path";
|
|
6944
5866
|
|
|
6945
5867
|
// src/summon.ts
|
|
6946
|
-
import { z as
|
|
5868
|
+
import { z as z12 } from "zod";
|
|
6947
5869
|
var COMPANION_LOCAL_MCP_SERVER = "cabane_companion";
|
|
6948
5870
|
var SUMMON_AGENT_TOOL = "summon_agent";
|
|
6949
5871
|
var SUMMON_AGENT_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${SUMMON_AGENT_TOOL}`;
|
|
@@ -6977,7 +5899,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
6977
5899
|
SUMMON_AGENT_TOOL,
|
|
6978
5900
|
"Summon another agent into THIS conversation \u2014 dispatch a peer to reply here on your turn. Use it to hand part of the work to a teammate, or pull in an expert, without leaving the conversation. Pass the peer's `agentId` (discover handles + ids via `list_agents`). The peer is dispatched on your turn's final reply, so write the context/ask into that reply first \u2014 it receives your message + this conversation to work from. Writing `@handle` in your prose does NOT summon anyone (agent prose never dispatches); this tool is the only in-thread lever. Single target \u2014 the last call wins. Summoning yourself is a no-op. Reach for it when the human wants the peer's answer right HERE, in front of them \u2014 the reply lands in this thread, so there's no return to wire (a return is for work YOU consume, never a courtesy notification). A handoff to a DIFFERENT conversation is `create_conversation` / `post_message` with their `dispatch` field instead.",
|
|
6979
5901
|
{
|
|
6980
|
-
agentId:
|
|
5902
|
+
agentId: z12.string().uuid().describe("The peer agent to summon \u2014 a workspace agent id from `list_agents`.")
|
|
6981
5903
|
},
|
|
6982
5904
|
async (args) => {
|
|
6983
5905
|
summonState.agentId = args.agentId;
|
|
@@ -6992,7 +5914,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
6992
5914
|
SKIP_TURN_TOOL,
|
|
6993
5915
|
`End your current turn WITHOUT posting a reply. Call this when you've been dispatched but the message genuinely doesn't need a response from you \u2014 a thanks/aside, a question already answered, chatter outside your lane, or a pile-on where someone else has it. Your turn ends silently: no message bubble is posted. The \`reason\` is a short free-text note for telemetry (e.g. "already answered by cabane", "thanks, nothing to add"). Prefer this over posting a low-value "ok!"/"got it" reply. Don't also write a reply when you skip \u2014 skipping IS the whole turn.`,
|
|
6994
5916
|
{
|
|
6995
|
-
reason:
|
|
5917
|
+
reason: z12.string().min(1).max(500).describe("Short reason you are declining \u2014 used for telemetry/debugging.")
|
|
6996
5918
|
},
|
|
6997
5919
|
async (args) => {
|
|
6998
5920
|
skipState.skipped = true;
|
|
@@ -7009,21 +5931,21 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
7009
5931
|
ASK_TOOL,
|
|
7010
5932
|
"Ask a HUMAN a structured question (or a short LIST of them) you need answered to continue, then END your turn \u2014 don't wait for the reply. Use it when you genuinely can't proceed without a person's input (a decision only they can make, a missing fact, a go/no-go). Pass `targetUserId` (a workspace member's user id \u2014 get it from `mcp__cabane__list_members`). Two forms: a SINGLE question \u2014 a `headline` (the actual question as one clear, capitalized sentence ending in `?`, \"Do we go to prod?\") plus a short `question` body for the framing the headline can't hold \u2014 OR, when a plan ends with SEVERAL bounded decisions at once (\"three calls before I build: A? B? C?\"), a `questions` array of 1\u20135 items, each `{ headline, body?, options? }`. **Prefer the list over cramming the extra decisions into prose or dropping them** \u2014 end the turn with one ask carrying every question, never pick one and bury the rest. Each question keeps the same form rules: a one-sentence `headline`, a short `body` frame (NOT a report \u2014 your status, links, and detail go in your REPLY, and the body renders inline markdown only: links/emphasis/inline code, no bulleted lists or headings), and 2\u20134 `options` when the answer is a bounded choice \u2014 for a yes/no go-ahead always pass them, so it's one click, not a typed reply. An option can be a short button label OR a sentence that carries its own context; short/binary sets render as inline buttons, long ones stack full-width. Provide EITHER `question` (single) or `questions` (array), never both. The ask is recorded as a first-class attention item aimed at that person; your final reply carries the surrounding CONTEXT (what you found, why you're stuck), the ask carries the QUESTION(S). An open ask marks you as blocked until EVERY question is answered, so raise one only when you truly can't proceed \u2014 never ceremonially. One ask per turn (last call wins). After asking, stop \u2014 when the person replies addressed to you, the ask resolves and you resume; other people's or agents' messages may wake you but leave it open. Targets a human only; to hand work to another AGENT use summon/dispatch instead.",
|
|
7011
5933
|
{
|
|
7012
|
-
targetUserId:
|
|
7013
|
-
question:
|
|
5934
|
+
targetUserId: z12.string().uuid().describe("The workspace member (human) to ask \u2014 a user id from `list_members`."),
|
|
5935
|
+
question: z12.string().min(1).max(400).optional().describe(
|
|
7014
5936
|
"SINGLE-question form: a short body \u2014 one or two sentences of framing the headline can't hold. NOT a report (capped, inline markdown only). Provide EITHER this or `questions`, not both. Put the crisp one-sentence question in `headline`."
|
|
7015
5937
|
),
|
|
7016
|
-
headline:
|
|
5938
|
+
headline: z12.string().min(1).max(120).optional().describe(
|
|
7017
5939
|
'SINGLE-question form: the question itself as ONE clear, capitalized sentence ending in `?` ("Do we go to prod?"). What the human reads first in the inbox and the chip \u2014 one scannable question, no elaboration (that goes in `question`). Strongly encouraged.'
|
|
7018
5940
|
),
|
|
7019
|
-
options:
|
|
7020
|
-
questions:
|
|
7021
|
-
|
|
7022
|
-
headline:
|
|
5941
|
+
options: z12.array(z12.string().min(1).max(200)).min(2).max(4).optional().describe("SINGLE-question form: optional 2\u20134 suggested one-click answers."),
|
|
5942
|
+
questions: z12.array(
|
|
5943
|
+
z12.object({
|
|
5944
|
+
headline: z12.string().min(1).max(120).describe(
|
|
7023
5945
|
'The one-sentence question ("Do we go to prod?") \u2014 required for each item.'
|
|
7024
5946
|
),
|
|
7025
|
-
body:
|
|
7026
|
-
options:
|
|
5947
|
+
body: z12.string().min(1).max(400).optional().describe("Optional short framing beneath the headline. NOT a report."),
|
|
5948
|
+
options: z12.array(z12.string().min(1).max(200)).min(2).max(4).optional().describe("Optional 2\u20134 one-click answers for this question.")
|
|
7027
5949
|
})
|
|
7028
5950
|
).min(1).max(5).optional().describe(
|
|
7029
5951
|
"MULTI-question form: 1\u20135 questions to ask at once, when a plan ends with several bounded decisions. Provide EITHER this or `question`/`headline`/`options`, not both."
|
|
@@ -7080,13 +6002,13 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
7080
6002
|
SUB_AGENT_TOOL,
|
|
7081
6003
|
"Spawn a sub-agent \u2014 hand a piece of work to a private worker with a fresh context window, whose result comes back to you automatically. Modeled on the Task tool, with ONE deliberate difference: it does NOT return the result inline. A callee's turn can run for minutes and no turn may hold an unbounded wait, so the shape is spawn-now, results-on-wake \u2014 this returns immediately with the child's `conversationId`, and the outcome lands LATER as a message in THIS conversation; you're woken once every sub-agent you have out in this conversation has returned. So DON'T wait for it: after spawning, finish whatever else this turn can do and end your turn (never poll the child with reads in a loop \u2014 the wake is automatic). Parallel fan-out = call this N times in one turn (they run concurrently; ONE wake when all are in); series = one call per turn. `prompt` is the child's opening instruction \u2014 make it self-contained (the sub-agent starts fresh, with only this prompt + the thread it lands in). `agentId` (optional) dispatches a PEER instead of yourself \u2014 same mechanics, a different mind (use for capability/context you lack); default (self) is the pure sub-worker with a clean context window. `title` (optional) names the child thread (results link it, so a legible title helps). A single sub-agent has no wall-clock advantage (you idle either way) \u2014 it pays when the callee has capability/context you lack, or to isolate a big read from your own session; the real win is fan-out. Don't spawn one for a lookup you can do in-turn with your own tools. The result returns to YOU to act on \u2014 reach for it when you're the consumer of the output, not as a way to notify a human: if a person just wants to read the result, dispatch a plain (no-return) conversation and link it instead of spawning a sub-agent.",
|
|
7082
6004
|
{
|
|
7083
|
-
prompt:
|
|
6005
|
+
prompt: z12.string().min(1).max(65536).describe(
|
|
7084
6006
|
"The sub-agent's opening instruction \u2014 self-contained (it starts with a fresh context window; only this prompt + the thread it lands in)."
|
|
7085
6007
|
),
|
|
7086
|
-
agentId:
|
|
6008
|
+
agentId: z12.string().uuid().optional().describe(
|
|
7087
6009
|
"Optional peer to run the sub-agent as (a workspace agent id from `list_agents`); omit to spawn yourself with a fresh context window."
|
|
7088
6010
|
),
|
|
7089
|
-
title:
|
|
6011
|
+
title: z12.string().max(200).optional().describe("Optional title for the child thread (result chips link it).")
|
|
7090
6012
|
},
|
|
7091
6013
|
async (args) => {
|
|
7092
6014
|
const result = await subAgentCreate(args);
|
|
@@ -7116,13 +6038,13 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
7116
6038
|
WAKE_ME_TOOL,
|
|
7117
6039
|
'Wake yourself later \u2014 end this turn now and be re-dispatched at a time you pick, with a note you write to yourself. Use it for "wait until X": when the thing you need hasn\'t happened yet (a conversation isn\'t done, a PR isn\'t merged, a human hasn\'t answered), arm a wake, end your turn, and you\'re woken later to CHECK \u2014 read the workspace, and either act or re-arm. This is the loop behind "check back in five minutes", "keep checking until {condition}", and a scheduled re-try ("re-send once that agent\'s limit resets"). Ground the delay before you arm it. Almost every wake is short \u2014 seconds to a couple of hours \u2014 waiting on a condition you can name: a session limit resetting, a PR merging, a deploy landing, a throttled dispatch to re-send. Reach past a few hours only when (a) a human asked for that timing, or (b) the wait is pinned to a real external event you can name \u2014 a report that only runs Mondays, a scheduled appointment, a known reset time. A speculative far-future check-in you invented yourself \u2014 "in two weeks I\'ll see whether this feature is used" \u2014 is the one thing not to arm: if no one asked and you can\'t name both what clears the wait and why it takes that long, don\'t arm it \u2014 finish now, or raise an `ask`. Pass EXACTLY ONE of `afterSeconds` (a relative delay \u2014 `300` for five minutes) or `at` (an absolute ISO-8601 timestamp WITH a zone, e.g. `2026-07-16T09:00:00-07:00` \u2014 YOU compute it from a phrase like "tomorrow morning"; the system never parses natural-language time). `note` is a message to your future self \u2014 it becomes the body of the wake message that re-dispatches you, so write the condition to re-check ("check whether CT441 merged yet"). The wake is armed when your turn SETTLES, not now, so the delay counts from the turn ending; one wake per turn (last call wins). This is the sanctioned way to schedule your own continuation \u2014 the ONLY one; never reach for a host cron/scheduler. Guardrails: at least 60s out, at most 14 days; widen the interval as a loop ages (5m \u2192 15m \u2192 1h\u2026) rather than hammering; after many consecutive re-arms with no other activity you\'ll be steered to raise an `ask` to the human instead. If a wake can\'t be armed you\'re re-dispatched with a note explaining why \u2014 never a silent drop.',
|
|
7118
6040
|
{
|
|
7119
|
-
afterSeconds:
|
|
6041
|
+
afterSeconds: z12.number().int().positive().optional().describe(
|
|
7120
6042
|
"Relative delay in seconds from when this turn ends (e.g. 300 = five minutes). Provide EITHER this or `at`, not both. Floor 60s, horizon 14 days \u2014 enforced server-side."
|
|
7121
6043
|
),
|
|
7122
|
-
at:
|
|
6044
|
+
at: z12.string().datetime({ offset: true }).optional().describe(
|
|
7123
6045
|
"Absolute ISO-8601 timestamp WITH a zone (`Z` or `\xB1HH:MM`), e.g. `2026-07-16T09:00:00-07:00`. YOU compute it from a natural-language phrase using the current datetime in your turn context. Provide EITHER this or `afterSeconds`, not both."
|
|
7124
6046
|
),
|
|
7125
|
-
note:
|
|
6047
|
+
note: z12.string().min(1).max(2e3).describe(
|
|
7126
6048
|
'A note to your future self \u2014 becomes the body of the wake message that re-dispatches you. Write the condition to re-check ("check whether the PR merged").'
|
|
7127
6049
|
)
|
|
7128
6050
|
},
|
|
@@ -7202,7 +6124,6 @@ function buildCompanionTurnRequest(params) {
|
|
|
7202
6124
|
local: {
|
|
7203
6125
|
...params.cwd ? { cwd: params.cwd } : {},
|
|
7204
6126
|
...params.env ? { env: params.env } : {},
|
|
7205
|
-
...params.nativeWorkAssignment ? { nativeWorkAssignment: params.nativeWorkAssignment } : {},
|
|
7206
6127
|
// User MCP servers (already `${PLACEHOLDER}`-resolved). Structurally the
|
|
7207
6128
|
// adapter's `ResolvedMcpServers`.
|
|
7208
6129
|
...Object.keys(params.mcpServers).length > 0 ? { mcpServers: params.mcpServers } : {},
|
|
@@ -7210,10 +6131,9 @@ function buildCompanionTurnRequest(params) {
|
|
|
7210
6131
|
...params.claudeCode ? { claudeCode: params.claudeCode } : {}
|
|
7211
6132
|
},
|
|
7212
6133
|
// Host-injected: the companion-local summon server (for the subprocess adapters,
|
|
7213
|
-
// under its own namespace)
|
|
6134
|
+
// under its own namespace).
|
|
7214
6135
|
extra: {
|
|
7215
|
-
mcpServers: { [COMPANION_LOCAL_MCP_SERVER]: params.summonServer }
|
|
7216
|
-
turnControl: params.turnControl
|
|
6136
|
+
mcpServers: { [COMPANION_LOCAL_MCP_SERVER]: params.summonServer }
|
|
7217
6137
|
}
|
|
7218
6138
|
};
|
|
7219
6139
|
}
|
|
@@ -7230,23 +6150,18 @@ function dirFor(workspaceId) {
|
|
|
7230
6150
|
function conversationDir(workspaceId, conversationId) {
|
|
7231
6151
|
return join9(dirFor(workspaceId), encodeURIComponent(conversationId));
|
|
7232
6152
|
}
|
|
7233
|
-
function pathFor3(workspaceId, conversationId, agentId
|
|
7234
|
-
|
|
7235
|
-
return join9(
|
|
7236
|
-
conversationDir(workspaceId, conversationId),
|
|
7237
|
-
`${encodeURIComponent(agentId)}${suffix}.json`
|
|
7238
|
-
);
|
|
6153
|
+
function pathFor3(workspaceId, conversationId, agentId) {
|
|
6154
|
+
return join9(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
|
|
7239
6155
|
}
|
|
7240
|
-
function readPrepared(workspaceId, conversationId, agentId
|
|
7241
|
-
const path3 = pathFor3(workspaceId, conversationId, agentId
|
|
6156
|
+
function readPrepared(workspaceId, conversationId, agentId) {
|
|
6157
|
+
const path3 = pathFor3(workspaceId, conversationId, agentId);
|
|
7242
6158
|
if (!existsSync7(path3)) return null;
|
|
7243
6159
|
try {
|
|
7244
6160
|
const parsed = JSON.parse(readFileSync6(path3, "utf8"));
|
|
7245
6161
|
if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
|
|
7246
6162
|
return {
|
|
7247
6163
|
cwd: parsed.cwd,
|
|
7248
|
-
...parsed.env && typeof parsed.env === "object" ? { env: parsed.env } : {}
|
|
7249
|
-
...parsed.nativeWorkAssignment && typeof parsed.nativeWorkAssignment === "object" ? { nativeWorkAssignment: parsed.nativeWorkAssignment } : {}
|
|
6164
|
+
...parsed.env && typeof parsed.env === "object" ? { env: parsed.env } : {}
|
|
7250
6165
|
};
|
|
7251
6166
|
}
|
|
7252
6167
|
return null;
|
|
@@ -7254,10 +6169,10 @@ function readPrepared(workspaceId, conversationId, agentId, assignmentKey) {
|
|
|
7254
6169
|
return null;
|
|
7255
6170
|
}
|
|
7256
6171
|
}
|
|
7257
|
-
function writePrepared(workspaceId, conversationId, agentId, result
|
|
6172
|
+
function writePrepared(workspaceId, conversationId, agentId, result) {
|
|
7258
6173
|
mkdirSync7(conversationDir(workspaceId, conversationId), { recursive: true });
|
|
7259
6174
|
writeFileSync6(
|
|
7260
|
-
pathFor3(workspaceId, conversationId, agentId
|
|
6175
|
+
pathFor3(workspaceId, conversationId, agentId),
|
|
7261
6176
|
JSON.stringify(result) + "\n",
|
|
7262
6177
|
"utf8"
|
|
7263
6178
|
);
|
|
@@ -7266,30 +6181,30 @@ function writePrepared(workspaceId, conversationId, agentId, result, assignmentK
|
|
|
7266
6181
|
// src/secrets.ts
|
|
7267
6182
|
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
7268
6183
|
import { join as join10 } from "path";
|
|
7269
|
-
import { z as
|
|
6184
|
+
import { z as z13 } from "zod";
|
|
7270
6185
|
var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
7271
6186
|
function secretsPath() {
|
|
7272
6187
|
return join10(cabaneDir(), "secrets.json");
|
|
7273
6188
|
}
|
|
7274
|
-
var secretStoreSchema =
|
|
6189
|
+
var secretStoreSchema = z13.record(z13.string(), z13.string());
|
|
7275
6190
|
function loadSecretStore() {
|
|
7276
6191
|
const path3 = secretsPath();
|
|
7277
6192
|
if (!existsSync8(path3)) return makeStore({});
|
|
7278
6193
|
let raw;
|
|
7279
6194
|
try {
|
|
7280
6195
|
raw = readFileSync7(path3, "utf8");
|
|
7281
|
-
} catch (
|
|
6196
|
+
} catch (err) {
|
|
7282
6197
|
throw new ConfigError(
|
|
7283
|
-
`couldn't read ${path3}: ${
|
|
6198
|
+
`couldn't read ${path3}: ${err instanceof Error ? err.message : String(err)}`
|
|
7284
6199
|
);
|
|
7285
6200
|
}
|
|
7286
6201
|
if (raw.trim().length === 0) return makeStore({});
|
|
7287
6202
|
let parsed;
|
|
7288
6203
|
try {
|
|
7289
6204
|
parsed = JSON.parse(raw);
|
|
7290
|
-
} catch (
|
|
6205
|
+
} catch (err) {
|
|
7291
6206
|
throw new ConfigError(
|
|
7292
|
-
`${path3} is not valid JSON: ${
|
|
6207
|
+
`${path3} is not valid JSON: ${err instanceof Error ? err.message : String(err)}. It must be a flat object of "NAME": "value" secret pairs.`
|
|
7293
6208
|
);
|
|
7294
6209
|
}
|
|
7295
6210
|
const result = secretStoreSchema.safeParse(parsed);
|
|
@@ -7303,8 +6218,8 @@ function loadSecretStore() {
|
|
|
7303
6218
|
function loadSecretStoreTolerant(onWarn) {
|
|
7304
6219
|
try {
|
|
7305
6220
|
return loadSecretStore();
|
|
7306
|
-
} catch (
|
|
7307
|
-
onWarn?.(
|
|
6221
|
+
} catch (err) {
|
|
6222
|
+
onWarn?.(err instanceof Error ? err.message : String(err));
|
|
7308
6223
|
return makeStore({});
|
|
7309
6224
|
}
|
|
7310
6225
|
}
|
|
@@ -7375,8 +6290,8 @@ var TranscriptWriter = class {
|
|
|
7375
6290
|
} catch {
|
|
7376
6291
|
}
|
|
7377
6292
|
pruneOld(dir2, RETAIN);
|
|
7378
|
-
} catch (
|
|
7379
|
-
this.fail(
|
|
6293
|
+
} catch (err) {
|
|
6294
|
+
this.fail(err);
|
|
7380
6295
|
}
|
|
7381
6296
|
this.line({ type: "_meta", ...meta });
|
|
7382
6297
|
}
|
|
@@ -7392,14 +6307,14 @@ var TranscriptWriter = class {
|
|
|
7392
6307
|
if (this.broken) return;
|
|
7393
6308
|
try {
|
|
7394
6309
|
appendFileSync(this.path, JSON.stringify(obj) + "\n", { mode: 384 });
|
|
7395
|
-
} catch (
|
|
7396
|
-
this.fail(
|
|
6310
|
+
} catch (err) {
|
|
6311
|
+
this.fail(err);
|
|
7397
6312
|
}
|
|
7398
6313
|
}
|
|
7399
|
-
fail(
|
|
6314
|
+
fail(err) {
|
|
7400
6315
|
if (this.broken) return;
|
|
7401
6316
|
this.broken = true;
|
|
7402
|
-
this.onWarn?.(`transcript write failed (${
|
|
6317
|
+
this.onWarn?.(`transcript write failed (${err instanceof Error ? err.message : String(err)})`);
|
|
7403
6318
|
}
|
|
7404
6319
|
};
|
|
7405
6320
|
function fileName(meta) {
|
|
@@ -7437,9 +6352,9 @@ var EMPTY_FINAL_BODY = "Done \u2014 see the changes above.";
|
|
|
7437
6352
|
var TurnCommitter = class {
|
|
7438
6353
|
constructor(deps) {
|
|
7439
6354
|
this.deps = deps;
|
|
7440
|
-
this.onError = (
|
|
6355
|
+
this.onError = (err, hook) => {
|
|
7441
6356
|
deps.log.warn(
|
|
7442
|
-
{ err:
|
|
6357
|
+
{ err: err instanceof Error ? err.message : String(err), hook },
|
|
7443
6358
|
"dispatcher: transcript callback failed"
|
|
7444
6359
|
);
|
|
7445
6360
|
};
|
|
@@ -7504,9 +6419,9 @@ var TurnCommitter = class {
|
|
|
7504
6419
|
signal: deps.signal,
|
|
7505
6420
|
nextSeq: deps.nextSeq,
|
|
7506
6421
|
emptyFinalBody: EMPTY_FINAL_BODY,
|
|
7507
|
-
onError: (
|
|
6422
|
+
onError: (err) => {
|
|
7508
6423
|
deps.log.warn(
|
|
7509
|
-
{ err:
|
|
6424
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
7510
6425
|
"dispatcher: empty-final commit failed"
|
|
7511
6426
|
);
|
|
7512
6427
|
}
|
|
@@ -7529,8 +6444,8 @@ var TurnCommitter = class {
|
|
|
7529
6444
|
if (event.type === "session" || event.type === "result") return;
|
|
7530
6445
|
try {
|
|
7531
6446
|
await this.emit(event);
|
|
7532
|
-
} catch (
|
|
7533
|
-
this.onError(
|
|
6447
|
+
} catch (err) {
|
|
6448
|
+
this.onError(err, event.type);
|
|
7534
6449
|
}
|
|
7535
6450
|
}
|
|
7536
6451
|
// End-of-turn empty-final promotion. The held-text flush is now the adapter's
|
|
@@ -7726,10 +6641,27 @@ var UNEXPECTED_ROLE_REASON = `refused the wake trigger (role "system" isn't disp
|
|
|
7726
6641
|
var SKIPPED_MARKER_BODY = "(skipped)";
|
|
7727
6642
|
var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
|
|
7728
6643
|
var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
|
|
7729
|
-
var DEFAULT_AGENT_TOTAL_TIMEOUT_MS =
|
|
6644
|
+
var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
|
|
7730
6645
|
function runKey(conversationId, agentId) {
|
|
7731
6646
|
return `${conversationId}|${agentId}`;
|
|
7732
6647
|
}
|
|
6648
|
+
function describeSubAgentError(status, body) {
|
|
6649
|
+
const code = body && typeof body === "object" && "error" in body ? String(body.error) : void 0;
|
|
6650
|
+
switch (code) {
|
|
6651
|
+
case "callout_cap_exceeded":
|
|
6652
|
+
return "sub_agent: you already have the maximum open sub-agents for this thread. Wait for some to return \u2014 you're woken once they're all back \u2014 before spawning more.";
|
|
6653
|
+
case "callout_depth_exceeded":
|
|
6654
|
+
return "sub_agent: this would nest sub-agents too deep (max 3 levels). Have the current worker report back rather than spawning another layer.";
|
|
6655
|
+
case "dispatch_agent_not_found":
|
|
6656
|
+
return "sub_agent: no live agent in this workspace matches that `agentId`. Check `list_agents`, or omit `agentId` to spawn yourself.";
|
|
6657
|
+
case "dispatch_return_requires_turn":
|
|
6658
|
+
case "dispatch_return_requires_agent":
|
|
6659
|
+
case "dispatch_return_requires_dispatch":
|
|
6660
|
+
return `sub_agent: the spawn was rejected (${code}). This is a turn-context problem, not something to retry blindly \u2014 report it rather than looping.`;
|
|
6661
|
+
default:
|
|
6662
|
+
return `sub_agent: the spawn failed (${code ?? `HTTP ${status}`}).`;
|
|
6663
|
+
}
|
|
6664
|
+
}
|
|
7733
6665
|
var Dispatcher = class {
|
|
7734
6666
|
constructor(opts) {
|
|
7735
6667
|
this.opts = opts;
|
|
@@ -7756,7 +6688,7 @@ var Dispatcher = class {
|
|
|
7756
6688
|
// clear it — the SJ383 `finally` after the SDK loop is the one clear, and
|
|
7757
6689
|
// every pre-run exit returns before reaching it. So each pre-run failure has
|
|
7758
6690
|
// to clear `active_run_started_at` itself, mirroring that `finally`, or the
|
|
7759
|
-
// indicator strands until the
|
|
6691
|
+
// indicator strands until the 12h age sweep.
|
|
7760
6692
|
//
|
|
7761
6693
|
// `errorReason` controls the server's duplicate-notice rule (the active-run
|
|
7762
6694
|
// PATCH handler in conversations.ts): a clear carrying `errorReason` makes the
|
|
@@ -7776,9 +6708,9 @@ var Dispatcher = class {
|
|
|
7776
6708
|
payload.agentId,
|
|
7777
6709
|
body
|
|
7778
6710
|
);
|
|
7779
|
-
} catch (
|
|
6711
|
+
} catch (err) {
|
|
7780
6712
|
turnLog.warn(
|
|
7781
|
-
{ err:
|
|
6713
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
7782
6714
|
"dispatcher: pre-run active-run clear failed terminally; server age-sweep is the backstop"
|
|
7783
6715
|
);
|
|
7784
6716
|
}
|
|
@@ -7804,16 +6736,16 @@ var Dispatcher = class {
|
|
|
7804
6736
|
payload.messageId,
|
|
7805
6737
|
turnId
|
|
7806
6738
|
);
|
|
7807
|
-
} catch (
|
|
7808
|
-
const status =
|
|
6739
|
+
} catch (err) {
|
|
6740
|
+
const status = err instanceof ApiError ? err.status : 0;
|
|
7809
6741
|
if (status === 404) {
|
|
7810
6742
|
turnLog.warn(
|
|
7811
|
-
{ err:
|
|
6743
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
7812
6744
|
"dispatcher: turn-context 404 (stale event \u2014 conversation/message/participant gone); skipping"
|
|
7813
6745
|
);
|
|
7814
6746
|
return this.concludeBeforeRun(payload, turnLog, startedAt, "turn_context_not_found");
|
|
7815
6747
|
}
|
|
7816
|
-
const reason =
|
|
6748
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
7817
6749
|
turnLog.error({ err: reason }, "dispatcher: failed to fetch turn context");
|
|
7818
6750
|
const fetchReason = `fetch_failed: ${reason}`;
|
|
7819
6751
|
return this.concludeBeforeRun(payload, turnLog, startedAt, fetchReason, fetchReason);
|
|
@@ -7876,20 +6808,11 @@ var Dispatcher = class {
|
|
|
7876
6808
|
effectiveCwd = void 0;
|
|
7877
6809
|
}
|
|
7878
6810
|
let hookEnv;
|
|
7879
|
-
let preparedNativeAssignment;
|
|
7880
6811
|
if (prepareHook) {
|
|
7881
|
-
const
|
|
7882
|
-
const assignmentKey = assignment ? `${assignment.executionId}:${assignment.activationEpoch}` : void 0;
|
|
7883
|
-
const cached2 = readPrepared(
|
|
7884
|
-
workspaceId,
|
|
7885
|
-
payload.conversationId,
|
|
7886
|
-
payload.agentId,
|
|
7887
|
-
assignmentKey
|
|
7888
|
-
);
|
|
6812
|
+
const cached2 = readPrepared(workspaceId, payload.conversationId, payload.agentId);
|
|
7889
6813
|
if (cached2) {
|
|
7890
6814
|
effectiveCwd = cached2.cwd;
|
|
7891
6815
|
hookEnv = cached2.env;
|
|
7892
|
-
preparedNativeAssignment = cached2.nativeWorkAssignment;
|
|
7893
6816
|
} else {
|
|
7894
6817
|
const delayMs = this.opts.preparingRowDelayMs ?? DEFAULT_PREPARING_ROW_DELAY_MS;
|
|
7895
6818
|
let preparingStarted = false;
|
|
@@ -7904,9 +6827,9 @@ var Dispatcher = class {
|
|
|
7904
6827
|
summary: "",
|
|
7905
6828
|
phase,
|
|
7906
6829
|
seq
|
|
7907
|
-
}).catch((
|
|
6830
|
+
}).catch((err) => {
|
|
7908
6831
|
turnLog.warn(
|
|
7909
|
-
{ err:
|
|
6832
|
+
{ err: err instanceof Error ? err.message : String(err), phase },
|
|
7910
6833
|
"dispatcher: preparing-activity report failed (continuing with the hook)"
|
|
7911
6834
|
);
|
|
7912
6835
|
});
|
|
@@ -7928,25 +6851,17 @@ var Dispatcher = class {
|
|
|
7928
6851
|
// tasker prepare hook keys its per-task env off. Defaults to `[]` for
|
|
7929
6852
|
// an older API. The conversation anchor is gone (CT319).
|
|
7930
6853
|
triggerEntryPaths: turnContext.conversation.triggerEntryPaths ?? [],
|
|
7931
|
-
...turnContext.conversation.nativeWorkAssignment ? { nativeWorkAssignment: turnContext.conversation.nativeWorkAssignment } : {},
|
|
7932
6854
|
title: turnContext.conversation.title
|
|
7933
6855
|
});
|
|
7934
6856
|
clearTimeout(preparingTimer);
|
|
7935
6857
|
if (preparingStarted) reportPreparing("done");
|
|
7936
|
-
writePrepared(
|
|
7937
|
-
workspaceId,
|
|
7938
|
-
payload.conversationId,
|
|
7939
|
-
payload.agentId,
|
|
7940
|
-
result,
|
|
7941
|
-
assignmentKey
|
|
7942
|
-
);
|
|
6858
|
+
writePrepared(workspaceId, payload.conversationId, payload.agentId, result);
|
|
7943
6859
|
effectiveCwd = result.cwd;
|
|
7944
6860
|
hookEnv = result.env;
|
|
7945
|
-
|
|
7946
|
-
} catch (err2) {
|
|
6861
|
+
} catch (err) {
|
|
7947
6862
|
clearTimeout(preparingTimer);
|
|
7948
6863
|
if (preparingStarted) reportPreparing("error");
|
|
7949
|
-
const reason =
|
|
6864
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
7950
6865
|
turnLog.error({ err: reason }, "dispatcher: prepare hook failed");
|
|
7951
6866
|
try {
|
|
7952
6867
|
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
@@ -7969,6 +6884,19 @@ ${reason}`,
|
|
|
7969
6884
|
}
|
|
7970
6885
|
}
|
|
7971
6886
|
}
|
|
6887
|
+
let turnEnv = hookEnv;
|
|
6888
|
+
if (effectiveCwd && turnContext.runtime === "codex") {
|
|
6889
|
+
const tmpDir = join12(effectiveCwd, "node_modules", ".cache", "cabane-tmp", turnId);
|
|
6890
|
+
try {
|
|
6891
|
+
mkdirSync9(tmpDir, { recursive: true });
|
|
6892
|
+
turnEnv = { ...hookEnv, TMPDIR: tmpDir };
|
|
6893
|
+
} catch (err) {
|
|
6894
|
+
turnLog.warn(
|
|
6895
|
+
{ err: err instanceof Error ? err.message : String(err), tmpDir },
|
|
6896
|
+
"dispatcher: failed to create per-turn TMPDIR \u2014 proceeding with the inherited temp dir"
|
|
6897
|
+
);
|
|
6898
|
+
}
|
|
6899
|
+
}
|
|
7972
6900
|
const key = runKey(payload.conversationId, payload.agentId);
|
|
7973
6901
|
const abortController = new AbortController();
|
|
7974
6902
|
this.aborts.set(key, abortController);
|
|
@@ -7983,9 +6911,9 @@ ${reason}`,
|
|
|
7983
6911
|
// this live turn for an abandoned one and close it with a `stopped`.
|
|
7984
6912
|
turnId
|
|
7985
6913
|
});
|
|
7986
|
-
} catch (
|
|
6914
|
+
} catch (err) {
|
|
7987
6915
|
turnLog.warn(
|
|
7988
|
-
{ err:
|
|
6916
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
7989
6917
|
"dispatcher: active-run flag set failed terminally; proceeding"
|
|
7990
6918
|
);
|
|
7991
6919
|
}
|
|
@@ -8022,35 +6950,6 @@ ${reason}`,
|
|
|
8022
6950
|
subAgentCreate,
|
|
8023
6951
|
wakeState
|
|
8024
6952
|
);
|
|
8025
|
-
const nativeTurnControl = {
|
|
8026
|
-
summon: (agentId) => {
|
|
8027
|
-
summonState.agentId = agentId;
|
|
8028
|
-
},
|
|
8029
|
-
skip: (reason) => {
|
|
8030
|
-
skipState.skipped = true;
|
|
8031
|
-
skipState.reason = reason;
|
|
8032
|
-
},
|
|
8033
|
-
ask: (p) => {
|
|
8034
|
-
askState.targetUserId = p.targetUserId;
|
|
8035
|
-
if (p.questions && p.questions.length > 0) {
|
|
8036
|
-
askState.questions = p.questions;
|
|
8037
|
-
askState.question = null;
|
|
8038
|
-
askState.headline = null;
|
|
8039
|
-
askState.options = null;
|
|
8040
|
-
} else {
|
|
8041
|
-
askState.question = p.question ?? null;
|
|
8042
|
-
askState.headline = p.headline ?? null;
|
|
8043
|
-
askState.options = p.options ?? null;
|
|
8044
|
-
askState.questions = null;
|
|
8045
|
-
}
|
|
8046
|
-
},
|
|
8047
|
-
wake: (p) => {
|
|
8048
|
-
wakeState.afterSeconds = p.afterSeconds ?? null;
|
|
8049
|
-
wakeState.at = p.at ?? null;
|
|
8050
|
-
wakeState.note = p.note;
|
|
8051
|
-
},
|
|
8052
|
-
subAgent: subAgentCreate
|
|
8053
|
-
};
|
|
8054
6953
|
const request = buildCompanionTurnRequest({
|
|
8055
6954
|
turnContext,
|
|
8056
6955
|
baseUrl: this.opts.baseUrl,
|
|
@@ -8060,11 +6959,11 @@ ${reason}`,
|
|
|
8060
6959
|
...turnContext.turnToken ? { turnToken: turnContext.turnToken } : {},
|
|
8061
6960
|
// SJ524: the hook-resolved cwd overrides the static local cwd.
|
|
8062
6961
|
...effectiveCwd ? { cwd: effectiveCwd } : {},
|
|
8063
|
-
|
|
8064
|
-
|
|
6962
|
+
// CT804: `turnEnv` = the prepare-hook env plus the per-turn checkout-local
|
|
6963
|
+
// TMPDIR (falls back to `hookEnv` when no cwd was resolved).
|
|
6964
|
+
...turnEnv ? { env: turnEnv } : {},
|
|
8065
6965
|
mcpServers: resolvedMcpServers,
|
|
8066
6966
|
summonServer,
|
|
8067
|
-
turnControl: nativeTurnControl,
|
|
8068
6967
|
// CT238: this turn's conversation, forwarded as the active-conversation
|
|
8069
6968
|
// header so a cross-thread post/spawn stamps its origin.
|
|
8070
6969
|
activeConversationId: payload.conversationId,
|
|
@@ -8082,22 +6981,19 @@ ${reason}`,
|
|
|
8082
6981
|
if (this.opts.codexEnabled) {
|
|
8083
6982
|
adapters.push(createCodexAdapter({ enabled: true, onWarn }));
|
|
8084
6983
|
}
|
|
8085
|
-
if (this.opts.cabaneNativeApiKey) {
|
|
8086
|
-
adapters.push(createCabaneNativeAdapter({ apiKey: this.opts.cabaneNativeApiKey, onWarn }));
|
|
8087
|
-
}
|
|
8088
6984
|
const registry = createAdapterRegistry(adapters);
|
|
8089
6985
|
let adapter;
|
|
8090
6986
|
try {
|
|
8091
6987
|
adapter = selectAdapter(registry, turnContext.runtime);
|
|
8092
|
-
} catch (
|
|
8093
|
-
if (!(
|
|
6988
|
+
} catch (err) {
|
|
6989
|
+
if (!(err instanceof RuntimeUnavailableError)) throw err;
|
|
8094
6990
|
turnLog.error(
|
|
8095
|
-
{ runtime:
|
|
6991
|
+
{ runtime: err.runtime, available: err.available },
|
|
8096
6992
|
"dispatcher: turn runtime not available on this device"
|
|
8097
6993
|
);
|
|
8098
6994
|
try {
|
|
8099
6995
|
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
8100
|
-
body: `${RUNTIME_UNAVAILABLE_PREFIX} ${
|
|
6996
|
+
body: `${RUNTIME_UNAVAILABLE_PREFIX} ${err.message}`,
|
|
8101
6997
|
kind: "final",
|
|
8102
6998
|
turnId,
|
|
8103
6999
|
parentMessageId: payload.messageId
|
|
@@ -8112,7 +7008,7 @@ ${reason}`,
|
|
|
8112
7008
|
payload,
|
|
8113
7009
|
turnLog,
|
|
8114
7010
|
startedAt,
|
|
8115
|
-
`runtime_unavailable:${
|
|
7011
|
+
`runtime_unavailable:${err.runtime}`
|
|
8116
7012
|
);
|
|
8117
7013
|
}
|
|
8118
7014
|
if (prepareHook && hookEnv?.CABANE_TASK_ID) {
|
|
@@ -8246,9 +7142,9 @@ ${reason}`,
|
|
|
8246
7142
|
skipState.skipped = true;
|
|
8247
7143
|
skipState.reason = intent.skipReason;
|
|
8248
7144
|
}
|
|
8249
|
-
} catch (
|
|
7145
|
+
} catch (err) {
|
|
8250
7146
|
turnLog.warn(
|
|
8251
|
-
{ err:
|
|
7147
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
8252
7148
|
"dispatcher: turn-control intent fetch failed; turn-control effects for this turn are dropped"
|
|
8253
7149
|
);
|
|
8254
7150
|
}
|
|
@@ -8294,9 +7190,9 @@ ${reason}`,
|
|
|
8294
7190
|
payload.agentId,
|
|
8295
7191
|
{ agentSessionId: event.state }
|
|
8296
7192
|
);
|
|
8297
|
-
} catch (
|
|
7193
|
+
} catch (err) {
|
|
8298
7194
|
turnLog.warn(
|
|
8299
|
-
{ err:
|
|
7195
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
8300
7196
|
"dispatcher: session-id write failed (will retry next turn)"
|
|
8301
7197
|
);
|
|
8302
7198
|
}
|
|
@@ -8345,18 +7241,18 @@ ${reason}`,
|
|
|
8345
7241
|
parentMessageId: payload.messageId,
|
|
8346
7242
|
...skipWake ? { wake: skipWake } : {}
|
|
8347
7243
|
});
|
|
8348
|
-
} catch (
|
|
7244
|
+
} catch (err) {
|
|
8349
7245
|
turnLog.warn(
|
|
8350
|
-
{ err:
|
|
7246
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
8351
7247
|
"dispatcher: skipped-marker commit failed"
|
|
8352
7248
|
);
|
|
8353
7249
|
}
|
|
8354
7250
|
} else {
|
|
8355
7251
|
await committer.finalize(okResult);
|
|
8356
7252
|
}
|
|
8357
|
-
} catch (
|
|
7253
|
+
} catch (err) {
|
|
8358
7254
|
okResult = false;
|
|
8359
|
-
resultReason =
|
|
7255
|
+
resultReason = err instanceof Error ? err.message : String(err);
|
|
8360
7256
|
turnLog.error({ err: resultReason }, "dispatcher: SDK query threw");
|
|
8361
7257
|
} finally {
|
|
8362
7258
|
if (idleTimer) clearTimeout(idleTimer);
|
|
@@ -8392,9 +7288,9 @@ ${reason}`,
|
|
|
8392
7288
|
// CT113: the stopped marker is still "about" the triggering message.
|
|
8393
7289
|
parentMessageId: payload.messageId
|
|
8394
7290
|
});
|
|
8395
|
-
} catch (
|
|
7291
|
+
} catch (err) {
|
|
8396
7292
|
turnLog.warn(
|
|
8397
|
-
{ err:
|
|
7293
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
8398
7294
|
"dispatcher: stopped-marker commit failed"
|
|
8399
7295
|
);
|
|
8400
7296
|
}
|
|
@@ -8435,9 +7331,9 @@ ${reason}`,
|
|
|
8435
7331
|
payload.agentId,
|
|
8436
7332
|
body
|
|
8437
7333
|
);
|
|
8438
|
-
} catch (
|
|
7334
|
+
} catch (err) {
|
|
8439
7335
|
turnLog.warn(
|
|
8440
|
-
{ err:
|
|
7336
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
8441
7337
|
"dispatcher: active-run clear failed terminally; server age-sweep is the backstop"
|
|
8442
7338
|
);
|
|
8443
7339
|
}
|
|
@@ -8505,7 +7401,6 @@ function buildCompanionManifest(opts) {
|
|
|
8505
7401
|
if (opts.claudeCode) runtimes.push({ name: "claude-code", version: v.claudeCode ?? null });
|
|
8506
7402
|
if (opts.opencode) runtimes.push({ name: "opencode", version: v.opencode ?? null });
|
|
8507
7403
|
if (opts.codex) runtimes.push({ name: "codex", version: v.codex ?? null });
|
|
8508
|
-
if (opts.cabaneNative) runtimes.push({ name: "cabane-native", version: null });
|
|
8509
7404
|
return { runtimes, capabilities: { ...DEVICE_MANIFEST.capabilities } };
|
|
8510
7405
|
}
|
|
8511
7406
|
|
|
@@ -8743,13 +7638,13 @@ var Outbox = class {
|
|
|
8743
7638
|
try {
|
|
8744
7639
|
writeFileSync7(tmp, JSON.stringify(entry) + "\n", "utf8");
|
|
8745
7640
|
renameSync3(tmp, target);
|
|
8746
|
-
} catch (
|
|
7641
|
+
} catch (err) {
|
|
8747
7642
|
try {
|
|
8748
7643
|
rmSync5(tmp, { force: true });
|
|
8749
7644
|
} catch {
|
|
8750
7645
|
}
|
|
8751
7646
|
this.log?.warn(
|
|
8752
|
-
{ workspaceId: this.workspaceId, err:
|
|
7647
|
+
{ workspaceId: this.workspaceId, err: err instanceof Error ? err.message : String(err) },
|
|
8753
7648
|
"companion outbox: failed to persist entry"
|
|
8754
7649
|
);
|
|
8755
7650
|
return;
|
|
@@ -8846,40 +7741,43 @@ var Outbox = class {
|
|
|
8846
7741
|
};
|
|
8847
7742
|
|
|
8848
7743
|
// src/run-config.ts
|
|
8849
|
-
import { z as
|
|
8850
|
-
var mcpStdioServerSchema =
|
|
8851
|
-
type:
|
|
8852
|
-
command:
|
|
8853
|
-
args:
|
|
8854
|
-
env:
|
|
7744
|
+
import { z as z14 } from "zod";
|
|
7745
|
+
var mcpStdioServerSchema = z14.object({
|
|
7746
|
+
type: z14.literal("stdio").optional(),
|
|
7747
|
+
command: z14.string().min(1),
|
|
7748
|
+
args: z14.array(z14.string()).optional(),
|
|
7749
|
+
env: z14.record(z14.string(), z14.string()).optional()
|
|
8855
7750
|
});
|
|
8856
|
-
var mcpHttpServerSchema =
|
|
8857
|
-
type:
|
|
8858
|
-
url:
|
|
8859
|
-
headers:
|
|
7751
|
+
var mcpHttpServerSchema = z14.object({
|
|
7752
|
+
type: z14.literal("http"),
|
|
7753
|
+
url: z14.string().url(),
|
|
7754
|
+
headers: z14.record(z14.string(), z14.string()).optional()
|
|
8860
7755
|
});
|
|
8861
|
-
var mcpSseServerSchema =
|
|
8862
|
-
type:
|
|
8863
|
-
url:
|
|
8864
|
-
headers:
|
|
7756
|
+
var mcpSseServerSchema = z14.object({
|
|
7757
|
+
type: z14.literal("sse"),
|
|
7758
|
+
url: z14.string().url(),
|
|
7759
|
+
headers: z14.record(z14.string(), z14.string()).optional()
|
|
8865
7760
|
});
|
|
8866
|
-
var mcpServerDefSchema =
|
|
7761
|
+
var mcpServerDefSchema = z14.union([
|
|
8867
7762
|
mcpHttpServerSchema,
|
|
8868
7763
|
mcpSseServerSchema,
|
|
8869
7764
|
mcpStdioServerSchema
|
|
8870
7765
|
]);
|
|
8871
|
-
var thinkingConfigSchema =
|
|
8872
|
-
|
|
8873
|
-
|
|
8874
|
-
|
|
7766
|
+
var thinkingConfigSchema = z14.discriminatedUnion("type", [
|
|
7767
|
+
z14.object({ type: z14.literal("adaptive") }),
|
|
7768
|
+
z14.object({ type: z14.literal("enabled"), budgetTokens: z14.number().int().positive().optional() }),
|
|
7769
|
+
z14.object({ type: z14.literal("disabled") })
|
|
8875
7770
|
]);
|
|
8876
|
-
var effortSchema =
|
|
8877
|
-
var runConfigSchema =
|
|
8878
|
-
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
|
|
7771
|
+
var effortSchema = z14.enum(["low", "medium", "high", "xhigh", "max"]);
|
|
7772
|
+
var runConfigSchema = z14.object({
|
|
7773
|
+
// CT788: the host-access binary replaced the `assistant`/`coding`/`custom` mode
|
|
7774
|
+
// trio + its custom tool lists — `true` grants the host filesystem/shell, absent
|
|
7775
|
+
// is the locked surface. Kept in lockstep with `@cabane/shared`'s
|
|
7776
|
+
// `agentRunConfigSchema` (independent zod, forward-compatible: unknown keys are
|
|
7777
|
+
// stripped, so an older companion riding a newer server never rejects the config).
|
|
7778
|
+
hostAccess: z14.boolean().optional(),
|
|
7779
|
+
mcpServers: z14.record(z14.string(), mcpServerDefSchema).optional(),
|
|
7780
|
+
model: z14.string().min(1).optional(),
|
|
8883
7781
|
thinking: thinkingConfigSchema.optional(),
|
|
8884
7782
|
effort: effortSchema.optional()
|
|
8885
7783
|
});
|
|
@@ -8924,20 +7822,20 @@ var SseSubscriber = class {
|
|
|
8924
7822
|
try {
|
|
8925
7823
|
await this.connect();
|
|
8926
7824
|
backoff = 500;
|
|
8927
|
-
} catch (
|
|
7825
|
+
} catch (err) {
|
|
8928
7826
|
if (this.aborted) return;
|
|
8929
|
-
if (
|
|
7827
|
+
if (err instanceof ApiError && (err.status === 401 || err.status === 403)) {
|
|
8930
7828
|
this.opts.log.error(
|
|
8931
|
-
{ workspaceId: this.opts.workspaceId, status:
|
|
7829
|
+
{ workspaceId: this.opts.workspaceId, status: err.status },
|
|
8932
7830
|
"SSE auth failed \u2014 tearing down this workspace subscriber"
|
|
8933
7831
|
);
|
|
8934
|
-
this.opts.onAuthFailure(
|
|
7832
|
+
this.opts.onAuthFailure(err.status);
|
|
8935
7833
|
return;
|
|
8936
7834
|
}
|
|
8937
7835
|
this.opts.log.warn(
|
|
8938
7836
|
{
|
|
8939
7837
|
workspaceId: this.opts.workspaceId,
|
|
8940
|
-
err:
|
|
7838
|
+
err: err instanceof Error ? err.message : String(err),
|
|
8941
7839
|
backoff
|
|
8942
7840
|
},
|
|
8943
7841
|
"SSE disconnected; reconnecting"
|
|
@@ -9080,7 +7978,7 @@ var CompanionSupervisor = class {
|
|
|
9080
7978
|
if (!this.config.deviceToken) {
|
|
9081
7979
|
this.log.warn("companion: not paired (no device token) \u2014 run `cabane-companion pair`");
|
|
9082
7980
|
process.stdout.write(
|
|
9083
|
-
"companion: this device is not paired \u2014 run `cabane-companion pair
|
|
7981
|
+
"companion: this device is not paired \u2014 run `cabane-companion pair`, then confirm the short code in Settings \u2192 Connectors.\n"
|
|
9084
7982
|
);
|
|
9085
7983
|
return;
|
|
9086
7984
|
}
|
|
@@ -9135,9 +8033,6 @@ var CompanionSupervisor = class {
|
|
|
9135
8033
|
// like opencode — the CLI's presence is the operator's responsibility;
|
|
9136
8034
|
// a misconfigured device fails the turn loudly, never silently).
|
|
9137
8035
|
codex: isCodexEnabled(this.config),
|
|
9138
|
-
// CT598: advertise the native runtime when an OpenRouter key is set. Key
|
|
9139
|
-
// absent → not advertised, so a native turn never routes here.
|
|
9140
|
-
cabaneNative: isCabaneNativeEnabled(),
|
|
9141
8036
|
// CT571/CT586: each runtime's `version` from the latest harness probe
|
|
9142
8037
|
// (fail-soft to null). Informational only — the server matches on name.
|
|
9143
8038
|
versions: this.harnessVersions
|
|
@@ -9155,9 +8050,9 @@ var CompanionSupervisor = class {
|
|
|
9155
8050
|
this.hub.setDevice({ deviceId: res.deviceId });
|
|
9156
8051
|
this.deviceId = res.deviceId;
|
|
9157
8052
|
this.checkVersionSkew(res.serverVersion);
|
|
9158
|
-
} catch (
|
|
8053
|
+
} catch (err) {
|
|
9159
8054
|
this.log.warn(
|
|
9160
|
-
{ err:
|
|
8055
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
9161
8056
|
"companion: device heartbeat failed (will retry on next tick)"
|
|
9162
8057
|
);
|
|
9163
8058
|
}
|
|
@@ -9193,12 +8088,12 @@ var CompanionSupervisor = class {
|
|
|
9193
8088
|
const resp = await this.deviceApi.getAssignments();
|
|
9194
8089
|
items = resp.assignments;
|
|
9195
8090
|
device = resp.device;
|
|
9196
|
-
} catch (
|
|
8091
|
+
} catch (err) {
|
|
9197
8092
|
this.log.error(
|
|
9198
|
-
{ err:
|
|
8093
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
9199
8094
|
"companion: assignments pull failed \u2014 check the device is still active in the cabane app"
|
|
9200
8095
|
);
|
|
9201
|
-
this.hub.setDeviceError(
|
|
8096
|
+
this.hub.setDeviceError(err instanceof Error ? err.message : String(err));
|
|
9202
8097
|
return;
|
|
9203
8098
|
}
|
|
9204
8099
|
this.hub.setDevice({ deviceId: device.id, deviceLabel: device.label });
|
|
@@ -9275,7 +8170,7 @@ var CompanionSupervisor = class {
|
|
|
9275
8170
|
agentId: it.agentId,
|
|
9276
8171
|
username: it.agentUsername,
|
|
9277
8172
|
displayName: it.agentDisplayName,
|
|
9278
|
-
mode: runConfig.
|
|
8173
|
+
mode: runConfig.hostAccess ? "full" : "none",
|
|
9279
8174
|
hasCredential: false,
|
|
9280
8175
|
missingSecrets: missing
|
|
9281
8176
|
});
|
|
@@ -9295,7 +8190,7 @@ var CompanionSupervisor = class {
|
|
|
9295
8190
|
agentId: it.agentId,
|
|
9296
8191
|
username: it.agentUsername,
|
|
9297
8192
|
displayName: it.agentDisplayName,
|
|
9298
|
-
mode: runConfig.
|
|
8193
|
+
mode: runConfig.hostAccess ? "full" : "none",
|
|
9299
8194
|
hasCredential: true,
|
|
9300
8195
|
missingSecrets: missing
|
|
9301
8196
|
});
|
|
@@ -9379,12 +8274,9 @@ var CompanionSupervisor = class {
|
|
|
9379
8274
|
// CT481: register the codex adapter when this device offers codex; unset
|
|
9380
8275
|
// leaves an `openai/…` turn to fail loudly (no silent claude-code fallback).
|
|
9381
8276
|
...isCodexEnabled(this.config) ? { codexEnabled: true } : {},
|
|
9382
|
-
// CT598: register the cabane-native adapter when an OpenRouter key is set;
|
|
9383
|
-
// unset leaves a `cabane-native/…` turn to fail loudly (no silent fallback).
|
|
9384
|
-
...cabaneNativeApiKey() ? { cabaneNativeApiKey: cabaneNativeApiKey() } : {},
|
|
9385
8277
|
// CT556: per-turn timeout watchdog windows, from the companion's own env
|
|
9386
8278
|
// (`AGENT_IDLE_TIMEOUT_MS` / `AGENT_TOTAL_TIMEOUT_MS`). Unset → the
|
|
9387
|
-
// dispatcher's baked-in defaults (10 min idle /
|
|
8279
|
+
// dispatcher's baked-in defaults (10 min idle / 6h total).
|
|
9388
8280
|
...positiveIntEnv("AGENT_IDLE_TIMEOUT_MS") !== void 0 ? { idleTimeoutMs: positiveIntEnv("AGENT_IDLE_TIMEOUT_MS") } : {},
|
|
9389
8281
|
...positiveIntEnv("AGENT_TOTAL_TIMEOUT_MS") !== void 0 ? { totalTimeoutMs: positiveIntEnv("AGENT_TOTAL_TIMEOUT_MS") } : {},
|
|
9390
8282
|
observer: this.hub.observerFor(ctx.workspaceId, ctx.agentId, ctx.workspaceSlug),
|
|
@@ -9451,9 +8343,9 @@ var CompanionSupervisor = class {
|
|
|
9451
8343
|
let wire;
|
|
9452
8344
|
try {
|
|
9453
8345
|
wire = JSON.parse(ev.data);
|
|
9454
|
-
} catch (
|
|
8346
|
+
} catch (err) {
|
|
9455
8347
|
this.log.warn(
|
|
9456
|
-
{ err:
|
|
8348
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
9457
8349
|
"malformed SSE payload"
|
|
9458
8350
|
);
|
|
9459
8351
|
return;
|
|
@@ -9500,13 +8392,13 @@ var CompanionSupervisor = class {
|
|
|
9500
8392
|
}
|
|
9501
8393
|
const chainKey = `${payload.conversationId}|${payload.agentId}`;
|
|
9502
8394
|
const prev = wr.chains.get(chainKey) ?? Promise.resolve();
|
|
9503
|
-
const tail = prev.then(() => this.runDispatch(wr, ev, payload, agent)).catch((
|
|
8395
|
+
const tail = prev.then(() => this.runDispatch(wr, ev, payload, agent)).catch((err) => {
|
|
9504
8396
|
this.log.warn(
|
|
9505
8397
|
{
|
|
9506
8398
|
workspaceId: wr.workspaceId,
|
|
9507
8399
|
conversationId: payload.conversationId,
|
|
9508
8400
|
agentId: payload.agentId,
|
|
9509
|
-
err:
|
|
8401
|
+
err: err instanceof Error ? err.message : String(err)
|
|
9510
8402
|
},
|
|
9511
8403
|
"companion: conversation turn handler threw"
|
|
9512
8404
|
);
|
|
@@ -9598,10 +8490,10 @@ var CompanionSupervisor = class {
|
|
|
9598
8490
|
} else {
|
|
9599
8491
|
drainDelay = DRAIN_BASE_MS;
|
|
9600
8492
|
}
|
|
9601
|
-
} catch (
|
|
8493
|
+
} catch (err) {
|
|
9602
8494
|
drainDelay = Math.min(drainDelay * 2, DRAIN_MAX_MS);
|
|
9603
8495
|
this.log.warn(
|
|
9604
|
-
{ agentId, err:
|
|
8496
|
+
{ agentId, err: err instanceof Error ? err.message : String(err) },
|
|
9605
8497
|
"companion: outbox drain pass threw (will retry with backoff)"
|
|
9606
8498
|
);
|
|
9607
8499
|
} finally {
|
|
@@ -9668,9 +8560,9 @@ var CompanionSupervisor = class {
|
|
|
9668
8560
|
codex: signals.codexVersion
|
|
9669
8561
|
};
|
|
9670
8562
|
this.hub.setHarnesses(deriveHarnessSnapshot(signals).harnesses);
|
|
9671
|
-
} catch (
|
|
8563
|
+
} catch (err) {
|
|
9672
8564
|
this.log.warn(
|
|
9673
|
-
{ err:
|
|
8565
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
9674
8566
|
"companion: harness probe failed (will retry on next beat)"
|
|
9675
8567
|
);
|
|
9676
8568
|
}
|
|
@@ -9768,10 +8660,13 @@ var CompanionSupervisor = class {
|
|
|
9768
8660
|
await Promise.race([
|
|
9769
8661
|
Promise.allSettled(turns),
|
|
9770
8662
|
new Promise((resolve) => {
|
|
9771
|
-
timer = setTimeout(
|
|
9772
|
-
|
|
9773
|
-
|
|
9774
|
-
|
|
8663
|
+
timer = setTimeout(
|
|
8664
|
+
() => {
|
|
8665
|
+
timedOut = true;
|
|
8666
|
+
resolve();
|
|
8667
|
+
},
|
|
8668
|
+
Math.max(0, graceMs)
|
|
8669
|
+
);
|
|
9775
8670
|
timer.unref?.();
|
|
9776
8671
|
})
|
|
9777
8672
|
]);
|
|
@@ -9825,17 +8720,17 @@ var RECOVERABLE_CODES = /* @__PURE__ */ new Set([
|
|
|
9825
8720
|
"ERR_STREAM_DESTROYED",
|
|
9826
8721
|
"ERR_STREAM_WRITE_AFTER_END"
|
|
9827
8722
|
]);
|
|
9828
|
-
function errorCode(
|
|
9829
|
-
if (
|
|
9830
|
-
const code =
|
|
8723
|
+
function errorCode(err) {
|
|
8724
|
+
if (err && typeof err === "object" && "code" in err) {
|
|
8725
|
+
const code = err.code;
|
|
9831
8726
|
if (typeof code === "string") return code;
|
|
9832
8727
|
}
|
|
9833
8728
|
return void 0;
|
|
9834
8729
|
}
|
|
9835
|
-
function isRecoverableSocketError(
|
|
9836
|
-
const code = errorCode(
|
|
8730
|
+
function isRecoverableSocketError(err) {
|
|
8731
|
+
const code = errorCode(err);
|
|
9837
8732
|
if (code && RECOVERABLE_CODES.has(code)) return true;
|
|
9838
|
-
const message =
|
|
8733
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
9839
8734
|
return /\bEPIPE\b|\bECONNRESET\b/.test(message);
|
|
9840
8735
|
}
|
|
9841
8736
|
function installProcessSafetyNet(log, opts = {}) {
|
|
@@ -9844,16 +8739,16 @@ function installProcessSafetyNet(log, opts = {}) {
|
|
|
9844
8739
|
stream.on("error", () => {
|
|
9845
8740
|
});
|
|
9846
8741
|
}
|
|
9847
|
-
proc.on("uncaughtException", (
|
|
8742
|
+
proc.on("uncaughtException", (err) => handleUncaught(log, err, "uncaughtException"));
|
|
9848
8743
|
proc.on(
|
|
9849
8744
|
"unhandledRejection",
|
|
9850
8745
|
(reason) => handleUncaught(log, reason, "unhandledRejection")
|
|
9851
8746
|
);
|
|
9852
8747
|
}
|
|
9853
|
-
function handleUncaught(log,
|
|
9854
|
-
const message =
|
|
9855
|
-
const code = errorCode(
|
|
9856
|
-
if (isRecoverableSocketError(
|
|
8748
|
+
function handleUncaught(log, err, origin) {
|
|
8749
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
8750
|
+
const code = errorCode(err);
|
|
8751
|
+
if (isRecoverableSocketError(err)) {
|
|
9857
8752
|
log.warn(
|
|
9858
8753
|
{ origin, code, err: message },
|
|
9859
8754
|
"companion: recovered from a socket error (kept running \u2014 a broken pipe never kills the companion)"
|
|
@@ -9861,7 +8756,7 @@ function handleUncaught(log, err2, origin) {
|
|
|
9861
8756
|
return;
|
|
9862
8757
|
}
|
|
9863
8758
|
log.error(
|
|
9864
|
-
{ origin, code, err: message, stack:
|
|
8759
|
+
{ origin, code, err: message, stack: err instanceof Error ? err.stack : void 0 },
|
|
9865
8760
|
"companion: uncaught error (kept running \u2014 see the stack above)"
|
|
9866
8761
|
);
|
|
9867
8762
|
}
|
|
@@ -9898,14 +8793,14 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
9898
8793
|
cfg = requireConfig();
|
|
9899
8794
|
claudeCode = await probeClaude();
|
|
9900
8795
|
await ensureRuntimeAvailable(cfg, { probeClaude: async () => claudeCode });
|
|
9901
|
-
} catch (
|
|
8796
|
+
} catch (err) {
|
|
9902
8797
|
recordCrash({
|
|
9903
|
-
reason:
|
|
9904
|
-
...errorCode(
|
|
8798
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
8799
|
+
...errorCode(err) ? { code: errorCode(err) } : {},
|
|
9905
8800
|
origin: "startup",
|
|
9906
8801
|
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
9907
8802
|
});
|
|
9908
|
-
throw
|
|
8803
|
+
throw err;
|
|
9909
8804
|
}
|
|
9910
8805
|
if (cfg.logLevel) log.level = cfg.logLevel;
|
|
9911
8806
|
const harnessVersions = await probeHarnessVersions({
|