@cabane/companion 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -29
- package/dist/cli.js +662 -1823
- package/dist/pairing-config.js +29 -53
- package/dist/runtime.js +490 -1585
- 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");
|
|
@@ -1011,8 +979,20 @@ async function claudeOnPath() {
|
|
|
1011
979
|
});
|
|
1012
980
|
});
|
|
1013
981
|
}
|
|
982
|
+
var CODEX_PROBE_TIMEOUT_MS = 4e3;
|
|
983
|
+
async function codexOnPath() {
|
|
984
|
+
const version = await Promise.race([
|
|
985
|
+
probeCliVersion("codex"),
|
|
986
|
+
new Promise((resolve) => {
|
|
987
|
+
const timer = setTimeout(() => resolve(null), CODEX_PROBE_TIMEOUT_MS);
|
|
988
|
+
timer.unref?.();
|
|
989
|
+
})
|
|
990
|
+
]);
|
|
991
|
+
return version !== null;
|
|
992
|
+
}
|
|
1014
993
|
async function ensureRuntimeAvailable(cfg, deps = {}) {
|
|
1015
994
|
const probeClaude = deps.probeClaude ?? claudeOnPath;
|
|
995
|
+
const probeCodex = deps.probeCodex ?? codexOnPath;
|
|
1016
996
|
const warn = deps.warn ?? ((message) => process.stderr.write(`${message}
|
|
1017
997
|
`));
|
|
1018
998
|
if (await probeClaude()) return;
|
|
@@ -1021,15 +1001,15 @@ async function ensureRuntimeAvailable(cfg, deps = {}) {
|
|
|
1021
1001
|
...isCodexEnabled(cfg) ? ["codex"] : []
|
|
1022
1002
|
];
|
|
1023
1003
|
if (alternates.length > 0) {
|
|
1004
|
+
const exposed = alternates.join(" + ");
|
|
1024
1005
|
warn(
|
|
1025
|
-
`warning: Claude Code isn\u2019t on your PATH, so this device
|
|
1026
|
-
" + "
|
|
1027
|
-
)} only \u2014 it advertises just those runtimes, so assign it matching models (a Claude-model agent won\u2019t be routed here). Install Claude Code (\`npm i -g @anthropic-ai/claude-code\`) if you want it to run Claude models too.`
|
|
1006
|
+
`warning: Claude Code isn\u2019t on your PATH, so this device exposes ${exposed} only \u2014 it advertises nothing else, so a Claude-model agent won\u2019t be routed here. Assign it agents on models ${exposed} can run, or install Claude Code (\`npm i -g @anthropic-ai/claude-code\`) and log in if you want this device to run Claude models too.`
|
|
1028
1007
|
);
|
|
1029
1008
|
return;
|
|
1030
1009
|
}
|
|
1010
|
+
const installedButUnexposed = await probeCodex() ? 'Codex is installed on this machine but not exposed \u2014 enable it with `{ "codex": { "enabled": true } }` in ~/.cabane/config.json. Otherwise, expose a harness:\n' : "Expose at least one:\n";
|
|
1031
1011
|
throw new CompanionError(
|
|
1032
|
-
"
|
|
1012
|
+
"This device exposes no harness, so no agent turn can run here. " + installedButUnexposed + ' \u2022 Claude Code \u2014 install it (`npm i -g @anthropic-ai/claude-code`) and log in (`claude`, then follow the prompts); it\u2019s exposed automatically once `claude` is on your PATH.\n \u2022 Codex \u2014 install the Codex CLI and log in (`codex login`), then add `{ "codex": { "enabled": true } }` to ~/.cabane/config.json.\n \u2022 opencode \u2014 run `opencode serve --port 4096`, then add `{ "opencode": { "serverUrl": "http://127.0.0.1:4096" } }` to ~/.cabane/config.json.\nThen run `cabane-companion start` again (see the companion README).'
|
|
1033
1013
|
);
|
|
1034
1014
|
}
|
|
1035
1015
|
|
|
@@ -1101,8 +1081,8 @@ async function verifyRuntime(state, fetchImpl = fetch) {
|
|
|
1101
1081
|
let res;
|
|
1102
1082
|
try {
|
|
1103
1083
|
res = await fetchImpl(url, { signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) });
|
|
1104
|
-
} catch (
|
|
1105
|
-
return isConnRefused(
|
|
1084
|
+
} catch (err) {
|
|
1085
|
+
return isConnRefused(err) ? "stale" : "unknown";
|
|
1106
1086
|
}
|
|
1107
1087
|
if (!res.ok) return "unknown";
|
|
1108
1088
|
let body;
|
|
@@ -1114,9 +1094,9 @@ async function verifyRuntime(state, fetchImpl = fetch) {
|
|
|
1114
1094
|
if (typeof body.instance_id !== "string") return "unknown";
|
|
1115
1095
|
return body.instance_id === state.instanceId ? "ours" : "stale";
|
|
1116
1096
|
}
|
|
1117
|
-
function isConnRefused(
|
|
1118
|
-
if (!
|
|
1119
|
-
const cause =
|
|
1097
|
+
function isConnRefused(err) {
|
|
1098
|
+
if (!err || typeof err !== "object") return false;
|
|
1099
|
+
const cause = err.cause;
|
|
1120
1100
|
return !!cause && typeof cause === "object" && cause.code === "ECONNREFUSED";
|
|
1121
1101
|
}
|
|
1122
1102
|
function trimSlash(s) {
|
|
@@ -1179,10 +1159,10 @@ var CabaneApi = class {
|
|
|
1179
1159
|
for (let attempt = 1; ; attempt++) {
|
|
1180
1160
|
try {
|
|
1181
1161
|
return await this.attempt(method, path3, body, signal);
|
|
1182
|
-
} catch (
|
|
1183
|
-
if (attempt >= maxAttempts || signal?.aborted || !isRetryable(
|
|
1162
|
+
} catch (err) {
|
|
1163
|
+
if (attempt >= maxAttempts || signal?.aborted || !isRetryable(err)) throw err;
|
|
1184
1164
|
await sleep(RETRY_BACKOFF_MS[attempt - 1], signal);
|
|
1185
|
-
if (signal?.aborted) throw
|
|
1165
|
+
if (signal?.aborted) throw err;
|
|
1186
1166
|
}
|
|
1187
1167
|
}
|
|
1188
1168
|
}
|
|
@@ -1210,14 +1190,14 @@ var CabaneApi = class {
|
|
|
1210
1190
|
retry: true,
|
|
1211
1191
|
...signal ? { signal } : {}
|
|
1212
1192
|
});
|
|
1213
|
-
} catch (
|
|
1193
|
+
} catch (err) {
|
|
1214
1194
|
const outbox = this.opts.outbox;
|
|
1215
|
-
if (!outbox) throw
|
|
1216
|
-
if (signal?.aborted || isAbortError(
|
|
1217
|
-
if (!isRetryable(
|
|
1195
|
+
if (!outbox) throw err;
|
|
1196
|
+
if (signal?.aborted || isAbortError(err)) throw err;
|
|
1197
|
+
if (!isRetryable(err)) throw err;
|
|
1218
1198
|
outbox.persist({ enqueuedAt: Date.now(), turnId, seq, method: "POST", path: path3, body, kind });
|
|
1219
1199
|
this.opts.log?.warn(
|
|
1220
|
-
{ kind, turnId, seq, err:
|
|
1200
|
+
{ kind, turnId, seq, err: err instanceof Error ? err.message : String(err) },
|
|
1221
1201
|
"companion: commit queued to outbox after transient failure (will drain when the API returns)"
|
|
1222
1202
|
);
|
|
1223
1203
|
}
|
|
@@ -1241,10 +1221,10 @@ var CabaneApi = class {
|
|
|
1241
1221
|
await this.request(entry.method, entry.path, entry.body, { retry: true });
|
|
1242
1222
|
outbox.remove(entry.turnId, entry.seq);
|
|
1243
1223
|
progressed = true;
|
|
1244
|
-
} catch (
|
|
1245
|
-
if (
|
|
1224
|
+
} catch (err) {
|
|
1225
|
+
if (err instanceof ApiError && err.status >= 400 && err.status < 500) {
|
|
1246
1226
|
this.opts.log?.warn(
|
|
1247
|
-
{ kind: entry.kind, turnId: entry.turnId, seq: entry.seq, status:
|
|
1227
|
+
{ kind: entry.kind, turnId: entry.turnId, seq: entry.seq, status: err.status },
|
|
1248
1228
|
"companion outbox: discarding entry on terminal 4xx (will never land)"
|
|
1249
1229
|
);
|
|
1250
1230
|
outbox.remove(entry.turnId, entry.seq);
|
|
@@ -1392,11 +1372,11 @@ var CabaneApi = class {
|
|
|
1392
1372
|
try {
|
|
1393
1373
|
await this.request("PATCH", path3, body, { retry: true });
|
|
1394
1374
|
outbox?.remove(key, ACTIVE_RUN_OUTBOX_SEQ);
|
|
1395
|
-
} catch (
|
|
1396
|
-
if (!outbox) throw
|
|
1397
|
-
if (!isRetryable(
|
|
1375
|
+
} catch (err) {
|
|
1376
|
+
if (!outbox) throw err;
|
|
1377
|
+
if (!isRetryable(err)) {
|
|
1398
1378
|
outbox.remove(key, ACTIVE_RUN_OUTBOX_SEQ);
|
|
1399
|
-
throw
|
|
1379
|
+
throw err;
|
|
1400
1380
|
}
|
|
1401
1381
|
outbox.persist({
|
|
1402
1382
|
enqueuedAt: Date.now(),
|
|
@@ -1408,7 +1388,7 @@ var CabaneApi = class {
|
|
|
1408
1388
|
kind: "active-run"
|
|
1409
1389
|
});
|
|
1410
1390
|
this.opts.log?.warn(
|
|
1411
|
-
{ conversationId, agentId, err:
|
|
1391
|
+
{ conversationId, agentId, err: err instanceof Error ? err.message : String(err) },
|
|
1412
1392
|
"companion: active-run write queued to outbox after transient failure (will drain when the API returns)"
|
|
1413
1393
|
);
|
|
1414
1394
|
}
|
|
@@ -1507,13 +1487,13 @@ var CabaneApi = class {
|
|
|
1507
1487
|
return res.messages.find((m) => m.id === messageId2) ?? null;
|
|
1508
1488
|
}
|
|
1509
1489
|
};
|
|
1510
|
-
function isRetryable(
|
|
1511
|
-
if (
|
|
1512
|
-
if (isAbortError(
|
|
1490
|
+
function isRetryable(err) {
|
|
1491
|
+
if (err instanceof ApiError) return err.status >= 500;
|
|
1492
|
+
if (isAbortError(err)) return false;
|
|
1513
1493
|
return true;
|
|
1514
1494
|
}
|
|
1515
|
-
function isAbortError(
|
|
1516
|
-
return
|
|
1495
|
+
function isAbortError(err) {
|
|
1496
|
+
return err instanceof Error && err.name === "AbortError";
|
|
1517
1497
|
}
|
|
1518
1498
|
function sleep(ms, signal) {
|
|
1519
1499
|
return new Promise((resolve) => {
|
|
@@ -1531,8 +1511,8 @@ function sleep(ms, signal) {
|
|
|
1531
1511
|
}
|
|
1532
1512
|
function errorMessage(status, body) {
|
|
1533
1513
|
if (body && typeof body === "object" && "error" in body) {
|
|
1534
|
-
const
|
|
1535
|
-
if (typeof
|
|
1514
|
+
const err = body.error;
|
|
1515
|
+
if (typeof err === "string") return `${status} ${err}`;
|
|
1536
1516
|
}
|
|
1537
1517
|
if (typeof body === "string" && body.length > 0) return `${status} ${body.slice(0, 200)}`;
|
|
1538
1518
|
return `${status} error`;
|
|
@@ -1588,8 +1568,8 @@ var DeviceApi = class {
|
|
|
1588
1568
|
};
|
|
1589
1569
|
function errorMessage2(status, body) {
|
|
1590
1570
|
if (body && typeof body === "object" && "error" in body) {
|
|
1591
|
-
const
|
|
1592
|
-
if (typeof
|
|
1571
|
+
const err = body.error;
|
|
1572
|
+
if (typeof err === "string") return `${status} ${err}`;
|
|
1593
1573
|
}
|
|
1594
1574
|
if (typeof body === "string" && body.length > 0) return `${status} ${body.slice(0, 200)}`;
|
|
1595
1575
|
return `${status} error`;
|
|
@@ -1606,11 +1586,11 @@ import {
|
|
|
1606
1586
|
writeFileSync as writeFileSync3
|
|
1607
1587
|
} from "fs";
|
|
1608
1588
|
import { dirname as dirname4, join as join6 } from "path";
|
|
1609
|
-
import { z as
|
|
1589
|
+
import { z as z3 } from "zod";
|
|
1610
1590
|
function credentialsPath() {
|
|
1611
1591
|
return join6(cabaneDir(), "credentials.json");
|
|
1612
1592
|
}
|
|
1613
|
-
var credentialStoreSchema =
|
|
1593
|
+
var credentialStoreSchema = z3.record(z3.string(), z3.string());
|
|
1614
1594
|
function load() {
|
|
1615
1595
|
const path3 = credentialsPath();
|
|
1616
1596
|
if (!existsSync4(path3)) return {};
|
|
@@ -1643,12 +1623,12 @@ function save(map) {
|
|
|
1643
1623
|
} catch {
|
|
1644
1624
|
}
|
|
1645
1625
|
renameSync2(tmp, path3);
|
|
1646
|
-
} catch (
|
|
1626
|
+
} catch (err) {
|
|
1647
1627
|
try {
|
|
1648
1628
|
rmSync3(tmp, { force: true });
|
|
1649
1629
|
} catch {
|
|
1650
1630
|
}
|
|
1651
|
-
throw
|
|
1631
|
+
throw err;
|
|
1652
1632
|
}
|
|
1653
1633
|
}
|
|
1654
1634
|
function getCredential(agentId) {
|
|
@@ -1821,44 +1801,44 @@ function noResume() {
|
|
|
1821
1801
|
var TURN_PROTOCOL_VERSION = 1;
|
|
1822
1802
|
|
|
1823
1803
|
// packages/agent-runtime/src/host-policy.ts
|
|
1824
|
-
import { z as
|
|
1825
|
-
var hostPolicySchema =
|
|
1804
|
+
import { z as z4 } from "zod";
|
|
1805
|
+
var hostPolicySchema = z4.object({
|
|
1826
1806
|
// Host filesystem + shell: `Bash`/`Read`/`Write`/`Edit`/`Glob`/`Grep`, the
|
|
1827
1807
|
// notebook read/write pair, git-worktree tools, and the `REPL` host code-exec
|
|
1828
1808
|
// tool. Off under the locked assistant surface (today's `DISALLOWED_TOOLS`);
|
|
1829
1809
|
// on under `coding` mode.
|
|
1830
|
-
hostFs:
|
|
1810
|
+
hostFs: z4.boolean(),
|
|
1831
1811
|
// Public web (today's `WebSearch` / `WebFetch`, the `DEFAULT_WEB_TOOLS`). Public
|
|
1832
1812
|
// web, not host reach — granted by default today, but expressible as a grant.
|
|
1833
|
-
web:
|
|
1813
|
+
web: z4.boolean(),
|
|
1834
1814
|
// Browser automation (the Playwright MCP surface). Varies by host: a companion has
|
|
1835
1815
|
// it, the house executor does not (CT230).
|
|
1836
|
-
browser:
|
|
1816
|
+
browser: z4.boolean(),
|
|
1837
1817
|
// User-configured MCP servers permitted. False for the house executor
|
|
1838
1818
|
// (CT227: Cabane agents run no user MCP servers), true for a personal companion.
|
|
1839
|
-
userMcp:
|
|
1819
|
+
userMcp: z4.boolean(),
|
|
1840
1820
|
// Subagent / sub-dispatch (`Task` / `Agent`). A per-host GRANT (see the CT261
|
|
1841
1821
|
// amendment above): `false` on the locked assistant/house surface (banned via
|
|
1842
1822
|
// `DISALLOWED_TOOLS`), `true` under coding / custom where the fan-out is left to
|
|
1843
1823
|
// the coding allow-hook (which reaches sub-agent tool calls, CT680) or the user
|
|
1844
1824
|
// allowlist. The subagent completes within the turn, so
|
|
1845
1825
|
// it's not the turn-model invariant `scheduling` is.
|
|
1846
|
-
subagents:
|
|
1826
|
+
subagents: z4.boolean(),
|
|
1847
1827
|
// ── Hard platform invariants — always denied, never granted ────────────────
|
|
1848
1828
|
// Deferred re-invocation / scheduling (`ScheduleWakeup`, the `Cron*` / `Task*`
|
|
1849
1829
|
// families, `Monitor`, `Workflow`, …). A turn is one query that resolves when
|
|
1850
1830
|
// `result` fires; a scheduled callback fires after the reply window has closed
|
|
1851
1831
|
// and strands the agent (the CT155/CT156 rule).
|
|
1852
|
-
scheduling:
|
|
1832
|
+
scheduling: z4.literal("never"),
|
|
1853
1833
|
// Human-facing UI prompts (`AskUserQuestion`). A Cabane conversation has no
|
|
1854
1834
|
// handler to answer a structured prompt, so the call hangs the turn
|
|
1855
1835
|
// (`UNSUPPORTED_TOOLS`). The agent asks in its reply instead.
|
|
1856
|
-
uiPrompts:
|
|
1836
|
+
uiPrompts: z4.literal("never")
|
|
1857
1837
|
});
|
|
1858
1838
|
|
|
1859
1839
|
// packages/agent-runtime/src/turn-event.ts
|
|
1860
|
-
import { z as
|
|
1861
|
-
var turnEventSchema =
|
|
1840
|
+
import { z as z5 } from "zod";
|
|
1841
|
+
var turnEventSchema = z5.discriminatedUnion("type", [
|
|
1862
1842
|
// The runtime's opaque session state, emitted when the adapter learns it (e.g.
|
|
1863
1843
|
// the SDK `system/init` frame). The platform stores `state` verbatim per
|
|
1864
1844
|
// (conversation, agent) and hands it back on the next turn; only the adapter
|
|
@@ -1878,19 +1858,19 @@ var turnEventSchema = z6.discriminatedUnion("type", [
|
|
|
1878
1858
|
// on the companion, after the server committed the manifest). Runtime-neutral: a
|
|
1879
1859
|
// plain boolean, not a runtime-specific reason string (that stays in the
|
|
1880
1860
|
// adapter's `onWarn` log). Additive + optional — an old receiver ignores it.
|
|
1881
|
-
|
|
1882
|
-
type:
|
|
1883
|
-
state:
|
|
1884
|
-
degraded:
|
|
1861
|
+
z5.object({
|
|
1862
|
+
type: z5.literal("session"),
|
|
1863
|
+
state: z5.string(),
|
|
1864
|
+
degraded: z5.boolean().optional()
|
|
1885
1865
|
}),
|
|
1886
1866
|
// One readable thinking summary. Maps `onThinking({ text })`. Transient —
|
|
1887
1867
|
// surfaced live, never persisted as durable content.
|
|
1888
|
-
|
|
1868
|
+
z5.object({ type: z5.literal("thinking"), text: z5.string() }),
|
|
1889
1869
|
// Assistant text. Maps `onAssistantText({ text, final })` — `text`→`body`,
|
|
1890
1870
|
// `final`→`terminal`. `terminal: false` is interim narration (commits as a
|
|
1891
1871
|
// `progress` row); `terminal: true` is the turn's closing reply (commits as
|
|
1892
1872
|
// the `final` row).
|
|
1893
|
-
|
|
1873
|
+
z5.object({ type: z5.literal("text"), body: z5.string(), terminal: z5.boolean() }),
|
|
1894
1874
|
// A tool-activity transition. Maps `onToolActivity` — `toolUseId`→`id`,
|
|
1895
1875
|
// `toolName`→`name` (already prefix-stripped: `cabane_read`, not
|
|
1896
1876
|
// `mcp__cabane__cabane_read`), `summary` is the short card label. `phase`
|
|
@@ -1905,15 +1885,15 @@ var turnEventSchema = z6.discriminatedUnion("type", [
|
|
|
1905
1885
|
// dropped the prefix; null for a host / built-in tool. The client tags Cabane
|
|
1906
1886
|
// MCP calls (`=== 'cabane'`) with a subtle glyph. Nullable + optional so a
|
|
1907
1887
|
// 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:
|
|
1888
|
+
z5.object({
|
|
1889
|
+
type: z5.literal("tool"),
|
|
1890
|
+
id: z5.string(),
|
|
1891
|
+
name: z5.string(),
|
|
1892
|
+
phase: z5.enum(["start", "done", "error"]),
|
|
1893
|
+
summary: z5.string(),
|
|
1894
|
+
input: z5.unknown().optional(),
|
|
1895
|
+
result: z5.unknown().optional(),
|
|
1896
|
+
mcpServer: z5.string().nullable().optional()
|
|
1917
1897
|
}),
|
|
1918
1898
|
// The turn's terminal outcome. Maps the `{ ok, reason }` both loops return
|
|
1919
1899
|
// inline. `ok:false` carries a machine reason (`no_session`, an error code);
|
|
@@ -1957,34 +1937,34 @@ var turnEventSchema = z6.discriminatedUnion("type", [
|
|
|
1957
1937
|
// `message_turn_metadata.resolved_model` / `resolved_config`. Optional ⇒
|
|
1958
1938
|
// backward-compatible: an old adapter/companion omits them, a cancel has no result
|
|
1959
1939
|
// 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:
|
|
1940
|
+
z5.object({
|
|
1941
|
+
type: z5.literal("result"),
|
|
1942
|
+
ok: z5.boolean(),
|
|
1943
|
+
reason: z5.string().optional(),
|
|
1944
|
+
usage: z5.object({
|
|
1945
|
+
inputTokens: z5.number(),
|
|
1946
|
+
outputTokens: z5.number(),
|
|
1947
|
+
cacheReadTokens: z5.number().optional(),
|
|
1948
|
+
cacheCreationTokens: z5.number().optional(),
|
|
1949
|
+
contextTokens: z5.number().optional(),
|
|
1950
|
+
contextWindow: z5.number().optional()
|
|
1971
1951
|
}).optional(),
|
|
1972
|
-
resolvedModel:
|
|
1973
|
-
resolvedConfig:
|
|
1974
|
-
effort:
|
|
1975
|
-
thinking:
|
|
1976
|
-
reasoningEffort:
|
|
1952
|
+
resolvedModel: z5.string().optional(),
|
|
1953
|
+
resolvedConfig: z5.object({
|
|
1954
|
+
effort: z5.string().optional(),
|
|
1955
|
+
thinking: z5.string().optional(),
|
|
1956
|
+
reasoningEffort: z5.string().optional()
|
|
1977
1957
|
}).optional()
|
|
1978
1958
|
})
|
|
1979
1959
|
]);
|
|
1980
1960
|
|
|
1981
1961
|
// packages/agent-runtime/src/failure.ts
|
|
1982
|
-
import { z as
|
|
1983
|
-
var turnFailureSchema =
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1962
|
+
import { z as z6 } from "zod";
|
|
1963
|
+
var turnFailureSchema = z6.discriminatedUnion("kind", [
|
|
1964
|
+
z6.object({ kind: z6.literal("usage_capped"), resetsAt: z6.string().optional() }),
|
|
1965
|
+
z6.object({ kind: z6.literal("rate_limited") }),
|
|
1966
|
+
z6.object({ kind: z6.literal("server_error") }),
|
|
1967
|
+
z6.object({ kind: z6.literal("auth_expired") })
|
|
1988
1968
|
]);
|
|
1989
1969
|
var USAGE_CAPPED = "usage_capped";
|
|
1990
1970
|
var RATE_LIMITED = "rate_limited";
|
|
@@ -2090,63 +2070,63 @@ var RATE_PATTERNS = [/rate[_ ]?limit/, /\b429\b/, /too many requests/];
|
|
|
2090
2070
|
var BARE_LIMIT = /\blimit (reached|exceeded)\b/;
|
|
2091
2071
|
|
|
2092
2072
|
// packages/agent-runtime/src/turn-request.ts
|
|
2093
|
-
import { z as
|
|
2094
|
-
var contentBlockSchema =
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
type:
|
|
2098
|
-
source:
|
|
2073
|
+
import { z as z7 } from "zod";
|
|
2074
|
+
var contentBlockSchema = z7.discriminatedUnion("type", [
|
|
2075
|
+
z7.object({ type: z7.literal("text"), text: z7.string() }),
|
|
2076
|
+
z7.object({
|
|
2077
|
+
type: z7.literal("image"),
|
|
2078
|
+
source: z7.object({ type: z7.literal("url"), url: z7.string() })
|
|
2099
2079
|
}),
|
|
2100
|
-
|
|
2101
|
-
type:
|
|
2102
|
-
source:
|
|
2080
|
+
z7.object({
|
|
2081
|
+
type: z7.literal("document"),
|
|
2082
|
+
source: z7.object({ type: z7.literal("url"), url: z7.string() })
|
|
2103
2083
|
})
|
|
2104
2084
|
]);
|
|
2105
|
-
var effortLevelSchema =
|
|
2106
|
-
var resolvedRunConfigSchema =
|
|
2107
|
-
model:
|
|
2085
|
+
var effortLevelSchema = z7.enum(["low", "medium", "high", "xhigh", "max"]);
|
|
2086
|
+
var resolvedRunConfigSchema = z7.object({
|
|
2087
|
+
model: z7.string().nullable(),
|
|
2108
2088
|
effort: effortLevelSchema.optional(),
|
|
2109
|
-
runtimeOptions:
|
|
2089
|
+
runtimeOptions: z7.record(z7.string(), z7.unknown()).optional()
|
|
2110
2090
|
});
|
|
2111
|
-
var resolvedMcpServerSchema =
|
|
2112
|
-
|
|
2113
|
-
type:
|
|
2114
|
-
command:
|
|
2115
|
-
args:
|
|
2116
|
-
env:
|
|
2091
|
+
var resolvedMcpServerSchema = z7.union([
|
|
2092
|
+
z7.object({
|
|
2093
|
+
type: z7.literal("stdio").optional(),
|
|
2094
|
+
command: z7.string(),
|
|
2095
|
+
args: z7.array(z7.string()).optional(),
|
|
2096
|
+
env: z7.record(z7.string(), z7.string()).optional()
|
|
2117
2097
|
}),
|
|
2118
|
-
|
|
2119
|
-
type:
|
|
2120
|
-
url:
|
|
2121
|
-
headers:
|
|
2098
|
+
z7.object({
|
|
2099
|
+
type: z7.literal("http"),
|
|
2100
|
+
url: z7.string(),
|
|
2101
|
+
headers: z7.record(z7.string(), z7.string()).optional()
|
|
2122
2102
|
}),
|
|
2123
|
-
|
|
2124
|
-
type:
|
|
2125
|
-
url:
|
|
2126
|
-
headers:
|
|
2103
|
+
z7.object({
|
|
2104
|
+
type: z7.literal("sse"),
|
|
2105
|
+
url: z7.string(),
|
|
2106
|
+
headers: z7.record(z7.string(), z7.string()).optional()
|
|
2127
2107
|
})
|
|
2128
2108
|
]);
|
|
2129
|
-
var resolvedMcpServersSchema =
|
|
2130
|
-
var hostInjectedServersSchema =
|
|
2131
|
-
var turnRequestSchema =
|
|
2109
|
+
var resolvedMcpServersSchema = z7.record(z7.string(), resolvedMcpServerSchema);
|
|
2110
|
+
var hostInjectedServersSchema = z7.record(z7.string(), z7.unknown());
|
|
2111
|
+
var turnRequestSchema = z7.object({
|
|
2132
2112
|
// Server-composed system prompt (core + capability prose + adapter addendum +
|
|
2133
2113
|
// charter). One string to the adapter.
|
|
2134
|
-
systemPrompt:
|
|
2114
|
+
systemPrompt: z7.string(),
|
|
2135
2115
|
// Server-composed per-turn user text (anchor reminder + the triggering message).
|
|
2136
|
-
prompt:
|
|
2116
|
+
prompt: z7.string(),
|
|
2137
2117
|
// The multi-block user-message body (text + vision).
|
|
2138
|
-
content:
|
|
2118
|
+
content: z7.array(contentBlockSchema),
|
|
2139
2119
|
// Portable-or-dialect run-config (above).
|
|
2140
2120
|
config: resolvedRunConfigSchema,
|
|
2141
2121
|
// Abstract capability grants; the adapter maps them to tool names.
|
|
2142
2122
|
policy: hostPolicySchema,
|
|
2143
2123
|
// Prior opaque session state, or null for a fresh session.
|
|
2144
|
-
session:
|
|
2124
|
+
session: z7.string().nullable(),
|
|
2145
2125
|
// The cabane control-plane coordinates for this turn's MCP + post-back.
|
|
2146
|
-
cabane:
|
|
2147
|
-
mcpUrl:
|
|
2148
|
-
bearer:
|
|
2149
|
-
activeConversationId:
|
|
2126
|
+
cabane: z7.object({
|
|
2127
|
+
mcpUrl: z7.string(),
|
|
2128
|
+
bearer: z7.string(),
|
|
2129
|
+
activeConversationId: z7.string(),
|
|
2150
2130
|
// CT714: the scoped TURN-CONTROL MCP endpoint (`/api/turn-control`). The
|
|
2151
2131
|
// EXTERNAL adapters (Codex / opencode) mount it by URL under the key
|
|
2152
2132
|
// `cabane_companion` — using the same `bearer` (the turn token) and the same
|
|
@@ -2156,7 +2136,7 @@ var turnRequestSchema = z8.object({
|
|
|
2156
2136
|
// claude-code ignores it (it mounts the in-process instance instead), and
|
|
2157
2137
|
// every existing `cabane`-block fixture keeps parsing unchanged; the
|
|
2158
2138
|
// companion always populates it (`build-options.ts`).
|
|
2159
|
-
turnControlUrl:
|
|
2139
|
+
turnControlUrl: z7.string().optional(),
|
|
2160
2140
|
// CT598: the workspace this turn runs in. The claude-code/opencode/codex
|
|
2161
2141
|
// adapters never need it (they reach Cabane through the `cabane` MCP server,
|
|
2162
2142
|
// which takes `workspaceId` as a per-tool arg the model supplies); the
|
|
@@ -2166,53 +2146,39 @@ var turnRequestSchema = z8.object({
|
|
|
2166
2146
|
// adapters' conformance fixtures, tests) keeps parsing unchanged — the companion
|
|
2167
2147
|
// always populates it (`build-options.ts`), and the native adapter fails the
|
|
2168
2148
|
// turn loudly when it is somehow absent rather than guessing.
|
|
2169
|
-
workspaceId:
|
|
2149
|
+
workspaceId: z7.string().optional(),
|
|
2170
2150
|
// CT752: the server-resolved workspace surface this credential exposes.
|
|
2171
2151
|
// Readiness uses this explicit fact to require `sdk` for code mode and the
|
|
2172
2152
|
// granular floor for classic mode; inventory contents alone cannot infer it
|
|
2173
2153
|
// because `sdk` is intentionally also available on the classic surface.
|
|
2174
|
-
workspaceToolSurface:
|
|
2154
|
+
workspaceToolSurface: z7.enum(["code", "classic"]).optional()
|
|
2175
2155
|
}),
|
|
2176
2156
|
// Machine-local resolution (host-filled): the checkout cwd, extra env from a
|
|
2177
2157
|
// 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(),
|
|
2158
|
+
local: z7.object({
|
|
2159
|
+
cwd: z7.string().optional(),
|
|
2160
|
+
env: z7.record(z7.string(), z7.string()).optional(),
|
|
2186
2161
|
mcpServers: resolvedMcpServersSchema.optional(),
|
|
2187
2162
|
// CT289: machine-local claude-code adapter knobs the operator sets on a
|
|
2188
2163
|
// companion they run themselves — the auto-memory escape hatch. `autoMemory:
|
|
2189
2164
|
// true` opts back into Claude Code's auto-memory (governed by the operator's
|
|
2190
2165
|
// 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()
|
|
2166
|
+
// default in place (see `buildClaudeCodeOptions`).
|
|
2167
|
+
claudeCode: z7.object({ autoMemory: z7.boolean().optional() }).optional()
|
|
2194
2168
|
}),
|
|
2195
2169
|
// 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()
|
|
2170
|
+
extra: z7.object({
|
|
2171
|
+
mcpServers: hostInjectedServersSchema
|
|
2206
2172
|
})
|
|
2207
2173
|
});
|
|
2208
2174
|
|
|
2209
2175
|
// packages/agent-runtime/src/conformance.ts
|
|
2210
|
-
import { z as
|
|
2211
|
-
var conformanceFixtureSchema =
|
|
2212
|
-
name:
|
|
2176
|
+
import { z as z8 } from "zod";
|
|
2177
|
+
var conformanceFixtureSchema = z8.object({
|
|
2178
|
+
name: z8.string(),
|
|
2213
2179
|
request: turnRequestSchema,
|
|
2214
|
-
nativeStream:
|
|
2215
|
-
expected:
|
|
2180
|
+
nativeStream: z8.array(z8.unknown()),
|
|
2181
|
+
expected: z8.array(turnEventSchema)
|
|
2216
2182
|
});
|
|
2217
2183
|
|
|
2218
2184
|
// packages/agent-runtime/src/transcript.ts
|
|
@@ -2222,8 +2188,8 @@ function createTerminalTextBuffer() {
|
|
|
2222
2188
|
async function safeEmit(emit, event, onError) {
|
|
2223
2189
|
try {
|
|
2224
2190
|
await emit(event);
|
|
2225
|
-
} catch (
|
|
2226
|
-
onError?.(
|
|
2191
|
+
} catch (err) {
|
|
2192
|
+
onError?.(err, event.type);
|
|
2227
2193
|
}
|
|
2228
2194
|
}
|
|
2229
2195
|
async function processAssistantMessage(msg, emit, pending, buffer, onError) {
|
|
@@ -2484,16 +2450,16 @@ var TurnPump = class {
|
|
|
2484
2450
|
// minimal note. Skipped when cancelled or already final. The held-text flush
|
|
2485
2451
|
// that precedes it is a classification concern, driven by the caller before
|
|
2486
2452
|
// this runs.
|
|
2487
|
-
async finalize(
|
|
2488
|
-
if (!
|
|
2453
|
+
async finalize(ok) {
|
|
2454
|
+
if (!ok || this.opts.signal.aborted || this.emittedFinal) return;
|
|
2489
2455
|
const body = this.lastProgressBody ?? this.opts.emptyFinalBody;
|
|
2490
2456
|
const seq = this.opts.nextSeq();
|
|
2491
2457
|
try {
|
|
2492
2458
|
await this.opts.commit.commitMessage({ body, kind: "final", seq });
|
|
2493
2459
|
this.emittedFinal = true;
|
|
2494
2460
|
this.finalReplyBody = body;
|
|
2495
|
-
} catch (
|
|
2496
|
-
this.opts.onError?.(
|
|
2461
|
+
} catch (err) {
|
|
2462
|
+
this.opts.onError?.(err, "empty-final");
|
|
2497
2463
|
}
|
|
2498
2464
|
}
|
|
2499
2465
|
// Whether the turn has committed its `final` row — read by the host to decide
|
|
@@ -2517,7 +2483,7 @@ import {
|
|
|
2517
2483
|
var CLAUDE_CODE_ADDENDUM = "";
|
|
2518
2484
|
|
|
2519
2485
|
// packages/agent-runtime/src/claude-code/policy.ts
|
|
2520
|
-
import { z as
|
|
2486
|
+
import { z as z9 } from "zod";
|
|
2521
2487
|
var HOST_FS_TOOLS = [
|
|
2522
2488
|
// shell + local filesystem
|
|
2523
2489
|
"Bash",
|
|
@@ -2567,30 +2533,20 @@ function withThinkingSummaries(thinking) {
|
|
|
2567
2533
|
if (thinking.type === "disabled") return thinking;
|
|
2568
2534
|
return { display: "summarized", ...thinking };
|
|
2569
2535
|
}
|
|
2570
|
-
var claudeCodeDialectSchema =
|
|
2571
|
-
thinking:
|
|
2572
|
-
|
|
2573
|
-
type:
|
|
2574
|
-
display:
|
|
2536
|
+
var claudeCodeDialectSchema = z9.object({
|
|
2537
|
+
thinking: z9.discriminatedUnion("type", [
|
|
2538
|
+
z9.object({
|
|
2539
|
+
type: z9.literal("adaptive"),
|
|
2540
|
+
display: z9.enum(["summarized", "omitted"]).optional()
|
|
2575
2541
|
}),
|
|
2576
|
-
|
|
2577
|
-
type:
|
|
2578
|
-
budgetTokens:
|
|
2579
|
-
display:
|
|
2542
|
+
z9.object({
|
|
2543
|
+
type: z9.literal("enabled"),
|
|
2544
|
+
budgetTokens: z9.number().int().positive().optional(),
|
|
2545
|
+
display: z9.enum(["summarized", "omitted"]).optional()
|
|
2580
2546
|
}),
|
|
2581
|
-
|
|
2547
|
+
z9.object({ type: z9.literal("disabled") })
|
|
2582
2548
|
]).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()
|
|
2549
|
+
hostAccess: z9.boolean().optional()
|
|
2594
2550
|
}).loose();
|
|
2595
2551
|
function readThinking(runtimeOptions) {
|
|
2596
2552
|
const dialect = runtimeOptions?.["claude-code"];
|
|
@@ -2669,23 +2625,25 @@ function buildClaudeCodeOptions(req, augment) {
|
|
|
2669
2625
|
};
|
|
2670
2626
|
}
|
|
2671
2627
|
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";
|
|
2628
|
+
const useCodingPreset = dialect.success ? dialect.data.hostAccess ?? false : false;
|
|
2675
2629
|
const cabaneGlob = `mcp__${CABANE_MCP_SERVER}__*`;
|
|
2676
2630
|
const extraServerGlobs = Object.keys(req.extra.mcpServers).map((name) => `mcp__${name}__*`);
|
|
2677
2631
|
const allowedTools = dedupe([
|
|
2678
2632
|
cabaneGlob,
|
|
2679
2633
|
...extraServerGlobs,
|
|
2680
|
-
...policy.web ? DEFAULT_WEB_TOOLS : []
|
|
2681
|
-
...customAllowed
|
|
2634
|
+
...policy.web ? DEFAULT_WEB_TOOLS : []
|
|
2682
2635
|
]);
|
|
2683
|
-
const disallowedTools = dedupe([...disallowedToolsFor(policy)
|
|
2636
|
+
const disallowedTools = dedupe([...disallowedToolsFor(policy)]);
|
|
2684
2637
|
const resumeDecision = decideResume(req.session, cwd);
|
|
2685
2638
|
const resume = "resume" in resumeDecision ? resumeDecision.resume : null;
|
|
2686
2639
|
const freshReason = "fresh" in resumeDecision ? resumeDecision.reason : void 0;
|
|
2687
2640
|
const devControlsAutoMemory = req.local.claudeCode?.autoMemory === true;
|
|
2688
2641
|
const model = parseClaudeCodeModel(config.model);
|
|
2642
|
+
if (model === null) {
|
|
2643
|
+
console.warn(
|
|
2644
|
+
`[agent-runtime/claude-code] CT823: dispatching a subscription turn with NO concrete model (config.model=${JSON.stringify(config.model)}) \u2014 the SDK will fall back to its bundled default. This should be unreachable post-CT823; the model resolver should have supplied a concrete house default.`
|
|
2645
|
+
);
|
|
2646
|
+
}
|
|
2689
2647
|
const base = {
|
|
2690
2648
|
// model / effort: `model` is pinned only when the config names a real one —
|
|
2691
2649
|
// omitted for "let it choose" (see `parseClaudeCodeModel`), so the SDK picks
|
|
@@ -2746,7 +2704,7 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
2746
2704
|
out.push(event);
|
|
2747
2705
|
};
|
|
2748
2706
|
let sessionEmitted = false;
|
|
2749
|
-
let
|
|
2707
|
+
let ok = false;
|
|
2750
2708
|
let resultReason;
|
|
2751
2709
|
let sawResult = false;
|
|
2752
2710
|
let usage;
|
|
@@ -2792,8 +2750,8 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
2792
2750
|
if (typeof info.rateLimitType === "string") rateLimitType = info.rateLimitType;
|
|
2793
2751
|
}
|
|
2794
2752
|
} else if (msg.type === "auth_status") {
|
|
2795
|
-
const
|
|
2796
|
-
if (typeof
|
|
2753
|
+
const err = msg.error;
|
|
2754
|
+
if (typeof err === "string" && err.length > 0) authError = err;
|
|
2797
2755
|
} else if (msg.type === "result") {
|
|
2798
2756
|
sawResult = true;
|
|
2799
2757
|
usage = readSdkUsage(msg);
|
|
@@ -2805,7 +2763,7 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
2805
2763
|
}
|
|
2806
2764
|
const isError = msg.is_error === true;
|
|
2807
2765
|
if (msg.subtype === "success" && !isError) {
|
|
2808
|
-
|
|
2766
|
+
ok = true;
|
|
2809
2767
|
} else {
|
|
2810
2768
|
const resultText = msg.result ?? "";
|
|
2811
2769
|
const terminalReason = msg.terminal_reason;
|
|
@@ -2819,26 +2777,26 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
2819
2777
|
...rateLimitResetIso ? { resetsAt: rateLimitResetIso } : {}
|
|
2820
2778
|
} : classifyAssistantError(lastAssistantError) ?? classifyErrorText([authError, errorText].filter(Boolean).join(" "));
|
|
2821
2779
|
resultReason = failure ? encodeFailureReason(failure) : isError ? `error:${resultText.slice(0, 200) || "unknown"}` : `result_error:${msg.subtype}`;
|
|
2822
|
-
|
|
2780
|
+
ok = false;
|
|
2823
2781
|
}
|
|
2824
2782
|
break;
|
|
2825
2783
|
}
|
|
2826
2784
|
}
|
|
2827
|
-
} catch (
|
|
2828
|
-
if (ctx.signal.aborted) throw
|
|
2829
|
-
const failure = classifyErrorText(
|
|
2830
|
-
if (!failure) throw
|
|
2831
|
-
|
|
2785
|
+
} catch (err) {
|
|
2786
|
+
if (ctx.signal.aborted) throw err;
|
|
2787
|
+
const failure = classifyErrorText(err instanceof Error ? err.message : String(err));
|
|
2788
|
+
if (!failure) throw err;
|
|
2789
|
+
ok = false;
|
|
2832
2790
|
resultReason = encodeFailureReason(failure);
|
|
2833
2791
|
sawResult = true;
|
|
2834
2792
|
}
|
|
2835
2793
|
if (ctx.signal.aborted) return;
|
|
2836
|
-
await flushHeldText(buffer, emit,
|
|
2794
|
+
await flushHeldText(buffer, emit, ok);
|
|
2837
2795
|
yield* drain(out);
|
|
2838
|
-
if (!
|
|
2796
|
+
if (!ok && !resultReason && !sawResult) resultReason = "no_result";
|
|
2839
2797
|
yield {
|
|
2840
2798
|
type: "result",
|
|
2841
|
-
ok
|
|
2799
|
+
ok,
|
|
2842
2800
|
...resultReason ? { reason: resultReason } : {},
|
|
2843
2801
|
...usage ? { usage } : {},
|
|
2844
2802
|
...resolvedModel ? { resolvedModel } : {}
|
|
@@ -3307,26 +3265,20 @@ function selectAdapter(registry, runtime) {
|
|
|
3307
3265
|
|
|
3308
3266
|
// packages/agent-runtime/src/opencode/addendum.ts
|
|
3309
3267
|
var OPENCODE_ADDENDUM = [
|
|
3310
|
-
"Your Cabane
|
|
3311
|
-
"
|
|
3312
|
-
"
|
|
3268
|
+
"Your one Cabane workspace tool has a plain name \u2014 `sdk` (you act on the",
|
|
3269
|
+
"workspace by writing a TypeScript program and calling `sdk` with it); the",
|
|
3270
|
+
"turn-control verbs (`ask`, `wake_me`, `summon_agent`, `skip_turn`) are",
|
|
3271
|
+
"plain-named too, as are the few ancillary cabane tools (`list_workspaces`,",
|
|
3272
|
+
"`read_binary`, `upload`, `begin_upload`, `finalize_upload`,",
|
|
3273
|
+
"`mint_render_token`), and the host tools are plain verbs (`bash`, `read`,",
|
|
3274
|
+
"`edit`). There is no `write`/`search`/`edit` CABANE tool here \u2014 those are",
|
|
3275
|
+
"`cabane` SDK calls inside your program, not tools (a bare `read`/`edit` is the",
|
|
3276
|
+
"HOST tool). If a tool appears in this prompt with an `mcp__\u2026__` prefix, that",
|
|
3313
3277
|
"prefix is not part of its name \u2014 call the tool by its plain verb. Write your",
|
|
3314
3278
|
"closing reply as the last thing you say in the turn: you can interleave",
|
|
3315
3279
|
"narration with tool calls, but only your final message is recorded as the",
|
|
3316
3280
|
"turn\u2019s reply."
|
|
3317
3281
|
].join(" ");
|
|
3318
|
-
var OPENCODE_ADDENDUM_CODE_MODE = [
|
|
3319
|
-
"Your one Cabane workspace tool has a plain name \u2014 `sdk` (you act on the",
|
|
3320
|
-
"workspace by writing a TypeScript program and calling `sdk` with it); the",
|
|
3321
|
-
"turn-control verbs (`ask`, `wake_me`, `summon_agent`, `skip_turn`) are",
|
|
3322
|
-
"plain-named too, and the host tools are plain verbs (`bash`, `read`, `edit`).",
|
|
3323
|
-
"There is no `write`/`search`/`edit` CABANE tool here \u2014 those are `cabane` SDK",
|
|
3324
|
-
"calls inside your program, not tools (a bare `read`/`edit` is the HOST tool). If",
|
|
3325
|
-
"a tool appears in this prompt with an `mcp__\u2026__` prefix, that prefix is not part",
|
|
3326
|
-
"of its name \u2014 call the tool by its plain verb. Write your closing reply as the",
|
|
3327
|
-
"last thing you say in the turn: you can interleave narration with tool calls, but",
|
|
3328
|
-
"only your final message is recorded as the turn\u2019s reply."
|
|
3329
|
-
].join(" ");
|
|
3330
3282
|
|
|
3331
3283
|
// packages/agent-runtime/src/opencode/events.ts
|
|
3332
3284
|
function asRecord(v) {
|
|
@@ -3401,9 +3353,9 @@ function readSessionId(properties) {
|
|
|
3401
3353
|
}
|
|
3402
3354
|
function readSessionError(properties) {
|
|
3403
3355
|
const props = asRecord(properties);
|
|
3404
|
-
const
|
|
3405
|
-
if (typeof
|
|
3406
|
-
const rec = asRecord(
|
|
3356
|
+
const err = props?.error;
|
|
3357
|
+
if (typeof err === "string") return err;
|
|
3358
|
+
const rec = asRecord(err);
|
|
3407
3359
|
if (!rec) return "unknown";
|
|
3408
3360
|
const { name, message } = deepestError(rec);
|
|
3409
3361
|
if (message && name && !isGenericErrorName(name)) return `${name}: ${message}`;
|
|
@@ -3439,7 +3391,7 @@ async function* decodeOpencodeStream(events, ctx) {
|
|
|
3439
3391
|
const pending = /* @__PURE__ */ new Map();
|
|
3440
3392
|
const startedTools = /* @__PURE__ */ new Set();
|
|
3441
3393
|
const finishedTools = /* @__PURE__ */ new Set();
|
|
3442
|
-
let
|
|
3394
|
+
let ok = false;
|
|
3443
3395
|
let reason;
|
|
3444
3396
|
let settled = false;
|
|
3445
3397
|
const userMessageIds = /* @__PURE__ */ new Set();
|
|
@@ -3502,7 +3454,7 @@ async function* decodeOpencodeStream(events, ctx) {
|
|
|
3502
3454
|
const sealed = sealHeld(held, true);
|
|
3503
3455
|
held = null;
|
|
3504
3456
|
if (sealed) yield sealed;
|
|
3505
|
-
|
|
3457
|
+
ok = true;
|
|
3506
3458
|
settled = true;
|
|
3507
3459
|
break;
|
|
3508
3460
|
} else if (ev.type === "session.error") {
|
|
@@ -3511,7 +3463,7 @@ async function* decodeOpencodeStream(events, ctx) {
|
|
|
3511
3463
|
const sealed = sealHeld(held, false);
|
|
3512
3464
|
held = null;
|
|
3513
3465
|
if (sealed) yield sealed;
|
|
3514
|
-
|
|
3466
|
+
ok = false;
|
|
3515
3467
|
const errorText = readSessionError(ev.properties);
|
|
3516
3468
|
const failure = classifyErrorText(errorText);
|
|
3517
3469
|
reason = failure ? encodeFailureReason(failure) : `error:${errorText.slice(0, 200)}`;
|
|
@@ -3526,7 +3478,7 @@ async function* decodeOpencodeStream(events, ctx) {
|
|
|
3526
3478
|
if (sealed) yield sealed;
|
|
3527
3479
|
reason = "no_terminal";
|
|
3528
3480
|
}
|
|
3529
|
-
yield { type: "result", ok
|
|
3481
|
+
yield { type: "result", ok, ...reason ? { reason } : {} };
|
|
3530
3482
|
}
|
|
3531
3483
|
function hasToolInput(input) {
|
|
3532
3484
|
return !!input && typeof input === "object" && Object.keys(input).length > 0;
|
|
@@ -3540,7 +3492,7 @@ function sealHeld(held, terminal) {
|
|
|
3540
3492
|
}
|
|
3541
3493
|
|
|
3542
3494
|
// packages/agent-runtime/src/opencode/policy.ts
|
|
3543
|
-
import { z as
|
|
3495
|
+
import { z as z10 } from "zod";
|
|
3544
3496
|
var OPENCODE_HOST_TOOLS = [
|
|
3545
3497
|
"bash",
|
|
3546
3498
|
"edit",
|
|
@@ -3567,8 +3519,8 @@ function opencodeToolPolicy(policy) {
|
|
|
3567
3519
|
deny(OPENCODE_UI_PROMPT_TOOLS);
|
|
3568
3520
|
return { tools, allowAllHostTools: policy.hostFs };
|
|
3569
3521
|
}
|
|
3570
|
-
var opencodeDialectSchema =
|
|
3571
|
-
agent:
|
|
3522
|
+
var opencodeDialectSchema = z10.object({
|
|
3523
|
+
agent: z10.string().min(1).optional()
|
|
3572
3524
|
}).loose();
|
|
3573
3525
|
function readOpencodeDialect(runtimeOptions) {
|
|
3574
3526
|
const parsed = opencodeDialectSchema.safeParse(runtimeOptions?.["opencode"] ?? {});
|
|
@@ -3789,9 +3741,9 @@ function createHttpOpencodeTransport(opts) {
|
|
|
3789
3741
|
// The lock is released when this stream finishes draining.
|
|
3790
3742
|
events: releaseAfter(parseSseStream(eventRes.body, sessionId, signal), release)
|
|
3791
3743
|
};
|
|
3792
|
-
} catch (
|
|
3744
|
+
} catch (err) {
|
|
3793
3745
|
release();
|
|
3794
|
-
throw
|
|
3746
|
+
throw err;
|
|
3795
3747
|
}
|
|
3796
3748
|
}
|
|
3797
3749
|
};
|
|
@@ -3878,7 +3830,7 @@ function createOpencodeAdapter(deps = {}) {
|
|
|
3878
3830
|
name: "opencode",
|
|
3879
3831
|
// CT614: surface-aware — a code-mode turn (only `code` mounted) is taught
|
|
3880
3832
|
// `code`, not the granular cabane names it no longer has.
|
|
3881
|
-
promptAddendum: (
|
|
3833
|
+
promptAddendum: () => OPENCODE_ADDENDUM,
|
|
3882
3834
|
dialectSchema: opencodeDialectSchema,
|
|
3883
3835
|
async *runTurn(req, signal) {
|
|
3884
3836
|
if (!transport) {
|
|
@@ -4278,14 +4230,6 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
4278
4230
|
|
|
4279
4231
|
// packages/agent-runtime/src/codex/addendum.ts
|
|
4280
4232
|
var CODEX_ADDENDUM = [
|
|
4281
|
-
"Your Cabane tools have plain names \u2014 `read`, `write`, `search`, `edit`,",
|
|
4282
|
-
"`post_message`, and so on. If a tool appears in this prompt with an `mcp__\u2026__`",
|
|
4283
|
-
"prefix, that prefix is not part of its name \u2014 call the tool by its plain verb.",
|
|
4284
|
-
"Write your closing reply as the last thing you say in the turn: you can",
|
|
4285
|
-
"interleave narration with tool calls, but only your final message is recorded",
|
|
4286
|
-
"as the turn\u2019s reply."
|
|
4287
|
-
].join(" ");
|
|
4288
|
-
var CODEX_ADDENDUM_CODE_MODE = [
|
|
4289
4233
|
"Your Cabane workspace tool is exposed to Codex as the qualified MCP tool",
|
|
4290
4234
|
"`mcp__cabane__sdk`. On Codex versions that defer MCP tools, locate it in the",
|
|
4291
4235
|
"`functions.exec` deferred-tool inventory and invoke that exact qualified tool",
|
|
@@ -4294,13 +4238,14 @@ var CODEX_ADDENDUM_CODE_MODE = [
|
|
|
4294
4238
|
"declare the SDK absent without attempting discovery and invocation. The SDK",
|
|
4295
4239
|
"call runs a TypeScript program against the ambient `cabane` object. The",
|
|
4296
4240
|
"turn-control verbs (`ask`, `wake_me`, `summon_agent`, `skip_turn`) are",
|
|
4297
|
-
"qualified `mcp__cabane_companion__\u2026` tools (and may be deferred too)
|
|
4298
|
-
"
|
|
4299
|
-
"
|
|
4300
|
-
"
|
|
4301
|
-
"
|
|
4302
|
-
"
|
|
4303
|
-
"
|
|
4241
|
+
"qualified `mcp__cabane_companion__\u2026` tools (and may be deferred too), as are",
|
|
4242
|
+
"the few ancillary cabane tools (`mcp__cabane__list_workspaces`,",
|
|
4243
|
+
"`mcp__cabane__read_binary`, the upload tools, `mcp__cabane__mint_render_token`).",
|
|
4244
|
+
"There is no Cabane `read`/`write`/`search`/`edit` tool here \u2014 those are `cabane`",
|
|
4245
|
+
"SDK calls inside your program, not tools. If a tool appears in this prompt with",
|
|
4246
|
+
"an `mcp__\u2026__` prefix, preserve that qualified name. Write your closing reply as",
|
|
4247
|
+
"the last thing you say in the turn: you can interleave narration with tool",
|
|
4248
|
+
"calls, but only your final message is recorded as the turn\u2019s reply."
|
|
4304
4249
|
].join(" ");
|
|
4305
4250
|
|
|
4306
4251
|
// packages/agent-runtime/src/codex/events.ts
|
|
@@ -4384,9 +4329,9 @@ function readItemType(item) {
|
|
|
4384
4329
|
function readErrorMessage(ev) {
|
|
4385
4330
|
const direct = str(ev.message);
|
|
4386
4331
|
if (direct) return direct;
|
|
4387
|
-
const
|
|
4388
|
-
if (
|
|
4389
|
-
const m = str(
|
|
4332
|
+
const err = asRecord2(ev.error);
|
|
4333
|
+
if (err) {
|
|
4334
|
+
const m = str(err.message);
|
|
4390
4335
|
if (m) return m;
|
|
4391
4336
|
}
|
|
4392
4337
|
return "unknown";
|
|
@@ -4442,7 +4387,7 @@ async function* decodeCodexStream(events, ctx) {
|
|
|
4442
4387
|
const startedTools = /* @__PURE__ */ new Set();
|
|
4443
4388
|
const finishedTools = /* @__PURE__ */ new Set();
|
|
4444
4389
|
let sessionEmitted = false;
|
|
4445
|
-
let
|
|
4390
|
+
let ok = false;
|
|
4446
4391
|
let reason;
|
|
4447
4392
|
let usage;
|
|
4448
4393
|
let settled = false;
|
|
@@ -4473,7 +4418,7 @@ async function* decodeCodexStream(events, ctx) {
|
|
|
4473
4418
|
const message = readItemMessage(item);
|
|
4474
4419
|
if (isModelMetadataError(message)) {
|
|
4475
4420
|
yield* flushInterim();
|
|
4476
|
-
|
|
4421
|
+
ok = false;
|
|
4477
4422
|
reason = `model_unavailable:${message.slice(0, 200)}`;
|
|
4478
4423
|
settled = true;
|
|
4479
4424
|
break;
|
|
@@ -4528,13 +4473,13 @@ async function* decodeCodexStream(events, ctx) {
|
|
|
4528
4473
|
held = null;
|
|
4529
4474
|
if (sealed) yield sealed;
|
|
4530
4475
|
usage = readUsage(ev);
|
|
4531
|
-
|
|
4476
|
+
ok = true;
|
|
4532
4477
|
settled = true;
|
|
4533
4478
|
break;
|
|
4534
4479
|
}
|
|
4535
4480
|
if (ev.type === "turn.failed" || ev.type === "error") {
|
|
4536
4481
|
yield* flushInterim();
|
|
4537
|
-
|
|
4482
|
+
ok = false;
|
|
4538
4483
|
const text = readErrorMessage(ev);
|
|
4539
4484
|
const failure = classifyErrorText(text);
|
|
4540
4485
|
reason = failure ? encodeFailureReason(failure) : `error:${text.slice(0, 200)}`;
|
|
@@ -4552,7 +4497,7 @@ async function* decodeCodexStream(events, ctx) {
|
|
|
4552
4497
|
const resolvedConfig = ctx.resolvedReasoningEffort ? { reasoningEffort: ctx.resolvedReasoningEffort } : void 0;
|
|
4553
4498
|
yield {
|
|
4554
4499
|
type: "result",
|
|
4555
|
-
ok
|
|
4500
|
+
ok,
|
|
4556
4501
|
...reason ? { reason } : {},
|
|
4557
4502
|
...usage ? { usage } : {},
|
|
4558
4503
|
...ctx.resolvedModel ? { resolvedModel: ctx.resolvedModel } : {},
|
|
@@ -4570,7 +4515,7 @@ function sealHeld2(held, terminal) {
|
|
|
4570
4515
|
}
|
|
4571
4516
|
|
|
4572
4517
|
// packages/agent-runtime/src/codex/policy.ts
|
|
4573
|
-
import { z as
|
|
4518
|
+
import { z as z11 } from "zod";
|
|
4574
4519
|
function codexToolPolicy(policy) {
|
|
4575
4520
|
return policy.hostFs ? {
|
|
4576
4521
|
permissionProfile: "cabane-coding",
|
|
@@ -4585,8 +4530,8 @@ function codexToolPolicy(policy) {
|
|
|
4585
4530
|
};
|
|
4586
4531
|
}
|
|
4587
4532
|
var CODEX_REASONING_EFFORTS = ["minimal", "low", "medium", "high", "xhigh"];
|
|
4588
|
-
var codexDialectSchema =
|
|
4589
|
-
modelReasoningEffort:
|
|
4533
|
+
var codexDialectSchema = z11.object({
|
|
4534
|
+
modelReasoningEffort: z11.enum(CODEX_REASONING_EFFORTS).optional()
|
|
4590
4535
|
}).loose();
|
|
4591
4536
|
function readCodexDialect(runtimeOptions) {
|
|
4592
4537
|
const parsed = codexDialectSchema.safeParse(runtimeOptions?.["codex"] ?? {});
|
|
@@ -4594,7 +4539,7 @@ function readCodexDialect(runtimeOptions) {
|
|
|
4594
4539
|
}
|
|
4595
4540
|
|
|
4596
4541
|
// packages/agent-runtime/src/codex/model.ts
|
|
4597
|
-
var CODEX_DEFAULT_MODEL_BARES = /* @__PURE__ */ new Set(["default"
|
|
4542
|
+
var CODEX_DEFAULT_MODEL_BARES = /* @__PURE__ */ new Set(["default"]);
|
|
4598
4543
|
function parseCodexModel(model) {
|
|
4599
4544
|
const sep = model.indexOf("/");
|
|
4600
4545
|
const bare = sep === -1 || model.slice(0, sep) !== "openai" ? model : model.slice(sep + 1);
|
|
@@ -4609,10 +4554,16 @@ function buildRunSpec2(req, resumeThreadId) {
|
|
|
4609
4554
|
const { policy, config } = req;
|
|
4610
4555
|
const directory = req.local.cwd ?? "";
|
|
4611
4556
|
const dialect = readCodexDialect(config.runtimeOptions);
|
|
4557
|
+
const model = config.model ? parseCodexModel(config.model) : null;
|
|
4558
|
+
if (model === null) {
|
|
4559
|
+
console.warn(
|
|
4560
|
+
`[agent-runtime/codex] CT823: dispatching a subscription turn with NO concrete model (config.model=${JSON.stringify(config.model)}) \u2014 Codex will fall back to its own default. This should be unreachable post-CT823; the model resolver should have supplied a concrete house default.`
|
|
4561
|
+
);
|
|
4562
|
+
}
|
|
4612
4563
|
return {
|
|
4613
4564
|
resumeThreadId,
|
|
4614
4565
|
directory,
|
|
4615
|
-
model
|
|
4566
|
+
model,
|
|
4616
4567
|
policy: codexToolPolicy(policy),
|
|
4617
4568
|
skipGitRepoCheck: true,
|
|
4618
4569
|
...dialect.modelReasoningEffort ? { modelReasoningEffort: dialect.modelReasoningEffort } : {},
|
|
@@ -4681,9 +4632,11 @@ function buildConfig(req) {
|
|
|
4681
4632
|
};
|
|
4682
4633
|
}
|
|
4683
4634
|
const policy = codexToolPolicy(req.policy);
|
|
4635
|
+
const tmpDir = req.local.env?.TMPDIR;
|
|
4684
4636
|
return {
|
|
4685
4637
|
mcp_servers,
|
|
4686
4638
|
experimental_use_rmcp_client: true,
|
|
4639
|
+
...tmpDir ? { shell_environment_policy: { set: { TMPDIR: tmpDir } } } : {},
|
|
4687
4640
|
...policy.permissionProfile ? {
|
|
4688
4641
|
// CT733: named permission profiles are Codex's split-filesystem path.
|
|
4689
4642
|
// `:root = read` preserves coding-mode host reads; the one explicit
|
|
@@ -4961,7 +4914,7 @@ var CodexExec = class {
|
|
|
4961
4914
|
signal: args.signal
|
|
4962
4915
|
});
|
|
4963
4916
|
let spawnError = null;
|
|
4964
|
-
child.once("error", (
|
|
4917
|
+
child.once("error", (err) => spawnError = err);
|
|
4965
4918
|
if (!child.stdin) {
|
|
4966
4919
|
child.kill();
|
|
4967
4920
|
throw new Error("Child process has no stdin");
|
|
@@ -5278,7 +5231,7 @@ function createCodexAdapter(deps = {}) {
|
|
|
5278
5231
|
name: "codex",
|
|
5279
5232
|
// CT614: surface-aware — a code-mode turn (only `code` mounted) is taught
|
|
5280
5233
|
// `code`, not the granular names it no longer has.
|
|
5281
|
-
promptAddendum: (
|
|
5234
|
+
promptAddendum: () => CODEX_ADDENDUM,
|
|
5282
5235
|
dialectSchema: codexDialectSchema,
|
|
5283
5236
|
async *runTurn(req, signal) {
|
|
5284
5237
|
if (!transport) {
|
|
@@ -5865,1027 +5818,6 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
5865
5818
|
}
|
|
5866
5819
|
];
|
|
5867
5820
|
|
|
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
5821
|
// packages/agent-runtime/src/claude-code/sdk.ts
|
|
6890
5822
|
import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
6891
5823
|
|
|
@@ -6943,7 +5875,7 @@ import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync
|
|
|
6943
5875
|
import { join as join12 } from "path";
|
|
6944
5876
|
|
|
6945
5877
|
// src/summon.ts
|
|
6946
|
-
import { z as
|
|
5878
|
+
import { z as z12 } from "zod";
|
|
6947
5879
|
var COMPANION_LOCAL_MCP_SERVER = "cabane_companion";
|
|
6948
5880
|
var SUMMON_AGENT_TOOL = "summon_agent";
|
|
6949
5881
|
var SUMMON_AGENT_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${SUMMON_AGENT_TOOL}`;
|
|
@@ -6977,7 +5909,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
6977
5909
|
SUMMON_AGENT_TOOL,
|
|
6978
5910
|
"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
5911
|
{
|
|
6980
|
-
agentId:
|
|
5912
|
+
agentId: z12.string().uuid().describe("The peer agent to summon \u2014 a workspace agent id from `list_agents`.")
|
|
6981
5913
|
},
|
|
6982
5914
|
async (args) => {
|
|
6983
5915
|
summonState.agentId = args.agentId;
|
|
@@ -6992,7 +5924,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
6992
5924
|
SKIP_TURN_TOOL,
|
|
6993
5925
|
`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
5926
|
{
|
|
6995
|
-
reason:
|
|
5927
|
+
reason: z12.string().min(1).max(500).describe("Short reason you are declining \u2014 used for telemetry/debugging.")
|
|
6996
5928
|
},
|
|
6997
5929
|
async (args) => {
|
|
6998
5930
|
skipState.skipped = true;
|
|
@@ -7009,21 +5941,21 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
7009
5941
|
ASK_TOOL,
|
|
7010
5942
|
"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
5943
|
{
|
|
7012
|
-
targetUserId:
|
|
7013
|
-
question:
|
|
5944
|
+
targetUserId: z12.string().uuid().describe("The workspace member (human) to ask \u2014 a user id from `list_members`."),
|
|
5945
|
+
question: z12.string().min(1).max(400).optional().describe(
|
|
7014
5946
|
"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
5947
|
),
|
|
7016
|
-
headline:
|
|
5948
|
+
headline: z12.string().min(1).max(120).optional().describe(
|
|
7017
5949
|
'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
5950
|
),
|
|
7019
|
-
options:
|
|
7020
|
-
questions:
|
|
7021
|
-
|
|
7022
|
-
headline:
|
|
5951
|
+
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."),
|
|
5952
|
+
questions: z12.array(
|
|
5953
|
+
z12.object({
|
|
5954
|
+
headline: z12.string().min(1).max(120).describe(
|
|
7023
5955
|
'The one-sentence question ("Do we go to prod?") \u2014 required for each item.'
|
|
7024
5956
|
),
|
|
7025
|
-
body:
|
|
7026
|
-
options:
|
|
5957
|
+
body: z12.string().min(1).max(400).optional().describe("Optional short framing beneath the headline. NOT a report."),
|
|
5958
|
+
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
5959
|
})
|
|
7028
5960
|
).min(1).max(5).optional().describe(
|
|
7029
5961
|
"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 +6012,13 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
7080
6012
|
SUB_AGENT_TOOL,
|
|
7081
6013
|
"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
6014
|
{
|
|
7083
|
-
prompt:
|
|
6015
|
+
prompt: z12.string().min(1).max(65536).describe(
|
|
7084
6016
|
"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
6017
|
),
|
|
7086
|
-
agentId:
|
|
6018
|
+
agentId: z12.string().uuid().optional().describe(
|
|
7087
6019
|
"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
6020
|
),
|
|
7089
|
-
title:
|
|
6021
|
+
title: z12.string().max(200).optional().describe("Optional title for the child thread (result chips link it).")
|
|
7090
6022
|
},
|
|
7091
6023
|
async (args) => {
|
|
7092
6024
|
const result = await subAgentCreate(args);
|
|
@@ -7116,13 +6048,13 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
7116
6048
|
WAKE_ME_TOOL,
|
|
7117
6049
|
'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
6050
|
{
|
|
7119
|
-
afterSeconds:
|
|
6051
|
+
afterSeconds: z12.number().int().positive().optional().describe(
|
|
7120
6052
|
"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
6053
|
),
|
|
7122
|
-
at:
|
|
6054
|
+
at: z12.string().datetime({ offset: true }).optional().describe(
|
|
7123
6055
|
"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
6056
|
),
|
|
7125
|
-
note:
|
|
6057
|
+
note: z12.string().min(1).max(2e3).describe(
|
|
7126
6058
|
'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
6059
|
)
|
|
7128
6060
|
},
|
|
@@ -7202,7 +6134,6 @@ function buildCompanionTurnRequest(params) {
|
|
|
7202
6134
|
local: {
|
|
7203
6135
|
...params.cwd ? { cwd: params.cwd } : {},
|
|
7204
6136
|
...params.env ? { env: params.env } : {},
|
|
7205
|
-
...params.nativeWorkAssignment ? { nativeWorkAssignment: params.nativeWorkAssignment } : {},
|
|
7206
6137
|
// User MCP servers (already `${PLACEHOLDER}`-resolved). Structurally the
|
|
7207
6138
|
// adapter's `ResolvedMcpServers`.
|
|
7208
6139
|
...Object.keys(params.mcpServers).length > 0 ? { mcpServers: params.mcpServers } : {},
|
|
@@ -7210,10 +6141,9 @@ function buildCompanionTurnRequest(params) {
|
|
|
7210
6141
|
...params.claudeCode ? { claudeCode: params.claudeCode } : {}
|
|
7211
6142
|
},
|
|
7212
6143
|
// Host-injected: the companion-local summon server (for the subprocess adapters,
|
|
7213
|
-
// under its own namespace)
|
|
6144
|
+
// under its own namespace).
|
|
7214
6145
|
extra: {
|
|
7215
|
-
mcpServers: { [COMPANION_LOCAL_MCP_SERVER]: params.summonServer }
|
|
7216
|
-
turnControl: params.turnControl
|
|
6146
|
+
mcpServers: { [COMPANION_LOCAL_MCP_SERVER]: params.summonServer }
|
|
7217
6147
|
}
|
|
7218
6148
|
};
|
|
7219
6149
|
}
|
|
@@ -7230,23 +6160,18 @@ function dirFor(workspaceId) {
|
|
|
7230
6160
|
function conversationDir(workspaceId, conversationId) {
|
|
7231
6161
|
return join9(dirFor(workspaceId), encodeURIComponent(conversationId));
|
|
7232
6162
|
}
|
|
7233
|
-
function pathFor3(workspaceId, conversationId, agentId
|
|
7234
|
-
|
|
7235
|
-
return join9(
|
|
7236
|
-
conversationDir(workspaceId, conversationId),
|
|
7237
|
-
`${encodeURIComponent(agentId)}${suffix}.json`
|
|
7238
|
-
);
|
|
6163
|
+
function pathFor3(workspaceId, conversationId, agentId) {
|
|
6164
|
+
return join9(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
|
|
7239
6165
|
}
|
|
7240
|
-
function readPrepared(workspaceId, conversationId, agentId
|
|
7241
|
-
const path3 = pathFor3(workspaceId, conversationId, agentId
|
|
6166
|
+
function readPrepared(workspaceId, conversationId, agentId) {
|
|
6167
|
+
const path3 = pathFor3(workspaceId, conversationId, agentId);
|
|
7242
6168
|
if (!existsSync7(path3)) return null;
|
|
7243
6169
|
try {
|
|
7244
6170
|
const parsed = JSON.parse(readFileSync6(path3, "utf8"));
|
|
7245
6171
|
if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
|
|
7246
6172
|
return {
|
|
7247
6173
|
cwd: parsed.cwd,
|
|
7248
|
-
...parsed.env && typeof parsed.env === "object" ? { env: parsed.env } : {}
|
|
7249
|
-
...parsed.nativeWorkAssignment && typeof parsed.nativeWorkAssignment === "object" ? { nativeWorkAssignment: parsed.nativeWorkAssignment } : {}
|
|
6174
|
+
...parsed.env && typeof parsed.env === "object" ? { env: parsed.env } : {}
|
|
7250
6175
|
};
|
|
7251
6176
|
}
|
|
7252
6177
|
return null;
|
|
@@ -7254,10 +6179,10 @@ function readPrepared(workspaceId, conversationId, agentId, assignmentKey) {
|
|
|
7254
6179
|
return null;
|
|
7255
6180
|
}
|
|
7256
6181
|
}
|
|
7257
|
-
function writePrepared(workspaceId, conversationId, agentId, result
|
|
6182
|
+
function writePrepared(workspaceId, conversationId, agentId, result) {
|
|
7258
6183
|
mkdirSync7(conversationDir(workspaceId, conversationId), { recursive: true });
|
|
7259
6184
|
writeFileSync6(
|
|
7260
|
-
pathFor3(workspaceId, conversationId, agentId
|
|
6185
|
+
pathFor3(workspaceId, conversationId, agentId),
|
|
7261
6186
|
JSON.stringify(result) + "\n",
|
|
7262
6187
|
"utf8"
|
|
7263
6188
|
);
|
|
@@ -7266,30 +6191,30 @@ function writePrepared(workspaceId, conversationId, agentId, result, assignmentK
|
|
|
7266
6191
|
// src/secrets.ts
|
|
7267
6192
|
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
7268
6193
|
import { join as join10 } from "path";
|
|
7269
|
-
import { z as
|
|
6194
|
+
import { z as z13 } from "zod";
|
|
7270
6195
|
var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
7271
6196
|
function secretsPath() {
|
|
7272
6197
|
return join10(cabaneDir(), "secrets.json");
|
|
7273
6198
|
}
|
|
7274
|
-
var secretStoreSchema =
|
|
6199
|
+
var secretStoreSchema = z13.record(z13.string(), z13.string());
|
|
7275
6200
|
function loadSecretStore() {
|
|
7276
6201
|
const path3 = secretsPath();
|
|
7277
6202
|
if (!existsSync8(path3)) return makeStore({});
|
|
7278
6203
|
let raw;
|
|
7279
6204
|
try {
|
|
7280
6205
|
raw = readFileSync7(path3, "utf8");
|
|
7281
|
-
} catch (
|
|
6206
|
+
} catch (err) {
|
|
7282
6207
|
throw new ConfigError(
|
|
7283
|
-
`couldn't read ${path3}: ${
|
|
6208
|
+
`couldn't read ${path3}: ${err instanceof Error ? err.message : String(err)}`
|
|
7284
6209
|
);
|
|
7285
6210
|
}
|
|
7286
6211
|
if (raw.trim().length === 0) return makeStore({});
|
|
7287
6212
|
let parsed;
|
|
7288
6213
|
try {
|
|
7289
6214
|
parsed = JSON.parse(raw);
|
|
7290
|
-
} catch (
|
|
6215
|
+
} catch (err) {
|
|
7291
6216
|
throw new ConfigError(
|
|
7292
|
-
`${path3} is not valid JSON: ${
|
|
6217
|
+
`${path3} is not valid JSON: ${err instanceof Error ? err.message : String(err)}. It must be a flat object of "NAME": "value" secret pairs.`
|
|
7293
6218
|
);
|
|
7294
6219
|
}
|
|
7295
6220
|
const result = secretStoreSchema.safeParse(parsed);
|
|
@@ -7303,8 +6228,8 @@ function loadSecretStore() {
|
|
|
7303
6228
|
function loadSecretStoreTolerant(onWarn) {
|
|
7304
6229
|
try {
|
|
7305
6230
|
return loadSecretStore();
|
|
7306
|
-
} catch (
|
|
7307
|
-
onWarn?.(
|
|
6231
|
+
} catch (err) {
|
|
6232
|
+
onWarn?.(err instanceof Error ? err.message : String(err));
|
|
7308
6233
|
return makeStore({});
|
|
7309
6234
|
}
|
|
7310
6235
|
}
|
|
@@ -7375,8 +6300,8 @@ var TranscriptWriter = class {
|
|
|
7375
6300
|
} catch {
|
|
7376
6301
|
}
|
|
7377
6302
|
pruneOld(dir2, RETAIN);
|
|
7378
|
-
} catch (
|
|
7379
|
-
this.fail(
|
|
6303
|
+
} catch (err) {
|
|
6304
|
+
this.fail(err);
|
|
7380
6305
|
}
|
|
7381
6306
|
this.line({ type: "_meta", ...meta });
|
|
7382
6307
|
}
|
|
@@ -7392,14 +6317,14 @@ var TranscriptWriter = class {
|
|
|
7392
6317
|
if (this.broken) return;
|
|
7393
6318
|
try {
|
|
7394
6319
|
appendFileSync(this.path, JSON.stringify(obj) + "\n", { mode: 384 });
|
|
7395
|
-
} catch (
|
|
7396
|
-
this.fail(
|
|
6320
|
+
} catch (err) {
|
|
6321
|
+
this.fail(err);
|
|
7397
6322
|
}
|
|
7398
6323
|
}
|
|
7399
|
-
fail(
|
|
6324
|
+
fail(err) {
|
|
7400
6325
|
if (this.broken) return;
|
|
7401
6326
|
this.broken = true;
|
|
7402
|
-
this.onWarn?.(`transcript write failed (${
|
|
6327
|
+
this.onWarn?.(`transcript write failed (${err instanceof Error ? err.message : String(err)})`);
|
|
7403
6328
|
}
|
|
7404
6329
|
};
|
|
7405
6330
|
function fileName(meta) {
|
|
@@ -7437,9 +6362,9 @@ var EMPTY_FINAL_BODY = "Done \u2014 see the changes above.";
|
|
|
7437
6362
|
var TurnCommitter = class {
|
|
7438
6363
|
constructor(deps) {
|
|
7439
6364
|
this.deps = deps;
|
|
7440
|
-
this.onError = (
|
|
6365
|
+
this.onError = (err, hook) => {
|
|
7441
6366
|
deps.log.warn(
|
|
7442
|
-
{ err:
|
|
6367
|
+
{ err: err instanceof Error ? err.message : String(err), hook },
|
|
7443
6368
|
"dispatcher: transcript callback failed"
|
|
7444
6369
|
);
|
|
7445
6370
|
};
|
|
@@ -7504,9 +6429,9 @@ var TurnCommitter = class {
|
|
|
7504
6429
|
signal: deps.signal,
|
|
7505
6430
|
nextSeq: deps.nextSeq,
|
|
7506
6431
|
emptyFinalBody: EMPTY_FINAL_BODY,
|
|
7507
|
-
onError: (
|
|
6432
|
+
onError: (err) => {
|
|
7508
6433
|
deps.log.warn(
|
|
7509
|
-
{ err:
|
|
6434
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
7510
6435
|
"dispatcher: empty-final commit failed"
|
|
7511
6436
|
);
|
|
7512
6437
|
}
|
|
@@ -7529,8 +6454,8 @@ var TurnCommitter = class {
|
|
|
7529
6454
|
if (event.type === "session" || event.type === "result") return;
|
|
7530
6455
|
try {
|
|
7531
6456
|
await this.emit(event);
|
|
7532
|
-
} catch (
|
|
7533
|
-
this.onError(
|
|
6457
|
+
} catch (err) {
|
|
6458
|
+
this.onError(err, event.type);
|
|
7534
6459
|
}
|
|
7535
6460
|
}
|
|
7536
6461
|
// End-of-turn empty-final promotion. The held-text flush is now the adapter's
|
|
@@ -7726,10 +6651,27 @@ var UNEXPECTED_ROLE_REASON = `refused the wake trigger (role "system" isn't disp
|
|
|
7726
6651
|
var SKIPPED_MARKER_BODY = "(skipped)";
|
|
7727
6652
|
var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
|
|
7728
6653
|
var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
|
|
7729
|
-
var DEFAULT_AGENT_TOTAL_TIMEOUT_MS =
|
|
6654
|
+
var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
|
|
7730
6655
|
function runKey(conversationId, agentId) {
|
|
7731
6656
|
return `${conversationId}|${agentId}`;
|
|
7732
6657
|
}
|
|
6658
|
+
function describeSubAgentError(status, body) {
|
|
6659
|
+
const code = body && typeof body === "object" && "error" in body ? String(body.error) : void 0;
|
|
6660
|
+
switch (code) {
|
|
6661
|
+
case "callout_cap_exceeded":
|
|
6662
|
+
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.";
|
|
6663
|
+
case "callout_depth_exceeded":
|
|
6664
|
+
return "sub_agent: this would nest sub-agents too deep (max 3 levels). Have the current worker report back rather than spawning another layer.";
|
|
6665
|
+
case "dispatch_agent_not_found":
|
|
6666
|
+
return "sub_agent: no live agent in this workspace matches that `agentId`. Check `list_agents`, or omit `agentId` to spawn yourself.";
|
|
6667
|
+
case "dispatch_return_requires_turn":
|
|
6668
|
+
case "dispatch_return_requires_agent":
|
|
6669
|
+
case "dispatch_return_requires_dispatch":
|
|
6670
|
+
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.`;
|
|
6671
|
+
default:
|
|
6672
|
+
return `sub_agent: the spawn failed (${code ?? `HTTP ${status}`}).`;
|
|
6673
|
+
}
|
|
6674
|
+
}
|
|
7733
6675
|
var Dispatcher = class {
|
|
7734
6676
|
constructor(opts) {
|
|
7735
6677
|
this.opts = opts;
|
|
@@ -7756,7 +6698,7 @@ var Dispatcher = class {
|
|
|
7756
6698
|
// clear it — the SJ383 `finally` after the SDK loop is the one clear, and
|
|
7757
6699
|
// every pre-run exit returns before reaching it. So each pre-run failure has
|
|
7758
6700
|
// to clear `active_run_started_at` itself, mirroring that `finally`, or the
|
|
7759
|
-
// indicator strands until the
|
|
6701
|
+
// indicator strands until the 12h age sweep.
|
|
7760
6702
|
//
|
|
7761
6703
|
// `errorReason` controls the server's duplicate-notice rule (the active-run
|
|
7762
6704
|
// PATCH handler in conversations.ts): a clear carrying `errorReason` makes the
|
|
@@ -7776,9 +6718,9 @@ var Dispatcher = class {
|
|
|
7776
6718
|
payload.agentId,
|
|
7777
6719
|
body
|
|
7778
6720
|
);
|
|
7779
|
-
} catch (
|
|
6721
|
+
} catch (err) {
|
|
7780
6722
|
turnLog.warn(
|
|
7781
|
-
{ err:
|
|
6723
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
7782
6724
|
"dispatcher: pre-run active-run clear failed terminally; server age-sweep is the backstop"
|
|
7783
6725
|
);
|
|
7784
6726
|
}
|
|
@@ -7804,16 +6746,16 @@ var Dispatcher = class {
|
|
|
7804
6746
|
payload.messageId,
|
|
7805
6747
|
turnId
|
|
7806
6748
|
);
|
|
7807
|
-
} catch (
|
|
7808
|
-
const status =
|
|
6749
|
+
} catch (err) {
|
|
6750
|
+
const status = err instanceof ApiError ? err.status : 0;
|
|
7809
6751
|
if (status === 404) {
|
|
7810
6752
|
turnLog.warn(
|
|
7811
|
-
{ err:
|
|
6753
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
7812
6754
|
"dispatcher: turn-context 404 (stale event \u2014 conversation/message/participant gone); skipping"
|
|
7813
6755
|
);
|
|
7814
6756
|
return this.concludeBeforeRun(payload, turnLog, startedAt, "turn_context_not_found");
|
|
7815
6757
|
}
|
|
7816
|
-
const reason =
|
|
6758
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
7817
6759
|
turnLog.error({ err: reason }, "dispatcher: failed to fetch turn context");
|
|
7818
6760
|
const fetchReason = `fetch_failed: ${reason}`;
|
|
7819
6761
|
return this.concludeBeforeRun(payload, turnLog, startedAt, fetchReason, fetchReason);
|
|
@@ -7876,20 +6818,11 @@ var Dispatcher = class {
|
|
|
7876
6818
|
effectiveCwd = void 0;
|
|
7877
6819
|
}
|
|
7878
6820
|
let hookEnv;
|
|
7879
|
-
let preparedNativeAssignment;
|
|
7880
6821
|
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
|
-
);
|
|
6822
|
+
const cached2 = readPrepared(workspaceId, payload.conversationId, payload.agentId);
|
|
7889
6823
|
if (cached2) {
|
|
7890
6824
|
effectiveCwd = cached2.cwd;
|
|
7891
6825
|
hookEnv = cached2.env;
|
|
7892
|
-
preparedNativeAssignment = cached2.nativeWorkAssignment;
|
|
7893
6826
|
} else {
|
|
7894
6827
|
const delayMs = this.opts.preparingRowDelayMs ?? DEFAULT_PREPARING_ROW_DELAY_MS;
|
|
7895
6828
|
let preparingStarted = false;
|
|
@@ -7904,9 +6837,9 @@ var Dispatcher = class {
|
|
|
7904
6837
|
summary: "",
|
|
7905
6838
|
phase,
|
|
7906
6839
|
seq
|
|
7907
|
-
}).catch((
|
|
6840
|
+
}).catch((err) => {
|
|
7908
6841
|
turnLog.warn(
|
|
7909
|
-
{ err:
|
|
6842
|
+
{ err: err instanceof Error ? err.message : String(err), phase },
|
|
7910
6843
|
"dispatcher: preparing-activity report failed (continuing with the hook)"
|
|
7911
6844
|
);
|
|
7912
6845
|
});
|
|
@@ -7928,25 +6861,17 @@ var Dispatcher = class {
|
|
|
7928
6861
|
// tasker prepare hook keys its per-task env off. Defaults to `[]` for
|
|
7929
6862
|
// an older API. The conversation anchor is gone (CT319).
|
|
7930
6863
|
triggerEntryPaths: turnContext.conversation.triggerEntryPaths ?? [],
|
|
7931
|
-
...turnContext.conversation.nativeWorkAssignment ? { nativeWorkAssignment: turnContext.conversation.nativeWorkAssignment } : {},
|
|
7932
6864
|
title: turnContext.conversation.title
|
|
7933
6865
|
});
|
|
7934
6866
|
clearTimeout(preparingTimer);
|
|
7935
6867
|
if (preparingStarted) reportPreparing("done");
|
|
7936
|
-
writePrepared(
|
|
7937
|
-
workspaceId,
|
|
7938
|
-
payload.conversationId,
|
|
7939
|
-
payload.agentId,
|
|
7940
|
-
result,
|
|
7941
|
-
assignmentKey
|
|
7942
|
-
);
|
|
6868
|
+
writePrepared(workspaceId, payload.conversationId, payload.agentId, result);
|
|
7943
6869
|
effectiveCwd = result.cwd;
|
|
7944
6870
|
hookEnv = result.env;
|
|
7945
|
-
|
|
7946
|
-
} catch (err2) {
|
|
6871
|
+
} catch (err) {
|
|
7947
6872
|
clearTimeout(preparingTimer);
|
|
7948
6873
|
if (preparingStarted) reportPreparing("error");
|
|
7949
|
-
const reason =
|
|
6874
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
7950
6875
|
turnLog.error({ err: reason }, "dispatcher: prepare hook failed");
|
|
7951
6876
|
try {
|
|
7952
6877
|
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
@@ -7969,6 +6894,19 @@ ${reason}`,
|
|
|
7969
6894
|
}
|
|
7970
6895
|
}
|
|
7971
6896
|
}
|
|
6897
|
+
let turnEnv = hookEnv;
|
|
6898
|
+
if (effectiveCwd && turnContext.runtime === "codex") {
|
|
6899
|
+
const tmpDir = join12(effectiveCwd, "node_modules", ".cache", "cabane-tmp", turnId);
|
|
6900
|
+
try {
|
|
6901
|
+
mkdirSync9(tmpDir, { recursive: true });
|
|
6902
|
+
turnEnv = { ...hookEnv, TMPDIR: tmpDir };
|
|
6903
|
+
} catch (err) {
|
|
6904
|
+
turnLog.warn(
|
|
6905
|
+
{ err: err instanceof Error ? err.message : String(err), tmpDir },
|
|
6906
|
+
"dispatcher: failed to create per-turn TMPDIR \u2014 proceeding with the inherited temp dir"
|
|
6907
|
+
);
|
|
6908
|
+
}
|
|
6909
|
+
}
|
|
7972
6910
|
const key = runKey(payload.conversationId, payload.agentId);
|
|
7973
6911
|
const abortController = new AbortController();
|
|
7974
6912
|
this.aborts.set(key, abortController);
|
|
@@ -7983,9 +6921,9 @@ ${reason}`,
|
|
|
7983
6921
|
// this live turn for an abandoned one and close it with a `stopped`.
|
|
7984
6922
|
turnId
|
|
7985
6923
|
});
|
|
7986
|
-
} catch (
|
|
6924
|
+
} catch (err) {
|
|
7987
6925
|
turnLog.warn(
|
|
7988
|
-
{ err:
|
|
6926
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
7989
6927
|
"dispatcher: active-run flag set failed terminally; proceeding"
|
|
7990
6928
|
);
|
|
7991
6929
|
}
|
|
@@ -8022,35 +6960,6 @@ ${reason}`,
|
|
|
8022
6960
|
subAgentCreate,
|
|
8023
6961
|
wakeState
|
|
8024
6962
|
);
|
|
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
6963
|
const request = buildCompanionTurnRequest({
|
|
8055
6964
|
turnContext,
|
|
8056
6965
|
baseUrl: this.opts.baseUrl,
|
|
@@ -8060,11 +6969,11 @@ ${reason}`,
|
|
|
8060
6969
|
...turnContext.turnToken ? { turnToken: turnContext.turnToken } : {},
|
|
8061
6970
|
// SJ524: the hook-resolved cwd overrides the static local cwd.
|
|
8062
6971
|
...effectiveCwd ? { cwd: effectiveCwd } : {},
|
|
8063
|
-
|
|
8064
|
-
|
|
6972
|
+
// CT804: `turnEnv` = the prepare-hook env plus the per-turn checkout-local
|
|
6973
|
+
// TMPDIR (falls back to `hookEnv` when no cwd was resolved).
|
|
6974
|
+
...turnEnv ? { env: turnEnv } : {},
|
|
8065
6975
|
mcpServers: resolvedMcpServers,
|
|
8066
6976
|
summonServer,
|
|
8067
|
-
turnControl: nativeTurnControl,
|
|
8068
6977
|
// CT238: this turn's conversation, forwarded as the active-conversation
|
|
8069
6978
|
// header so a cross-thread post/spawn stamps its origin.
|
|
8070
6979
|
activeConversationId: payload.conversationId,
|
|
@@ -8082,22 +6991,19 @@ ${reason}`,
|
|
|
8082
6991
|
if (this.opts.codexEnabled) {
|
|
8083
6992
|
adapters.push(createCodexAdapter({ enabled: true, onWarn }));
|
|
8084
6993
|
}
|
|
8085
|
-
if (this.opts.cabaneNativeApiKey) {
|
|
8086
|
-
adapters.push(createCabaneNativeAdapter({ apiKey: this.opts.cabaneNativeApiKey, onWarn }));
|
|
8087
|
-
}
|
|
8088
6994
|
const registry = createAdapterRegistry(adapters);
|
|
8089
6995
|
let adapter;
|
|
8090
6996
|
try {
|
|
8091
6997
|
adapter = selectAdapter(registry, turnContext.runtime);
|
|
8092
|
-
} catch (
|
|
8093
|
-
if (!(
|
|
6998
|
+
} catch (err) {
|
|
6999
|
+
if (!(err instanceof RuntimeUnavailableError)) throw err;
|
|
8094
7000
|
turnLog.error(
|
|
8095
|
-
{ runtime:
|
|
7001
|
+
{ runtime: err.runtime, available: err.available },
|
|
8096
7002
|
"dispatcher: turn runtime not available on this device"
|
|
8097
7003
|
);
|
|
8098
7004
|
try {
|
|
8099
7005
|
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
8100
|
-
body: `${RUNTIME_UNAVAILABLE_PREFIX} ${
|
|
7006
|
+
body: `${RUNTIME_UNAVAILABLE_PREFIX} ${err.message}`,
|
|
8101
7007
|
kind: "final",
|
|
8102
7008
|
turnId,
|
|
8103
7009
|
parentMessageId: payload.messageId
|
|
@@ -8112,7 +7018,7 @@ ${reason}`,
|
|
|
8112
7018
|
payload,
|
|
8113
7019
|
turnLog,
|
|
8114
7020
|
startedAt,
|
|
8115
|
-
`runtime_unavailable:${
|
|
7021
|
+
`runtime_unavailable:${err.runtime}`
|
|
8116
7022
|
);
|
|
8117
7023
|
}
|
|
8118
7024
|
if (prepareHook && hookEnv?.CABANE_TASK_ID) {
|
|
@@ -8246,9 +7152,9 @@ ${reason}`,
|
|
|
8246
7152
|
skipState.skipped = true;
|
|
8247
7153
|
skipState.reason = intent.skipReason;
|
|
8248
7154
|
}
|
|
8249
|
-
} catch (
|
|
7155
|
+
} catch (err) {
|
|
8250
7156
|
turnLog.warn(
|
|
8251
|
-
{ err:
|
|
7157
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
8252
7158
|
"dispatcher: turn-control intent fetch failed; turn-control effects for this turn are dropped"
|
|
8253
7159
|
);
|
|
8254
7160
|
}
|
|
@@ -8294,9 +7200,9 @@ ${reason}`,
|
|
|
8294
7200
|
payload.agentId,
|
|
8295
7201
|
{ agentSessionId: event.state }
|
|
8296
7202
|
);
|
|
8297
|
-
} catch (
|
|
7203
|
+
} catch (err) {
|
|
8298
7204
|
turnLog.warn(
|
|
8299
|
-
{ err:
|
|
7205
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
8300
7206
|
"dispatcher: session-id write failed (will retry next turn)"
|
|
8301
7207
|
);
|
|
8302
7208
|
}
|
|
@@ -8345,18 +7251,18 @@ ${reason}`,
|
|
|
8345
7251
|
parentMessageId: payload.messageId,
|
|
8346
7252
|
...skipWake ? { wake: skipWake } : {}
|
|
8347
7253
|
});
|
|
8348
|
-
} catch (
|
|
7254
|
+
} catch (err) {
|
|
8349
7255
|
turnLog.warn(
|
|
8350
|
-
{ err:
|
|
7256
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
8351
7257
|
"dispatcher: skipped-marker commit failed"
|
|
8352
7258
|
);
|
|
8353
7259
|
}
|
|
8354
7260
|
} else {
|
|
8355
7261
|
await committer.finalize(okResult);
|
|
8356
7262
|
}
|
|
8357
|
-
} catch (
|
|
7263
|
+
} catch (err) {
|
|
8358
7264
|
okResult = false;
|
|
8359
|
-
resultReason =
|
|
7265
|
+
resultReason = err instanceof Error ? err.message : String(err);
|
|
8360
7266
|
turnLog.error({ err: resultReason }, "dispatcher: SDK query threw");
|
|
8361
7267
|
} finally {
|
|
8362
7268
|
if (idleTimer) clearTimeout(idleTimer);
|
|
@@ -8392,9 +7298,9 @@ ${reason}`,
|
|
|
8392
7298
|
// CT113: the stopped marker is still "about" the triggering message.
|
|
8393
7299
|
parentMessageId: payload.messageId
|
|
8394
7300
|
});
|
|
8395
|
-
} catch (
|
|
7301
|
+
} catch (err) {
|
|
8396
7302
|
turnLog.warn(
|
|
8397
|
-
{ err:
|
|
7303
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
8398
7304
|
"dispatcher: stopped-marker commit failed"
|
|
8399
7305
|
);
|
|
8400
7306
|
}
|
|
@@ -8435,9 +7341,9 @@ ${reason}`,
|
|
|
8435
7341
|
payload.agentId,
|
|
8436
7342
|
body
|
|
8437
7343
|
);
|
|
8438
|
-
} catch (
|
|
7344
|
+
} catch (err) {
|
|
8439
7345
|
turnLog.warn(
|
|
8440
|
-
{ err:
|
|
7346
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
8441
7347
|
"dispatcher: active-run clear failed terminally; server age-sweep is the backstop"
|
|
8442
7348
|
);
|
|
8443
7349
|
}
|
|
@@ -8505,7 +7411,6 @@ function buildCompanionManifest(opts) {
|
|
|
8505
7411
|
if (opts.claudeCode) runtimes.push({ name: "claude-code", version: v.claudeCode ?? null });
|
|
8506
7412
|
if (opts.opencode) runtimes.push({ name: "opencode", version: v.opencode ?? null });
|
|
8507
7413
|
if (opts.codex) runtimes.push({ name: "codex", version: v.codex ?? null });
|
|
8508
|
-
if (opts.cabaneNative) runtimes.push({ name: "cabane-native", version: null });
|
|
8509
7414
|
return { runtimes, capabilities: { ...DEVICE_MANIFEST.capabilities } };
|
|
8510
7415
|
}
|
|
8511
7416
|
|
|
@@ -8628,8 +7533,8 @@ async function probeHarnessSignals(cfg, deps = {}) {
|
|
|
8628
7533
|
return {
|
|
8629
7534
|
claudeOnPath: claudeOnPathResult,
|
|
8630
7535
|
claudeVersion,
|
|
8631
|
-
// A parseable `codex --version` is our presence signal (
|
|
8632
|
-
//
|
|
7536
|
+
// A parseable `codex --version` is our presence signal (presence alone never
|
|
7537
|
+
// exposes codex; its config flag is the manifest gate either way).
|
|
8633
7538
|
codexOnPath: codexVersion !== null,
|
|
8634
7539
|
codexVersion,
|
|
8635
7540
|
codexEnabled: isCodexEnabled(cfg),
|
|
@@ -8743,13 +7648,13 @@ var Outbox = class {
|
|
|
8743
7648
|
try {
|
|
8744
7649
|
writeFileSync7(tmp, JSON.stringify(entry) + "\n", "utf8");
|
|
8745
7650
|
renameSync3(tmp, target);
|
|
8746
|
-
} catch (
|
|
7651
|
+
} catch (err) {
|
|
8747
7652
|
try {
|
|
8748
7653
|
rmSync5(tmp, { force: true });
|
|
8749
7654
|
} catch {
|
|
8750
7655
|
}
|
|
8751
7656
|
this.log?.warn(
|
|
8752
|
-
{ workspaceId: this.workspaceId, err:
|
|
7657
|
+
{ workspaceId: this.workspaceId, err: err instanceof Error ? err.message : String(err) },
|
|
8753
7658
|
"companion outbox: failed to persist entry"
|
|
8754
7659
|
);
|
|
8755
7660
|
return;
|
|
@@ -8846,40 +7751,43 @@ var Outbox = class {
|
|
|
8846
7751
|
};
|
|
8847
7752
|
|
|
8848
7753
|
// src/run-config.ts
|
|
8849
|
-
import { z as
|
|
8850
|
-
var mcpStdioServerSchema =
|
|
8851
|
-
type:
|
|
8852
|
-
command:
|
|
8853
|
-
args:
|
|
8854
|
-
env:
|
|
7754
|
+
import { z as z14 } from "zod";
|
|
7755
|
+
var mcpStdioServerSchema = z14.object({
|
|
7756
|
+
type: z14.literal("stdio").optional(),
|
|
7757
|
+
command: z14.string().min(1),
|
|
7758
|
+
args: z14.array(z14.string()).optional(),
|
|
7759
|
+
env: z14.record(z14.string(), z14.string()).optional()
|
|
8855
7760
|
});
|
|
8856
|
-
var mcpHttpServerSchema =
|
|
8857
|
-
type:
|
|
8858
|
-
url:
|
|
8859
|
-
headers:
|
|
7761
|
+
var mcpHttpServerSchema = z14.object({
|
|
7762
|
+
type: z14.literal("http"),
|
|
7763
|
+
url: z14.string().url(),
|
|
7764
|
+
headers: z14.record(z14.string(), z14.string()).optional()
|
|
8860
7765
|
});
|
|
8861
|
-
var mcpSseServerSchema =
|
|
8862
|
-
type:
|
|
8863
|
-
url:
|
|
8864
|
-
headers:
|
|
7766
|
+
var mcpSseServerSchema = z14.object({
|
|
7767
|
+
type: z14.literal("sse"),
|
|
7768
|
+
url: z14.string().url(),
|
|
7769
|
+
headers: z14.record(z14.string(), z14.string()).optional()
|
|
8865
7770
|
});
|
|
8866
|
-
var mcpServerDefSchema =
|
|
7771
|
+
var mcpServerDefSchema = z14.union([
|
|
8867
7772
|
mcpHttpServerSchema,
|
|
8868
7773
|
mcpSseServerSchema,
|
|
8869
7774
|
mcpStdioServerSchema
|
|
8870
7775
|
]);
|
|
8871
|
-
var thinkingConfigSchema =
|
|
8872
|
-
|
|
8873
|
-
|
|
8874
|
-
|
|
7776
|
+
var thinkingConfigSchema = z14.discriminatedUnion("type", [
|
|
7777
|
+
z14.object({ type: z14.literal("adaptive") }),
|
|
7778
|
+
z14.object({ type: z14.literal("enabled"), budgetTokens: z14.number().int().positive().optional() }),
|
|
7779
|
+
z14.object({ type: z14.literal("disabled") })
|
|
8875
7780
|
]);
|
|
8876
|
-
var effortSchema =
|
|
8877
|
-
var runConfigSchema =
|
|
8878
|
-
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
|
|
7781
|
+
var effortSchema = z14.enum(["low", "medium", "high", "xhigh", "max"]);
|
|
7782
|
+
var runConfigSchema = z14.object({
|
|
7783
|
+
// CT788: the host-access binary replaced the `assistant`/`coding`/`custom` mode
|
|
7784
|
+
// trio + its custom tool lists — `true` grants the host filesystem/shell, absent
|
|
7785
|
+
// is the locked surface. Kept in lockstep with `@cabane/shared`'s
|
|
7786
|
+
// `agentRunConfigSchema` (independent zod, forward-compatible: unknown keys are
|
|
7787
|
+
// stripped, so an older companion riding a newer server never rejects the config).
|
|
7788
|
+
hostAccess: z14.boolean().optional(),
|
|
7789
|
+
mcpServers: z14.record(z14.string(), mcpServerDefSchema).optional(),
|
|
7790
|
+
model: z14.string().min(1).optional(),
|
|
8883
7791
|
thinking: thinkingConfigSchema.optional(),
|
|
8884
7792
|
effort: effortSchema.optional()
|
|
8885
7793
|
});
|
|
@@ -8924,20 +7832,20 @@ var SseSubscriber = class {
|
|
|
8924
7832
|
try {
|
|
8925
7833
|
await this.connect();
|
|
8926
7834
|
backoff = 500;
|
|
8927
|
-
} catch (
|
|
7835
|
+
} catch (err) {
|
|
8928
7836
|
if (this.aborted) return;
|
|
8929
|
-
if (
|
|
7837
|
+
if (err instanceof ApiError && (err.status === 401 || err.status === 403)) {
|
|
8930
7838
|
this.opts.log.error(
|
|
8931
|
-
{ workspaceId: this.opts.workspaceId, status:
|
|
7839
|
+
{ workspaceId: this.opts.workspaceId, status: err.status },
|
|
8932
7840
|
"SSE auth failed \u2014 tearing down this workspace subscriber"
|
|
8933
7841
|
);
|
|
8934
|
-
this.opts.onAuthFailure(
|
|
7842
|
+
this.opts.onAuthFailure(err.status);
|
|
8935
7843
|
return;
|
|
8936
7844
|
}
|
|
8937
7845
|
this.opts.log.warn(
|
|
8938
7846
|
{
|
|
8939
7847
|
workspaceId: this.opts.workspaceId,
|
|
8940
|
-
err:
|
|
7848
|
+
err: err instanceof Error ? err.message : String(err),
|
|
8941
7849
|
backoff
|
|
8942
7850
|
},
|
|
8943
7851
|
"SSE disconnected; reconnecting"
|
|
@@ -9080,7 +7988,7 @@ var CompanionSupervisor = class {
|
|
|
9080
7988
|
if (!this.config.deviceToken) {
|
|
9081
7989
|
this.log.warn("companion: not paired (no device token) \u2014 run `cabane-companion pair`");
|
|
9082
7990
|
process.stdout.write(
|
|
9083
|
-
"companion: this device is not paired \u2014 run `cabane-companion pair
|
|
7991
|
+
"companion: this device is not paired \u2014 run `cabane-companion pair`, then confirm the short code in Settings \u2192 Connectors.\n"
|
|
9084
7992
|
);
|
|
9085
7993
|
return;
|
|
9086
7994
|
}
|
|
@@ -9135,9 +8043,6 @@ var CompanionSupervisor = class {
|
|
|
9135
8043
|
// like opencode — the CLI's presence is the operator's responsibility;
|
|
9136
8044
|
// a misconfigured device fails the turn loudly, never silently).
|
|
9137
8045
|
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
8046
|
// CT571/CT586: each runtime's `version` from the latest harness probe
|
|
9142
8047
|
// (fail-soft to null). Informational only — the server matches on name.
|
|
9143
8048
|
versions: this.harnessVersions
|
|
@@ -9155,9 +8060,9 @@ var CompanionSupervisor = class {
|
|
|
9155
8060
|
this.hub.setDevice({ deviceId: res.deviceId });
|
|
9156
8061
|
this.deviceId = res.deviceId;
|
|
9157
8062
|
this.checkVersionSkew(res.serverVersion);
|
|
9158
|
-
} catch (
|
|
8063
|
+
} catch (err) {
|
|
9159
8064
|
this.log.warn(
|
|
9160
|
-
{ err:
|
|
8065
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
9161
8066
|
"companion: device heartbeat failed (will retry on next tick)"
|
|
9162
8067
|
);
|
|
9163
8068
|
}
|
|
@@ -9193,12 +8098,12 @@ var CompanionSupervisor = class {
|
|
|
9193
8098
|
const resp = await this.deviceApi.getAssignments();
|
|
9194
8099
|
items = resp.assignments;
|
|
9195
8100
|
device = resp.device;
|
|
9196
|
-
} catch (
|
|
8101
|
+
} catch (err) {
|
|
9197
8102
|
this.log.error(
|
|
9198
|
-
{ err:
|
|
8103
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
9199
8104
|
"companion: assignments pull failed \u2014 check the device is still active in the cabane app"
|
|
9200
8105
|
);
|
|
9201
|
-
this.hub.setDeviceError(
|
|
8106
|
+
this.hub.setDeviceError(err instanceof Error ? err.message : String(err));
|
|
9202
8107
|
return;
|
|
9203
8108
|
}
|
|
9204
8109
|
this.hub.setDevice({ deviceId: device.id, deviceLabel: device.label });
|
|
@@ -9275,7 +8180,7 @@ var CompanionSupervisor = class {
|
|
|
9275
8180
|
agentId: it.agentId,
|
|
9276
8181
|
username: it.agentUsername,
|
|
9277
8182
|
displayName: it.agentDisplayName,
|
|
9278
|
-
mode: runConfig.
|
|
8183
|
+
mode: runConfig.hostAccess ? "full" : "none",
|
|
9279
8184
|
hasCredential: false,
|
|
9280
8185
|
missingSecrets: missing
|
|
9281
8186
|
});
|
|
@@ -9295,7 +8200,7 @@ var CompanionSupervisor = class {
|
|
|
9295
8200
|
agentId: it.agentId,
|
|
9296
8201
|
username: it.agentUsername,
|
|
9297
8202
|
displayName: it.agentDisplayName,
|
|
9298
|
-
mode: runConfig.
|
|
8203
|
+
mode: runConfig.hostAccess ? "full" : "none",
|
|
9299
8204
|
hasCredential: true,
|
|
9300
8205
|
missingSecrets: missing
|
|
9301
8206
|
});
|
|
@@ -9379,12 +8284,9 @@ var CompanionSupervisor = class {
|
|
|
9379
8284
|
// CT481: register the codex adapter when this device offers codex; unset
|
|
9380
8285
|
// leaves an `openai/…` turn to fail loudly (no silent claude-code fallback).
|
|
9381
8286
|
...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
8287
|
// CT556: per-turn timeout watchdog windows, from the companion's own env
|
|
9386
8288
|
// (`AGENT_IDLE_TIMEOUT_MS` / `AGENT_TOTAL_TIMEOUT_MS`). Unset → the
|
|
9387
|
-
// dispatcher's baked-in defaults (10 min idle /
|
|
8289
|
+
// dispatcher's baked-in defaults (10 min idle / 6h total).
|
|
9388
8290
|
...positiveIntEnv("AGENT_IDLE_TIMEOUT_MS") !== void 0 ? { idleTimeoutMs: positiveIntEnv("AGENT_IDLE_TIMEOUT_MS") } : {},
|
|
9389
8291
|
...positiveIntEnv("AGENT_TOTAL_TIMEOUT_MS") !== void 0 ? { totalTimeoutMs: positiveIntEnv("AGENT_TOTAL_TIMEOUT_MS") } : {},
|
|
9390
8292
|
observer: this.hub.observerFor(ctx.workspaceId, ctx.agentId, ctx.workspaceSlug),
|
|
@@ -9451,9 +8353,9 @@ var CompanionSupervisor = class {
|
|
|
9451
8353
|
let wire;
|
|
9452
8354
|
try {
|
|
9453
8355
|
wire = JSON.parse(ev.data);
|
|
9454
|
-
} catch (
|
|
8356
|
+
} catch (err) {
|
|
9455
8357
|
this.log.warn(
|
|
9456
|
-
{ err:
|
|
8358
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
9457
8359
|
"malformed SSE payload"
|
|
9458
8360
|
);
|
|
9459
8361
|
return;
|
|
@@ -9500,13 +8402,13 @@ var CompanionSupervisor = class {
|
|
|
9500
8402
|
}
|
|
9501
8403
|
const chainKey = `${payload.conversationId}|${payload.agentId}`;
|
|
9502
8404
|
const prev = wr.chains.get(chainKey) ?? Promise.resolve();
|
|
9503
|
-
const tail = prev.then(() => this.runDispatch(wr, ev, payload, agent)).catch((
|
|
8405
|
+
const tail = prev.then(() => this.runDispatch(wr, ev, payload, agent)).catch((err) => {
|
|
9504
8406
|
this.log.warn(
|
|
9505
8407
|
{
|
|
9506
8408
|
workspaceId: wr.workspaceId,
|
|
9507
8409
|
conversationId: payload.conversationId,
|
|
9508
8410
|
agentId: payload.agentId,
|
|
9509
|
-
err:
|
|
8411
|
+
err: err instanceof Error ? err.message : String(err)
|
|
9510
8412
|
},
|
|
9511
8413
|
"companion: conversation turn handler threw"
|
|
9512
8414
|
);
|
|
@@ -9598,10 +8500,10 @@ var CompanionSupervisor = class {
|
|
|
9598
8500
|
} else {
|
|
9599
8501
|
drainDelay = DRAIN_BASE_MS;
|
|
9600
8502
|
}
|
|
9601
|
-
} catch (
|
|
8503
|
+
} catch (err) {
|
|
9602
8504
|
drainDelay = Math.min(drainDelay * 2, DRAIN_MAX_MS);
|
|
9603
8505
|
this.log.warn(
|
|
9604
|
-
{ agentId, err:
|
|
8506
|
+
{ agentId, err: err instanceof Error ? err.message : String(err) },
|
|
9605
8507
|
"companion: outbox drain pass threw (will retry with backoff)"
|
|
9606
8508
|
);
|
|
9607
8509
|
} finally {
|
|
@@ -9668,9 +8570,9 @@ var CompanionSupervisor = class {
|
|
|
9668
8570
|
codex: signals.codexVersion
|
|
9669
8571
|
};
|
|
9670
8572
|
this.hub.setHarnesses(deriveHarnessSnapshot(signals).harnesses);
|
|
9671
|
-
} catch (
|
|
8573
|
+
} catch (err) {
|
|
9672
8574
|
this.log.warn(
|
|
9673
|
-
{ err:
|
|
8575
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
9674
8576
|
"companion: harness probe failed (will retry on next beat)"
|
|
9675
8577
|
);
|
|
9676
8578
|
}
|
|
@@ -9768,10 +8670,13 @@ var CompanionSupervisor = class {
|
|
|
9768
8670
|
await Promise.race([
|
|
9769
8671
|
Promise.allSettled(turns),
|
|
9770
8672
|
new Promise((resolve) => {
|
|
9771
|
-
timer = setTimeout(
|
|
9772
|
-
|
|
9773
|
-
|
|
9774
|
-
|
|
8673
|
+
timer = setTimeout(
|
|
8674
|
+
() => {
|
|
8675
|
+
timedOut = true;
|
|
8676
|
+
resolve();
|
|
8677
|
+
},
|
|
8678
|
+
Math.max(0, graceMs)
|
|
8679
|
+
);
|
|
9775
8680
|
timer.unref?.();
|
|
9776
8681
|
})
|
|
9777
8682
|
]);
|
|
@@ -9825,17 +8730,17 @@ var RECOVERABLE_CODES = /* @__PURE__ */ new Set([
|
|
|
9825
8730
|
"ERR_STREAM_DESTROYED",
|
|
9826
8731
|
"ERR_STREAM_WRITE_AFTER_END"
|
|
9827
8732
|
]);
|
|
9828
|
-
function errorCode(
|
|
9829
|
-
if (
|
|
9830
|
-
const code =
|
|
8733
|
+
function errorCode(err) {
|
|
8734
|
+
if (err && typeof err === "object" && "code" in err) {
|
|
8735
|
+
const code = err.code;
|
|
9831
8736
|
if (typeof code === "string") return code;
|
|
9832
8737
|
}
|
|
9833
8738
|
return void 0;
|
|
9834
8739
|
}
|
|
9835
|
-
function isRecoverableSocketError(
|
|
9836
|
-
const code = errorCode(
|
|
8740
|
+
function isRecoverableSocketError(err) {
|
|
8741
|
+
const code = errorCode(err);
|
|
9837
8742
|
if (code && RECOVERABLE_CODES.has(code)) return true;
|
|
9838
|
-
const message =
|
|
8743
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
9839
8744
|
return /\bEPIPE\b|\bECONNRESET\b/.test(message);
|
|
9840
8745
|
}
|
|
9841
8746
|
function installProcessSafetyNet(log, opts = {}) {
|
|
@@ -9844,16 +8749,16 @@ function installProcessSafetyNet(log, opts = {}) {
|
|
|
9844
8749
|
stream.on("error", () => {
|
|
9845
8750
|
});
|
|
9846
8751
|
}
|
|
9847
|
-
proc.on("uncaughtException", (
|
|
8752
|
+
proc.on("uncaughtException", (err) => handleUncaught(log, err, "uncaughtException"));
|
|
9848
8753
|
proc.on(
|
|
9849
8754
|
"unhandledRejection",
|
|
9850
8755
|
(reason) => handleUncaught(log, reason, "unhandledRejection")
|
|
9851
8756
|
);
|
|
9852
8757
|
}
|
|
9853
|
-
function handleUncaught(log,
|
|
9854
|
-
const message =
|
|
9855
|
-
const code = errorCode(
|
|
9856
|
-
if (isRecoverableSocketError(
|
|
8758
|
+
function handleUncaught(log, err, origin) {
|
|
8759
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
8760
|
+
const code = errorCode(err);
|
|
8761
|
+
if (isRecoverableSocketError(err)) {
|
|
9857
8762
|
log.warn(
|
|
9858
8763
|
{ origin, code, err: message },
|
|
9859
8764
|
"companion: recovered from a socket error (kept running \u2014 a broken pipe never kills the companion)"
|
|
@@ -9861,7 +8766,7 @@ function handleUncaught(log, err2, origin) {
|
|
|
9861
8766
|
return;
|
|
9862
8767
|
}
|
|
9863
8768
|
log.error(
|
|
9864
|
-
{ origin, code, err: message, stack:
|
|
8769
|
+
{ origin, code, err: message, stack: err instanceof Error ? err.stack : void 0 },
|
|
9865
8770
|
"companion: uncaught error (kept running \u2014 see the stack above)"
|
|
9866
8771
|
);
|
|
9867
8772
|
}
|
|
@@ -9898,14 +8803,14 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
9898
8803
|
cfg = requireConfig();
|
|
9899
8804
|
claudeCode = await probeClaude();
|
|
9900
8805
|
await ensureRuntimeAvailable(cfg, { probeClaude: async () => claudeCode });
|
|
9901
|
-
} catch (
|
|
8806
|
+
} catch (err) {
|
|
9902
8807
|
recordCrash({
|
|
9903
|
-
reason:
|
|
9904
|
-
...errorCode(
|
|
8808
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
8809
|
+
...errorCode(err) ? { code: errorCode(err) } : {},
|
|
9905
8810
|
origin: "startup",
|
|
9906
8811
|
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
9907
8812
|
});
|
|
9908
|
-
throw
|
|
8813
|
+
throw err;
|
|
9909
8814
|
}
|
|
9910
8815
|
if (cfg.logLevel) log.level = cfg.logLevel;
|
|
9911
8816
|
const harnessVersions = await probeHarnessVersions({
|