@cabane/companion 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -7
- package/dist/cli.js +523 -1696
- package/dist/pairing-config.js +29 -53
- package/dist/runtime.js +441 -1546
- package/dist/static/index.html +2 -3
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
} from "fs";
|
|
21
21
|
import { homedir, userInfo } from "os";
|
|
22
22
|
import { dirname, join } from "path";
|
|
23
|
-
import { z as
|
|
23
|
+
import { z as z2 } from "zod";
|
|
24
24
|
|
|
25
25
|
// src/errors.ts
|
|
26
26
|
var CompanionError = class extends Error {
|
|
@@ -53,9 +53,6 @@ var PrepareHookError = class extends CompanionError {
|
|
|
53
53
|
};
|
|
54
54
|
|
|
55
55
|
// src/pairing.ts
|
|
56
|
-
import { z } from "zod";
|
|
57
|
-
var PAIRING_VERSION = 1;
|
|
58
|
-
var DEVICE_TOKEN_PREFIX = "cabdev_";
|
|
59
56
|
function isAllowedBaseUrl(raw) {
|
|
60
57
|
let url;
|
|
61
58
|
try {
|
|
@@ -70,78 +67,24 @@ function isAllowedBaseUrl(raw) {
|
|
|
70
67
|
}
|
|
71
68
|
return false;
|
|
72
69
|
}
|
|
73
|
-
var pairingSchema = z.object({
|
|
74
|
-
// Bumped if the wire shape changes incompatibly. We only accept v1.
|
|
75
|
-
v: z.literal(PAIRING_VERSION),
|
|
76
|
-
baseUrl: z.string().url().refine(isAllowedBaseUrl, {
|
|
77
|
-
message: "baseUrl must be https (loopback http is allowed for local dev only)"
|
|
78
|
-
}),
|
|
79
|
-
// The `cabdev_` device token plaintext — the companion's one durable credential.
|
|
80
|
-
deviceToken: z.string().min(1).startsWith(DEVICE_TOKEN_PREFIX, {
|
|
81
|
-
message: 'deviceToken must be a cabane device token (starts with "cabdev_")'
|
|
82
|
-
}),
|
|
83
|
-
// Optional identity hints the app may include for nicer local display. The
|
|
84
|
-
// companion also learns these from the first assignments pull, so they're not
|
|
85
|
-
// required.
|
|
86
|
-
deviceId: z.string().min(1).optional(),
|
|
87
|
-
deviceLabel: z.string().min(1).optional()
|
|
88
|
-
});
|
|
89
|
-
function decodePairing(raw) {
|
|
90
|
-
const cleaned = raw.replace(/\s+/g, "");
|
|
91
|
-
if (cleaned.length === 0) {
|
|
92
|
-
throw new CompanionError("empty pairing string. Copy it from the cabane app and try again.");
|
|
93
|
-
}
|
|
94
|
-
const bytes = Buffer.from(cleaned, "base64url");
|
|
95
|
-
const json = bytes.toString("utf8");
|
|
96
|
-
let parsed;
|
|
97
|
-
try {
|
|
98
|
-
parsed = JSON.parse(json);
|
|
99
|
-
} catch {
|
|
100
|
-
throw new CompanionError(
|
|
101
|
-
`that doesn't look like a complete pairing string \u2014 received ${cleaned.length} characters that decoded to ${bytes.length} bytes, but they weren't valid JSON. Copy the WHOLE block from the cabane app (a partial or truncated copy is the usual cause).`
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
if (parsed && typeof parsed === "object" && "v" in parsed && parsed.v !== PAIRING_VERSION) {
|
|
105
|
-
throw new CompanionError(
|
|
106
|
-
`this pairing string is version ${String(parsed.v)}, but this companion only understands version ${PAIRING_VERSION}. Update cabane-companion (\`git pull\` + rebuild) and try again.`
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
const result = pairingSchema.safeParse(parsed);
|
|
110
|
-
if (!result.success) {
|
|
111
|
-
if (result.error.issues.some((i) => i.path[0] === "baseUrl")) {
|
|
112
|
-
throw new CompanionError(
|
|
113
|
-
"this pairing string points at a non-https cabane URL. The companion runs the server's prompt with permissions bypassed and sends its credentials over the same channel, so it refuses plaintext http (except localhost for local dev). Use an https base URL."
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
throw new CompanionError(
|
|
117
|
-
`the pairing string is missing or malformed fields: ${result.error.issues.map((i) => i.path.join(".") || "(root)").join(", ")}. Re-copy it from the cabane app.`
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
return result.data;
|
|
121
|
-
}
|
|
122
70
|
|
|
123
71
|
// src/prepare-hook.ts
|
|
124
72
|
import { spawn } from "child_process";
|
|
125
|
-
import { z
|
|
126
|
-
var prepareHookSchema =
|
|
127
|
-
command:
|
|
128
|
-
args:
|
|
73
|
+
import { z } from "zod";
|
|
74
|
+
var prepareHookSchema = z.object({
|
|
75
|
+
command: z.string().min(1),
|
|
76
|
+
args: z.array(z.string()).optional(),
|
|
129
77
|
// Extra env handed to the hook process itself (merged over process.env).
|
|
130
|
-
env:
|
|
78
|
+
env: z.record(z.string(), z.string()).optional(),
|
|
131
79
|
// Wall-clock cap for the hook. Provisioning is slow (minutes), so the
|
|
132
80
|
// default is generous; a hook that hangs past this is killed and the turn
|
|
133
81
|
// fails with a clear timeout message rather than pinning the companion.
|
|
134
|
-
timeoutMs:
|
|
82
|
+
timeoutMs: z.number().int().positive().optional()
|
|
135
83
|
}).strict();
|
|
136
84
|
var DEFAULT_TIMEOUT_MS = 10 * 6e4;
|
|
137
|
-
var prepareResultSchema =
|
|
138
|
-
cwd:
|
|
139
|
-
env:
|
|
140
|
-
nativeWorkAssignment: z2.object({
|
|
141
|
-
itemId: z2.string(),
|
|
142
|
-
executionId: z2.string(),
|
|
143
|
-
activationEpoch: z2.number().int().nonnegative()
|
|
144
|
-
}).strict().optional()
|
|
85
|
+
var prepareResultSchema = z.object({
|
|
86
|
+
cwd: z.string().min(1),
|
|
87
|
+
env: z.record(z.string(), z.string()).optional()
|
|
145
88
|
});
|
|
146
89
|
function parsePrepareOutput(stdout) {
|
|
147
90
|
const last = stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0).at(-1);
|
|
@@ -160,13 +103,12 @@ function parsePrepareOutput(stdout) {
|
|
|
160
103
|
const r = prepareResultSchema.safeParse(parsed);
|
|
161
104
|
if (!r.success) {
|
|
162
105
|
throw new PrepareHookError(
|
|
163
|
-
'prepare hook JSON must carry a non-empty string "cwd" (plus optional "env"
|
|
106
|
+
'prepare hook JSON must carry a non-empty string "cwd" (plus optional "env")'
|
|
164
107
|
);
|
|
165
108
|
}
|
|
166
109
|
return {
|
|
167
110
|
cwd: r.data.cwd,
|
|
168
|
-
...r.data.env ? { env: r.data.env } : {}
|
|
169
|
-
...r.data.nativeWorkAssignment ? { nativeWorkAssignment: r.data.nativeWorkAssignment } : {}
|
|
111
|
+
...r.data.env ? { env: r.data.env } : {}
|
|
170
112
|
};
|
|
171
113
|
}
|
|
172
114
|
return { cwd: last };
|
|
@@ -204,10 +146,10 @@ var runPrepareHook = (hook, input) => {
|
|
|
204
146
|
CABANE_CONVERSATION_TITLE: input.title ?? ""
|
|
205
147
|
}
|
|
206
148
|
});
|
|
207
|
-
} catch (
|
|
149
|
+
} catch (err) {
|
|
208
150
|
reject(
|
|
209
151
|
new PrepareHookError(
|
|
210
|
-
`prepare hook failed to start: ${
|
|
152
|
+
`prepare hook failed to start: ${err instanceof Error ? err.message : String(err)}`
|
|
211
153
|
)
|
|
212
154
|
);
|
|
213
155
|
return;
|
|
@@ -227,8 +169,8 @@ var runPrepareHook = (hook, input) => {
|
|
|
227
169
|
child.stderr?.on("data", (d) => {
|
|
228
170
|
stderr += d.toString();
|
|
229
171
|
});
|
|
230
|
-
child.on("error", (
|
|
231
|
-
finish(() => reject(new PrepareHookError(`prepare hook failed to start: ${
|
|
172
|
+
child.on("error", (err) => {
|
|
173
|
+
finish(() => reject(new PrepareHookError(`prepare hook failed to start: ${err.message}`)));
|
|
232
174
|
});
|
|
233
175
|
child.on("close", (code) => {
|
|
234
176
|
finish(() => {
|
|
@@ -241,8 +183,8 @@ var runPrepareHook = (hook, input) => {
|
|
|
241
183
|
}
|
|
242
184
|
try {
|
|
243
185
|
resolve(parsePrepareOutput(stdout));
|
|
244
|
-
} catch (
|
|
245
|
-
reject(
|
|
186
|
+
} catch (err) {
|
|
187
|
+
reject(err instanceof PrepareHookError ? err : new PrepareHookError(String(err)));
|
|
246
188
|
}
|
|
247
189
|
});
|
|
248
190
|
});
|
|
@@ -281,8 +223,8 @@ function cabaneDir() {
|
|
|
281
223
|
function configPath() {
|
|
282
224
|
return join(cabaneDir(), "config.json");
|
|
283
225
|
}
|
|
284
|
-
var localAgentConfigSchema =
|
|
285
|
-
cwd:
|
|
226
|
+
var localAgentConfigSchema = z2.object({
|
|
227
|
+
cwd: z2.string().optional(),
|
|
286
228
|
prepareHook: prepareHookSchema.optional(),
|
|
287
229
|
// CT289: the Claude Code auto-memory escape hatch. Auto-memory is forced OFF
|
|
288
230
|
// by default on every companion (memory belongs in the Cabane workspace, and a
|
|
@@ -291,13 +233,13 @@ var localAgentConfigSchema = z3.object({
|
|
|
291
233
|
// auto-memory back to your own `~/.claude/settings.json` — Cabane then stops
|
|
292
234
|
// injecting the off switch and your normal Claude Code memory workflow applies
|
|
293
235
|
// (in coding mode, where the checkout's project settings are read).
|
|
294
|
-
claudeCode:
|
|
236
|
+
claudeCode: z2.object({ autoMemory: z2.boolean().optional() }).strict().optional()
|
|
295
237
|
}).strict();
|
|
296
|
-
var companionConfigSchema =
|
|
238
|
+
var companionConfigSchema = z2.object({
|
|
297
239
|
// The cabane instance this device is paired with. SJ515: https-enforced
|
|
298
240
|
// (loopback exempt) so a hand-edited config can't smuggle a plaintext-http
|
|
299
241
|
// base URL onto the MITM-able channel the device token + prompt ride.
|
|
300
|
-
baseUrl:
|
|
242
|
+
baseUrl: z2.string().url().refine(isAllowedBaseUrl, {
|
|
301
243
|
message: "baseUrl must be https (loopback http is allowed for local dev only)"
|
|
302
244
|
}),
|
|
303
245
|
// The `cabdev_` device token plaintext — the companion's one durable credential,
|
|
@@ -305,22 +247,22 @@ var companionConfigSchema = z3.object({
|
|
|
305
247
|
// heartbeat endpoints. Optional so `cabane-companion logout` can strip it (a
|
|
306
248
|
// "paired but logged out" state the supervisor refuses to run) while keeping
|
|
307
249
|
// the rest of the config; `pair` always writes one.
|
|
308
|
-
deviceToken:
|
|
309
|
-
// Identity hints, learned from the
|
|
250
|
+
deviceToken: z2.string().optional(),
|
|
251
|
+
// Identity hints, learned from the device flow and refreshed on the first
|
|
310
252
|
// assignments pull. Cosmetic — used for `status`/dashboard display only.
|
|
311
|
-
deviceId:
|
|
312
|
-
deviceLabel:
|
|
253
|
+
deviceId: z2.string().optional(),
|
|
254
|
+
deviceLabel: z2.string().optional(),
|
|
313
255
|
// Optional per-agent machine-local overrides (cwd / prepareHook), keyed by
|
|
314
256
|
// agentId / username / `slug/username`. Hand-added by the operator; the companion
|
|
315
257
|
// never writes this (it only persists credentials + the device, elsewhere).
|
|
316
|
-
agents:
|
|
258
|
+
agents: z2.record(z2.string(), localAgentConfigSchema).optional(),
|
|
317
259
|
// Dashboard settings (all optional). dashboardPort: preferred bind port (next
|
|
318
260
|
// free one if taken); autoOpen: whether `start` opens the browser (the
|
|
319
261
|
// `--no-open` flag / `COMPANION_NO_OPEN=1` override per-run); logLevel: pino
|
|
320
262
|
// level, live-editable from the dashboard settings panel.
|
|
321
|
-
dashboardPort:
|
|
322
|
-
autoOpen:
|
|
323
|
-
logLevel:
|
|
263
|
+
dashboardPort: z2.number().int().min(1).max(65535).optional(),
|
|
264
|
+
autoOpen: z2.boolean().optional(),
|
|
265
|
+
logLevel: z2.enum(["warn", "info", "debug"]).optional(),
|
|
324
266
|
// CT270: the opencode runtime, when the operator runs one on this machine. The
|
|
325
267
|
// operator installs opencode, starts `opencode serve` (auth via opencode's own
|
|
326
268
|
// `/connect` — Cabane never sees provider keys), and points the companion at it
|
|
@@ -328,8 +270,8 @@ var companionConfigSchema = z3.object({
|
|
|
328
270
|
// heartbeat manifest (so the server offers DeepSeek/opencode models here and
|
|
329
271
|
// routes those turns to this device) AND registers the opencode adapter in the
|
|
330
272
|
// dispatcher. Absent → the device is claude-code-only, exactly as before.
|
|
331
|
-
opencode:
|
|
332
|
-
serverUrl:
|
|
273
|
+
opencode: z2.object({
|
|
274
|
+
serverUrl: z2.string().url()
|
|
333
275
|
}).strict().optional(),
|
|
334
276
|
// CT481: the codex runtime, when the operator runs Codex on this machine. Unlike
|
|
335
277
|
// opencode (a long-lived `opencode serve` addressed by URL), Codex is a local CLI
|
|
@@ -340,20 +282,13 @@ var companionConfigSchema = z3.object({
|
|
|
340
282
|
// keeps the block but turns it off). Enabling makes the device advertise the
|
|
341
283
|
// `codex` runtime on its heartbeat manifest AND registers the codex adapter in
|
|
342
284
|
// the dispatcher. Absent → the device doesn't offer codex, exactly as before.
|
|
343
|
-
codex:
|
|
344
|
-
enabled:
|
|
285
|
+
codex: z2.object({
|
|
286
|
+
enabled: z2.boolean().optional()
|
|
345
287
|
}).strict().optional()
|
|
346
288
|
});
|
|
347
289
|
function isCodexEnabled(cfg) {
|
|
348
290
|
return !!cfg.codex && cfg.codex.enabled !== false;
|
|
349
291
|
}
|
|
350
|
-
function cabaneNativeApiKey() {
|
|
351
|
-
const key = process.env.OPENROUTER_API_KEY?.trim();
|
|
352
|
-
return key ? key : void 0;
|
|
353
|
-
}
|
|
354
|
-
function isCabaneNativeEnabled() {
|
|
355
|
-
return cabaneNativeApiKey() !== void 0;
|
|
356
|
-
}
|
|
357
292
|
function localAgentConfig(cfg, agent) {
|
|
358
293
|
const map = cfg.agents ?? {};
|
|
359
294
|
return map[agent.agentId] ?? map[agent.agentUsername] ?? map[`${agent.workspaceSlug}/${agent.agentUsername}`] ?? {};
|
|
@@ -364,18 +299,18 @@ function loadConfig() {
|
|
|
364
299
|
let raw;
|
|
365
300
|
try {
|
|
366
301
|
raw = readFileSync(path3, "utf8");
|
|
367
|
-
} catch (
|
|
302
|
+
} catch (err) {
|
|
368
303
|
throw new ConfigError(
|
|
369
|
-
`couldn't read ${path3}: ${
|
|
304
|
+
`couldn't read ${path3}: ${err instanceof Error ? err.message : String(err)}`
|
|
370
305
|
);
|
|
371
306
|
}
|
|
372
307
|
if (raw.trim().length === 0) return null;
|
|
373
308
|
let parsed;
|
|
374
309
|
try {
|
|
375
310
|
parsed = JSON.parse(raw);
|
|
376
|
-
} catch (
|
|
311
|
+
} catch (err) {
|
|
377
312
|
throw new ConfigError(
|
|
378
|
-
`${path3} is not valid JSON: ${
|
|
313
|
+
`${path3} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
379
314
|
);
|
|
380
315
|
}
|
|
381
316
|
const result = companionConfigSchema.safeParse(parsed);
|
|
@@ -424,7 +359,7 @@ function loadConfigTolerant() {
|
|
|
424
359
|
}
|
|
425
360
|
const obj = parsed && typeof parsed === "object" ? parsed : {};
|
|
426
361
|
const local = {};
|
|
427
|
-
const agents =
|
|
362
|
+
const agents = z2.record(z2.string(), localAgentConfigSchema).safeParse(obj.agents);
|
|
428
363
|
if (agents.success) local.agents = agents.data;
|
|
429
364
|
if (typeof obj.dashboardPort === "number") local.dashboardPort = obj.dashboardPort;
|
|
430
365
|
if (typeof obj.autoOpen === "boolean") local.autoOpen = obj.autoOpen;
|
|
@@ -451,12 +386,12 @@ function saveConfig(cfg) {
|
|
|
451
386
|
} catch {
|
|
452
387
|
}
|
|
453
388
|
renameSync(tmp, path3);
|
|
454
|
-
} catch (
|
|
389
|
+
} catch (err) {
|
|
455
390
|
try {
|
|
456
391
|
rmSync(tmp, { force: true });
|
|
457
392
|
} catch {
|
|
458
393
|
}
|
|
459
|
-
throw
|
|
394
|
+
throw err;
|
|
460
395
|
}
|
|
461
396
|
}
|
|
462
397
|
function requireConfig() {
|
|
@@ -631,8 +566,8 @@ async function verifyRuntime(state, fetchImpl = fetch) {
|
|
|
631
566
|
let res;
|
|
632
567
|
try {
|
|
633
568
|
res = await fetchImpl(url, { signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) });
|
|
634
|
-
} catch (
|
|
635
|
-
return isConnRefused(
|
|
569
|
+
} catch (err) {
|
|
570
|
+
return isConnRefused(err) ? "stale" : "unknown";
|
|
636
571
|
}
|
|
637
572
|
if (!res.ok) return "unknown";
|
|
638
573
|
let body;
|
|
@@ -644,9 +579,9 @@ async function verifyRuntime(state, fetchImpl = fetch) {
|
|
|
644
579
|
if (typeof body.instance_id !== "string") return "unknown";
|
|
645
580
|
return body.instance_id === state.instanceId ? "ours" : "stale";
|
|
646
581
|
}
|
|
647
|
-
function isConnRefused(
|
|
648
|
-
if (!
|
|
649
|
-
const cause =
|
|
582
|
+
function isConnRefused(err) {
|
|
583
|
+
if (!err || typeof err !== "object") return false;
|
|
584
|
+
const cause = err.cause;
|
|
650
585
|
return !!cause && typeof cause === "object" && cause.code === "ECONNREFUSED";
|
|
651
586
|
}
|
|
652
587
|
function trimSlash(s) {
|
|
@@ -737,11 +672,11 @@ import {
|
|
|
737
672
|
writeFileSync as writeFileSync3
|
|
738
673
|
} from "fs";
|
|
739
674
|
import { dirname as dirname3, join as join4 } from "path";
|
|
740
|
-
import { z as
|
|
675
|
+
import { z as z3 } from "zod";
|
|
741
676
|
function credentialsPath() {
|
|
742
677
|
return join4(cabaneDir(), "credentials.json");
|
|
743
678
|
}
|
|
744
|
-
var credentialStoreSchema =
|
|
679
|
+
var credentialStoreSchema = z3.record(z3.string(), z3.string());
|
|
745
680
|
function load() {
|
|
746
681
|
const path3 = credentialsPath();
|
|
747
682
|
if (!existsSync3(path3)) return {};
|
|
@@ -774,12 +709,12 @@ function save(map) {
|
|
|
774
709
|
} catch {
|
|
775
710
|
}
|
|
776
711
|
renameSync2(tmp, path3);
|
|
777
|
-
} catch (
|
|
712
|
+
} catch (err) {
|
|
778
713
|
try {
|
|
779
714
|
rmSync3(tmp, { force: true });
|
|
780
715
|
} catch {
|
|
781
716
|
}
|
|
782
|
-
throw
|
|
717
|
+
throw err;
|
|
783
718
|
}
|
|
784
719
|
}
|
|
785
720
|
function getCredential(agentId) {
|
|
@@ -818,13 +753,13 @@ async function logout(opts = {}) {
|
|
|
818
753
|
}
|
|
819
754
|
if (!opts.yes) {
|
|
820
755
|
const message = opts.purge ? "Purge the entire local companion config (device + agent overrides + settings)?" : "Log out this device (removes the device token + cached agent credentials; keeps your config)?";
|
|
821
|
-
const
|
|
822
|
-
if (!
|
|
756
|
+
const ok = await confirm({ message, default: false });
|
|
757
|
+
if (!ok) {
|
|
823
758
|
process.stdout.write("cancelled\n");
|
|
824
759
|
return;
|
|
825
760
|
}
|
|
826
761
|
}
|
|
827
|
-
const serverNote = `the device remains registered server-side \u2014 remove it in the cabane app (Settings \u2192
|
|
762
|
+
const serverNote = `the device remains registered server-side \u2014 remove it in the cabane app (Settings \u2192 Connectors) if you want it gone there too.`;
|
|
828
763
|
if (opts.purge) {
|
|
829
764
|
deleteConfig();
|
|
830
765
|
clearCredentials();
|
|
@@ -841,10 +776,6 @@ async function logout(opts = {}) {
|
|
|
841
776
|
);
|
|
842
777
|
}
|
|
843
778
|
|
|
844
|
-
// src/commands/pair.ts
|
|
845
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
846
|
-
import { password } from "@inquirer/prompts";
|
|
847
|
-
|
|
848
779
|
// src/enrollment.ts
|
|
849
780
|
function trimBase(baseUrl) {
|
|
850
781
|
return baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
@@ -857,9 +788,9 @@ async function postJson(baseUrl, path3, body) {
|
|
|
857
788
|
headers: { "content-type": "application/json", accept: "application/json" },
|
|
858
789
|
body: JSON.stringify(body)
|
|
859
790
|
});
|
|
860
|
-
} catch (
|
|
791
|
+
} catch (err) {
|
|
861
792
|
throw new CompanionError(
|
|
862
|
-
`couldn't reach cabane at ${baseUrl}: ${
|
|
793
|
+
`couldn't reach cabane at ${baseUrl}: ${err instanceof Error ? err.message : String(err)}. Check the server URL (pass --server <url>) and your connection.`
|
|
863
794
|
);
|
|
864
795
|
}
|
|
865
796
|
const raw = await res.text();
|
|
@@ -874,7 +805,7 @@ async function postJson(baseUrl, path3, body) {
|
|
|
874
805
|
if (res.status >= 400) {
|
|
875
806
|
if (res.status === 404 && path3.endsWith("/code")) {
|
|
876
807
|
throw new CompanionError(
|
|
877
|
-
`this cabane server (${baseUrl}) doesn't support device-flow pairing yet. Update the server
|
|
808
|
+
`this cabane server (${baseUrl}) doesn't support device-flow pairing yet. Update the server to a version that supports \`cabane-companion pair\`.`
|
|
878
809
|
);
|
|
879
810
|
}
|
|
880
811
|
const msg = parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : `${res.status}`;
|
|
@@ -957,34 +888,6 @@ function writePairedConfig(paired) {
|
|
|
957
888
|
}
|
|
958
889
|
|
|
959
890
|
// src/commands/pair.ts
|
|
960
|
-
async function readStdin() {
|
|
961
|
-
const chunks = [];
|
|
962
|
-
for await (const chunk of process.stdin) {
|
|
963
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
964
|
-
}
|
|
965
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
966
|
-
}
|
|
967
|
-
async function resolvePairingString(opts) {
|
|
968
|
-
if (opts.file !== void 0) {
|
|
969
|
-
try {
|
|
970
|
-
return readFileSync4(opts.file, "utf8");
|
|
971
|
-
} catch (err2) {
|
|
972
|
-
throw new CompanionError(
|
|
973
|
-
`couldn't read the pairing string from ${opts.file}: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
974
|
-
);
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
if (opts.arg !== void 0) {
|
|
978
|
-
process.stderr.write(
|
|
979
|
-
"warning: passing the pairing string as an argument leaves it in your shell history and is readable by other processes while `pair` runs. Prefer `cabane-companion pair --legacy` (you'll be prompted to paste it) or `cabane-companion pair --legacy --file <path>`.\n"
|
|
980
|
-
);
|
|
981
|
-
return opts.arg;
|
|
982
|
-
}
|
|
983
|
-
if (!process.stdin.isTTY) {
|
|
984
|
-
return await readStdin();
|
|
985
|
-
}
|
|
986
|
-
return await password({ message: "Paste the pairing string from cabane:" });
|
|
987
|
-
}
|
|
988
891
|
function writePairedConfigCli(paired) {
|
|
989
892
|
const { note } = writePairedConfig(paired);
|
|
990
893
|
if (note) process.stderr.write(`note: ${note}
|
|
@@ -996,25 +899,28 @@ Run \`cabane-companion start\` \u2014 it will pull the agents assigned to this d
|
|
|
996
899
|
`
|
|
997
900
|
);
|
|
998
901
|
}
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
...pairing.deviceLabel ? { deviceLabel: pairing.deviceLabel } : {}
|
|
1009
|
-
});
|
|
1010
|
-
return;
|
|
902
|
+
function writeCompletedPairing(raw) {
|
|
903
|
+
let paired;
|
|
904
|
+
try {
|
|
905
|
+
paired = JSON.parse(raw);
|
|
906
|
+
} catch {
|
|
907
|
+
throw new Error("invalid completed pairing payload: expected JSON on stdin.");
|
|
908
|
+
}
|
|
909
|
+
if (!paired || typeof paired !== "object" || typeof paired.baseUrl !== "string" || typeof paired.deviceToken !== "string") {
|
|
910
|
+
throw new Error("invalid completed pairing payload: baseUrl and deviceToken are required.");
|
|
1011
911
|
}
|
|
912
|
+
writePairedConfigCli(paired);
|
|
913
|
+
}
|
|
914
|
+
async function pair(opts = {}) {
|
|
1012
915
|
const baseUrl = resolvePairBaseUrl(opts.server);
|
|
1013
916
|
const paired = await runDeviceFlow(baseUrl, (line) => process.stdout.write(`${line}
|
|
1014
917
|
`));
|
|
1015
918
|
writePairedConfigCli(paired);
|
|
1016
919
|
}
|
|
1017
920
|
|
|
921
|
+
// src/cli.ts
|
|
922
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
923
|
+
|
|
1018
924
|
// src/browser.ts
|
|
1019
925
|
import { spawn as spawn4 } from "child_process";
|
|
1020
926
|
import { platform } from "process";
|
|
@@ -1463,15 +1369,15 @@ var DEFAULT_PORT = 7474;
|
|
|
1463
1369
|
var PORT_FALLBACK_SPAN = 10;
|
|
1464
1370
|
function buildDashboardApp(deps) {
|
|
1465
1371
|
const app = new Hono();
|
|
1466
|
-
app.onError((
|
|
1467
|
-
if (
|
|
1468
|
-
const status2 =
|
|
1469
|
-
return c.json({ error:
|
|
1372
|
+
app.onError((err, c) => {
|
|
1373
|
+
if (err instanceof ApiError) {
|
|
1374
|
+
const status2 = err.status >= 400 && err.status < 600 ? err.status : 502;
|
|
1375
|
+
return c.json({ error: err.message }, status2);
|
|
1470
1376
|
}
|
|
1471
|
-
if (
|
|
1472
|
-
return c.json({ error:
|
|
1377
|
+
if (err instanceof CompanionError) {
|
|
1378
|
+
return c.json({ error: err.message }, 400);
|
|
1473
1379
|
}
|
|
1474
|
-
return c.json({ error:
|
|
1380
|
+
return c.json({ error: err instanceof Error ? err.message : "internal error" }, 500);
|
|
1475
1381
|
});
|
|
1476
1382
|
registerRoutes(app, { ...deps, staticDir: resolveStaticDir() });
|
|
1477
1383
|
return app;
|
|
@@ -1492,12 +1398,12 @@ async function startDashboard(opts) {
|
|
|
1492
1398
|
server.closeAllConnections?.();
|
|
1493
1399
|
})
|
|
1494
1400
|
};
|
|
1495
|
-
} catch (
|
|
1496
|
-
if (isAddrInUse(
|
|
1497
|
-
lastErr =
|
|
1401
|
+
} catch (err) {
|
|
1402
|
+
if (isAddrInUse(err)) {
|
|
1403
|
+
lastErr = err;
|
|
1498
1404
|
continue;
|
|
1499
1405
|
}
|
|
1500
|
-
throw
|
|
1406
|
+
throw err;
|
|
1501
1407
|
}
|
|
1502
1408
|
}
|
|
1503
1409
|
throw new CompanionError(
|
|
@@ -1513,16 +1419,16 @@ function listen(app, port) {
|
|
|
1513
1419
|
resolve(server);
|
|
1514
1420
|
}
|
|
1515
1421
|
});
|
|
1516
|
-
server.on("error", (
|
|
1422
|
+
server.on("error", (err) => {
|
|
1517
1423
|
if (!settled) {
|
|
1518
1424
|
settled = true;
|
|
1519
|
-
reject(
|
|
1425
|
+
reject(err);
|
|
1520
1426
|
}
|
|
1521
1427
|
});
|
|
1522
1428
|
});
|
|
1523
1429
|
}
|
|
1524
|
-
function isAddrInUse(
|
|
1525
|
-
return Boolean(
|
|
1430
|
+
function isAddrInUse(err) {
|
|
1431
|
+
return Boolean(err && typeof err === "object" && "code" in err && err.code === "EADDRINUSE");
|
|
1526
1432
|
}
|
|
1527
1433
|
function resolveStaticDir() {
|
|
1528
1434
|
return join6(dirname4(fileURLToPath2(import.meta.url)), "static");
|
|
@@ -1665,10 +1571,10 @@ var CabaneApi = class {
|
|
|
1665
1571
|
for (let attempt = 1; ; attempt++) {
|
|
1666
1572
|
try {
|
|
1667
1573
|
return await this.attempt(method, path3, body, signal);
|
|
1668
|
-
} catch (
|
|
1669
|
-
if (attempt >= maxAttempts || signal?.aborted || !isRetryable(
|
|
1574
|
+
} catch (err) {
|
|
1575
|
+
if (attempt >= maxAttempts || signal?.aborted || !isRetryable(err)) throw err;
|
|
1670
1576
|
await sleep2(RETRY_BACKOFF_MS[attempt - 1], signal);
|
|
1671
|
-
if (signal?.aborted) throw
|
|
1577
|
+
if (signal?.aborted) throw err;
|
|
1672
1578
|
}
|
|
1673
1579
|
}
|
|
1674
1580
|
}
|
|
@@ -1696,14 +1602,14 @@ var CabaneApi = class {
|
|
|
1696
1602
|
retry: true,
|
|
1697
1603
|
...signal ? { signal } : {}
|
|
1698
1604
|
});
|
|
1699
|
-
} catch (
|
|
1605
|
+
} catch (err) {
|
|
1700
1606
|
const outbox = this.opts.outbox;
|
|
1701
|
-
if (!outbox) throw
|
|
1702
|
-
if (signal?.aborted || isAbortError(
|
|
1703
|
-
if (!isRetryable(
|
|
1607
|
+
if (!outbox) throw err;
|
|
1608
|
+
if (signal?.aborted || isAbortError(err)) throw err;
|
|
1609
|
+
if (!isRetryable(err)) throw err;
|
|
1704
1610
|
outbox.persist({ enqueuedAt: Date.now(), turnId, seq, method: "POST", path: path3, body, kind });
|
|
1705
1611
|
this.opts.log?.warn(
|
|
1706
|
-
{ kind, turnId, seq, err:
|
|
1612
|
+
{ kind, turnId, seq, err: err instanceof Error ? err.message : String(err) },
|
|
1707
1613
|
"companion: commit queued to outbox after transient failure (will drain when the API returns)"
|
|
1708
1614
|
);
|
|
1709
1615
|
}
|
|
@@ -1727,10 +1633,10 @@ var CabaneApi = class {
|
|
|
1727
1633
|
await this.request(entry.method, entry.path, entry.body, { retry: true });
|
|
1728
1634
|
outbox.remove(entry.turnId, entry.seq);
|
|
1729
1635
|
progressed = true;
|
|
1730
|
-
} catch (
|
|
1731
|
-
if (
|
|
1636
|
+
} catch (err) {
|
|
1637
|
+
if (err instanceof ApiError && err.status >= 400 && err.status < 500) {
|
|
1732
1638
|
this.opts.log?.warn(
|
|
1733
|
-
{ kind: entry.kind, turnId: entry.turnId, seq: entry.seq, status:
|
|
1639
|
+
{ kind: entry.kind, turnId: entry.turnId, seq: entry.seq, status: err.status },
|
|
1734
1640
|
"companion outbox: discarding entry on terminal 4xx (will never land)"
|
|
1735
1641
|
);
|
|
1736
1642
|
outbox.remove(entry.turnId, entry.seq);
|
|
@@ -1878,11 +1784,11 @@ var CabaneApi = class {
|
|
|
1878
1784
|
try {
|
|
1879
1785
|
await this.request("PATCH", path3, body, { retry: true });
|
|
1880
1786
|
outbox?.remove(key, ACTIVE_RUN_OUTBOX_SEQ);
|
|
1881
|
-
} catch (
|
|
1882
|
-
if (!outbox) throw
|
|
1883
|
-
if (!isRetryable(
|
|
1787
|
+
} catch (err) {
|
|
1788
|
+
if (!outbox) throw err;
|
|
1789
|
+
if (!isRetryable(err)) {
|
|
1884
1790
|
outbox.remove(key, ACTIVE_RUN_OUTBOX_SEQ);
|
|
1885
|
-
throw
|
|
1791
|
+
throw err;
|
|
1886
1792
|
}
|
|
1887
1793
|
outbox.persist({
|
|
1888
1794
|
enqueuedAt: Date.now(),
|
|
@@ -1894,7 +1800,7 @@ var CabaneApi = class {
|
|
|
1894
1800
|
kind: "active-run"
|
|
1895
1801
|
});
|
|
1896
1802
|
this.opts.log?.warn(
|
|
1897
|
-
{ conversationId, agentId, err:
|
|
1803
|
+
{ conversationId, agentId, err: err instanceof Error ? err.message : String(err) },
|
|
1898
1804
|
"companion: active-run write queued to outbox after transient failure (will drain when the API returns)"
|
|
1899
1805
|
);
|
|
1900
1806
|
}
|
|
@@ -1993,13 +1899,13 @@ var CabaneApi = class {
|
|
|
1993
1899
|
return res.messages.find((m) => m.id === messageId2) ?? null;
|
|
1994
1900
|
}
|
|
1995
1901
|
};
|
|
1996
|
-
function isRetryable(
|
|
1997
|
-
if (
|
|
1998
|
-
if (isAbortError(
|
|
1902
|
+
function isRetryable(err) {
|
|
1903
|
+
if (err instanceof ApiError) return err.status >= 500;
|
|
1904
|
+
if (isAbortError(err)) return false;
|
|
1999
1905
|
return true;
|
|
2000
1906
|
}
|
|
2001
|
-
function isAbortError(
|
|
2002
|
-
return
|
|
1907
|
+
function isAbortError(err) {
|
|
1908
|
+
return err instanceof Error && err.name === "AbortError";
|
|
2003
1909
|
}
|
|
2004
1910
|
function sleep2(ms, signal) {
|
|
2005
1911
|
return new Promise((resolve) => {
|
|
@@ -2017,8 +1923,8 @@ function sleep2(ms, signal) {
|
|
|
2017
1923
|
}
|
|
2018
1924
|
function errorMessage(status2, body) {
|
|
2019
1925
|
if (body && typeof body === "object" && "error" in body) {
|
|
2020
|
-
const
|
|
2021
|
-
if (typeof
|
|
1926
|
+
const err = body.error;
|
|
1927
|
+
if (typeof err === "string") return `${status2} ${err}`;
|
|
2022
1928
|
}
|
|
2023
1929
|
if (typeof body === "string" && body.length > 0) return `${status2} ${body.slice(0, 200)}`;
|
|
2024
1930
|
return `${status2} error`;
|
|
@@ -2074,15 +1980,15 @@ var DeviceApi = class {
|
|
|
2074
1980
|
};
|
|
2075
1981
|
function errorMessage2(status2, body) {
|
|
2076
1982
|
if (body && typeof body === "object" && "error" in body) {
|
|
2077
|
-
const
|
|
2078
|
-
if (typeof
|
|
1983
|
+
const err = body.error;
|
|
1984
|
+
if (typeof err === "string") return `${status2} ${err}`;
|
|
2079
1985
|
}
|
|
2080
1986
|
if (typeof body === "string" && body.length > 0) return `${status2} ${body.slice(0, 200)}`;
|
|
2081
1987
|
return `${status2} error`;
|
|
2082
1988
|
}
|
|
2083
1989
|
|
|
2084
1990
|
// src/cursor.ts
|
|
2085
|
-
import { mkdirSync as mkdirSync6, readFileSync as
|
|
1991
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync5 } from "fs";
|
|
2086
1992
|
import { join as join7 } from "path";
|
|
2087
1993
|
function pathFor(workspaceId) {
|
|
2088
1994
|
return join7(cabaneDir(), "cursors", encodeURIComponent(workspaceId));
|
|
@@ -2090,7 +1996,7 @@ function pathFor(workspaceId) {
|
|
|
2090
1996
|
function readCursor(workspaceId) {
|
|
2091
1997
|
const path3 = pathFor(workspaceId);
|
|
2092
1998
|
if (!existsSync5(path3)) return null;
|
|
2093
|
-
const raw =
|
|
1999
|
+
const raw = readFileSync4(path3, "utf8").trim();
|
|
2094
2000
|
return raw.length > 0 ? raw : null;
|
|
2095
2001
|
}
|
|
2096
2002
|
function writeCursor(workspaceId, eventId) {
|
|
@@ -2139,7 +2045,7 @@ var CursorTracker = class {
|
|
|
2139
2045
|
};
|
|
2140
2046
|
|
|
2141
2047
|
// src/dispatch-dedupe.ts
|
|
2142
|
-
import { mkdirSync as mkdirSync7, readFileSync as
|
|
2048
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync6 } from "fs";
|
|
2143
2049
|
import { join as join8 } from "path";
|
|
2144
2050
|
var MAX_IDS = 256;
|
|
2145
2051
|
function dir(log) {
|
|
@@ -2152,7 +2058,7 @@ function readIds(log, workspaceId) {
|
|
|
2152
2058
|
const path3 = pathFor2(log, workspaceId);
|
|
2153
2059
|
if (!existsSync6(path3)) return [];
|
|
2154
2060
|
try {
|
|
2155
|
-
return
|
|
2061
|
+
return readFileSync5(path3, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
2156
2062
|
} catch {
|
|
2157
2063
|
return [];
|
|
2158
2064
|
}
|
|
@@ -2192,7 +2098,7 @@ function readResumeCounts(workspaceId) {
|
|
|
2192
2098
|
const path3 = resumePathFor(workspaceId);
|
|
2193
2099
|
if (!existsSync6(path3)) return out;
|
|
2194
2100
|
try {
|
|
2195
|
-
for (const line of
|
|
2101
|
+
for (const line of readFileSync5(path3, "utf8").split("\n")) {
|
|
2196
2102
|
const trimmed = line.trim();
|
|
2197
2103
|
if (!trimmed) continue;
|
|
2198
2104
|
const tab = trimmed.lastIndexOf(" ");
|
|
@@ -2228,44 +2134,44 @@ function noResume() {
|
|
|
2228
2134
|
var TURN_PROTOCOL_VERSION = 1;
|
|
2229
2135
|
|
|
2230
2136
|
// packages/agent-runtime/src/host-policy.ts
|
|
2231
|
-
import { z as
|
|
2232
|
-
var hostPolicySchema =
|
|
2137
|
+
import { z as z4 } from "zod";
|
|
2138
|
+
var hostPolicySchema = z4.object({
|
|
2233
2139
|
// Host filesystem + shell: `Bash`/`Read`/`Write`/`Edit`/`Glob`/`Grep`, the
|
|
2234
2140
|
// notebook read/write pair, git-worktree tools, and the `REPL` host code-exec
|
|
2235
2141
|
// tool. Off under the locked assistant surface (today's `DISALLOWED_TOOLS`);
|
|
2236
2142
|
// on under `coding` mode.
|
|
2237
|
-
hostFs:
|
|
2143
|
+
hostFs: z4.boolean(),
|
|
2238
2144
|
// Public web (today's `WebSearch` / `WebFetch`, the `DEFAULT_WEB_TOOLS`). Public
|
|
2239
2145
|
// web, not host reach — granted by default today, but expressible as a grant.
|
|
2240
|
-
web:
|
|
2146
|
+
web: z4.boolean(),
|
|
2241
2147
|
// Browser automation (the Playwright MCP surface). Varies by host: a companion has
|
|
2242
2148
|
// it, the house executor does not (CT230).
|
|
2243
|
-
browser:
|
|
2149
|
+
browser: z4.boolean(),
|
|
2244
2150
|
// User-configured MCP servers permitted. False for the house executor
|
|
2245
2151
|
// (CT227: Cabane agents run no user MCP servers), true for a personal companion.
|
|
2246
|
-
userMcp:
|
|
2152
|
+
userMcp: z4.boolean(),
|
|
2247
2153
|
// Subagent / sub-dispatch (`Task` / `Agent`). A per-host GRANT (see the CT261
|
|
2248
2154
|
// amendment above): `false` on the locked assistant/house surface (banned via
|
|
2249
2155
|
// `DISALLOWED_TOOLS`), `true` under coding / custom where the fan-out is left to
|
|
2250
2156
|
// the coding allow-hook (which reaches sub-agent tool calls, CT680) or the user
|
|
2251
2157
|
// allowlist. The subagent completes within the turn, so
|
|
2252
2158
|
// it's not the turn-model invariant `scheduling` is.
|
|
2253
|
-
subagents:
|
|
2159
|
+
subagents: z4.boolean(),
|
|
2254
2160
|
// ── Hard platform invariants — always denied, never granted ────────────────
|
|
2255
2161
|
// Deferred re-invocation / scheduling (`ScheduleWakeup`, the `Cron*` / `Task*`
|
|
2256
2162
|
// families, `Monitor`, `Workflow`, …). A turn is one query that resolves when
|
|
2257
2163
|
// `result` fires; a scheduled callback fires after the reply window has closed
|
|
2258
2164
|
// and strands the agent (the CT155/CT156 rule).
|
|
2259
|
-
scheduling:
|
|
2165
|
+
scheduling: z4.literal("never"),
|
|
2260
2166
|
// Human-facing UI prompts (`AskUserQuestion`). A Cabane conversation has no
|
|
2261
2167
|
// handler to answer a structured prompt, so the call hangs the turn
|
|
2262
2168
|
// (`UNSUPPORTED_TOOLS`). The agent asks in its reply instead.
|
|
2263
|
-
uiPrompts:
|
|
2169
|
+
uiPrompts: z4.literal("never")
|
|
2264
2170
|
});
|
|
2265
2171
|
|
|
2266
2172
|
// packages/agent-runtime/src/turn-event.ts
|
|
2267
|
-
import { z as
|
|
2268
|
-
var turnEventSchema =
|
|
2173
|
+
import { z as z5 } from "zod";
|
|
2174
|
+
var turnEventSchema = z5.discriminatedUnion("type", [
|
|
2269
2175
|
// The runtime's opaque session state, emitted when the adapter learns it (e.g.
|
|
2270
2176
|
// the SDK `system/init` frame). The platform stores `state` verbatim per
|
|
2271
2177
|
// (conversation, agent) and hands it back on the next turn; only the adapter
|
|
@@ -2285,19 +2191,19 @@ var turnEventSchema = z6.discriminatedUnion("type", [
|
|
|
2285
2191
|
// on the companion, after the server committed the manifest). Runtime-neutral: a
|
|
2286
2192
|
// plain boolean, not a runtime-specific reason string (that stays in the
|
|
2287
2193
|
// adapter's `onWarn` log). Additive + optional — an old receiver ignores it.
|
|
2288
|
-
|
|
2289
|
-
type:
|
|
2290
|
-
state:
|
|
2291
|
-
degraded:
|
|
2194
|
+
z5.object({
|
|
2195
|
+
type: z5.literal("session"),
|
|
2196
|
+
state: z5.string(),
|
|
2197
|
+
degraded: z5.boolean().optional()
|
|
2292
2198
|
}),
|
|
2293
2199
|
// One readable thinking summary. Maps `onThinking({ text })`. Transient —
|
|
2294
2200
|
// surfaced live, never persisted as durable content.
|
|
2295
|
-
|
|
2201
|
+
z5.object({ type: z5.literal("thinking"), text: z5.string() }),
|
|
2296
2202
|
// Assistant text. Maps `onAssistantText({ text, final })` — `text`→`body`,
|
|
2297
2203
|
// `final`→`terminal`. `terminal: false` is interim narration (commits as a
|
|
2298
2204
|
// `progress` row); `terminal: true` is the turn's closing reply (commits as
|
|
2299
2205
|
// the `final` row).
|
|
2300
|
-
|
|
2206
|
+
z5.object({ type: z5.literal("text"), body: z5.string(), terminal: z5.boolean() }),
|
|
2301
2207
|
// A tool-activity transition. Maps `onToolActivity` — `toolUseId`→`id`,
|
|
2302
2208
|
// `toolName`→`name` (already prefix-stripped: `cabane_read`, not
|
|
2303
2209
|
// `mcp__cabane__cabane_read`), `summary` is the short card label. `phase`
|
|
@@ -2312,15 +2218,15 @@ var turnEventSchema = z6.discriminatedUnion("type", [
|
|
|
2312
2218
|
// dropped the prefix; null for a host / built-in tool. The client tags Cabane
|
|
2313
2219
|
// MCP calls (`=== 'cabane'`) with a subtle glyph. Nullable + optional so a
|
|
2314
2220
|
// pre-CT496 producer that never sets it is unaffected (treated as null).
|
|
2315
|
-
|
|
2316
|
-
type:
|
|
2317
|
-
id:
|
|
2318
|
-
name:
|
|
2319
|
-
phase:
|
|
2320
|
-
summary:
|
|
2321
|
-
input:
|
|
2322
|
-
result:
|
|
2323
|
-
mcpServer:
|
|
2221
|
+
z5.object({
|
|
2222
|
+
type: z5.literal("tool"),
|
|
2223
|
+
id: z5.string(),
|
|
2224
|
+
name: z5.string(),
|
|
2225
|
+
phase: z5.enum(["start", "done", "error"]),
|
|
2226
|
+
summary: z5.string(),
|
|
2227
|
+
input: z5.unknown().optional(),
|
|
2228
|
+
result: z5.unknown().optional(),
|
|
2229
|
+
mcpServer: z5.string().nullable().optional()
|
|
2324
2230
|
}),
|
|
2325
2231
|
// The turn's terminal outcome. Maps the `{ ok, reason }` both loops return
|
|
2326
2232
|
// inline. `ok:false` carries a machine reason (`no_session`, an error code);
|
|
@@ -2364,34 +2270,34 @@ var turnEventSchema = z6.discriminatedUnion("type", [
|
|
|
2364
2270
|
// `message_turn_metadata.resolved_model` / `resolved_config`. Optional ⇒
|
|
2365
2271
|
// backward-compatible: an old adapter/companion omits them, a cancel has no result
|
|
2366
2272
|
// event at all, and the columns stay null → the UI shows `—`.
|
|
2367
|
-
|
|
2368
|
-
type:
|
|
2369
|
-
ok:
|
|
2370
|
-
reason:
|
|
2371
|
-
usage:
|
|
2372
|
-
inputTokens:
|
|
2373
|
-
outputTokens:
|
|
2374
|
-
cacheReadTokens:
|
|
2375
|
-
cacheCreationTokens:
|
|
2376
|
-
contextTokens:
|
|
2377
|
-
contextWindow:
|
|
2273
|
+
z5.object({
|
|
2274
|
+
type: z5.literal("result"),
|
|
2275
|
+
ok: z5.boolean(),
|
|
2276
|
+
reason: z5.string().optional(),
|
|
2277
|
+
usage: z5.object({
|
|
2278
|
+
inputTokens: z5.number(),
|
|
2279
|
+
outputTokens: z5.number(),
|
|
2280
|
+
cacheReadTokens: z5.number().optional(),
|
|
2281
|
+
cacheCreationTokens: z5.number().optional(),
|
|
2282
|
+
contextTokens: z5.number().optional(),
|
|
2283
|
+
contextWindow: z5.number().optional()
|
|
2378
2284
|
}).optional(),
|
|
2379
|
-
resolvedModel:
|
|
2380
|
-
resolvedConfig:
|
|
2381
|
-
effort:
|
|
2382
|
-
thinking:
|
|
2383
|
-
reasoningEffort:
|
|
2285
|
+
resolvedModel: z5.string().optional(),
|
|
2286
|
+
resolvedConfig: z5.object({
|
|
2287
|
+
effort: z5.string().optional(),
|
|
2288
|
+
thinking: z5.string().optional(),
|
|
2289
|
+
reasoningEffort: z5.string().optional()
|
|
2384
2290
|
}).optional()
|
|
2385
2291
|
})
|
|
2386
2292
|
]);
|
|
2387
2293
|
|
|
2388
2294
|
// packages/agent-runtime/src/failure.ts
|
|
2389
|
-
import { z as
|
|
2390
|
-
var turnFailureSchema =
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2295
|
+
import { z as z6 } from "zod";
|
|
2296
|
+
var turnFailureSchema = z6.discriminatedUnion("kind", [
|
|
2297
|
+
z6.object({ kind: z6.literal("usage_capped"), resetsAt: z6.string().optional() }),
|
|
2298
|
+
z6.object({ kind: z6.literal("rate_limited") }),
|
|
2299
|
+
z6.object({ kind: z6.literal("server_error") }),
|
|
2300
|
+
z6.object({ kind: z6.literal("auth_expired") })
|
|
2395
2301
|
]);
|
|
2396
2302
|
var USAGE_CAPPED = "usage_capped";
|
|
2397
2303
|
var RATE_LIMITED = "rate_limited";
|
|
@@ -2497,63 +2403,63 @@ var RATE_PATTERNS = [/rate[_ ]?limit/, /\b429\b/, /too many requests/];
|
|
|
2497
2403
|
var BARE_LIMIT = /\blimit (reached|exceeded)\b/;
|
|
2498
2404
|
|
|
2499
2405
|
// packages/agent-runtime/src/turn-request.ts
|
|
2500
|
-
import { z as
|
|
2501
|
-
var contentBlockSchema =
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
type:
|
|
2505
|
-
source:
|
|
2406
|
+
import { z as z7 } from "zod";
|
|
2407
|
+
var contentBlockSchema = z7.discriminatedUnion("type", [
|
|
2408
|
+
z7.object({ type: z7.literal("text"), text: z7.string() }),
|
|
2409
|
+
z7.object({
|
|
2410
|
+
type: z7.literal("image"),
|
|
2411
|
+
source: z7.object({ type: z7.literal("url"), url: z7.string() })
|
|
2506
2412
|
}),
|
|
2507
|
-
|
|
2508
|
-
type:
|
|
2509
|
-
source:
|
|
2413
|
+
z7.object({
|
|
2414
|
+
type: z7.literal("document"),
|
|
2415
|
+
source: z7.object({ type: z7.literal("url"), url: z7.string() })
|
|
2510
2416
|
})
|
|
2511
2417
|
]);
|
|
2512
|
-
var effortLevelSchema =
|
|
2513
|
-
var resolvedRunConfigSchema =
|
|
2514
|
-
model:
|
|
2418
|
+
var effortLevelSchema = z7.enum(["low", "medium", "high", "xhigh", "max"]);
|
|
2419
|
+
var resolvedRunConfigSchema = z7.object({
|
|
2420
|
+
model: z7.string().nullable(),
|
|
2515
2421
|
effort: effortLevelSchema.optional(),
|
|
2516
|
-
runtimeOptions:
|
|
2422
|
+
runtimeOptions: z7.record(z7.string(), z7.unknown()).optional()
|
|
2517
2423
|
});
|
|
2518
|
-
var resolvedMcpServerSchema =
|
|
2519
|
-
|
|
2520
|
-
type:
|
|
2521
|
-
command:
|
|
2522
|
-
args:
|
|
2523
|
-
env:
|
|
2424
|
+
var resolvedMcpServerSchema = z7.union([
|
|
2425
|
+
z7.object({
|
|
2426
|
+
type: z7.literal("stdio").optional(),
|
|
2427
|
+
command: z7.string(),
|
|
2428
|
+
args: z7.array(z7.string()).optional(),
|
|
2429
|
+
env: z7.record(z7.string(), z7.string()).optional()
|
|
2524
2430
|
}),
|
|
2525
|
-
|
|
2526
|
-
type:
|
|
2527
|
-
url:
|
|
2528
|
-
headers:
|
|
2431
|
+
z7.object({
|
|
2432
|
+
type: z7.literal("http"),
|
|
2433
|
+
url: z7.string(),
|
|
2434
|
+
headers: z7.record(z7.string(), z7.string()).optional()
|
|
2529
2435
|
}),
|
|
2530
|
-
|
|
2531
|
-
type:
|
|
2532
|
-
url:
|
|
2533
|
-
headers:
|
|
2436
|
+
z7.object({
|
|
2437
|
+
type: z7.literal("sse"),
|
|
2438
|
+
url: z7.string(),
|
|
2439
|
+
headers: z7.record(z7.string(), z7.string()).optional()
|
|
2534
2440
|
})
|
|
2535
2441
|
]);
|
|
2536
|
-
var resolvedMcpServersSchema =
|
|
2537
|
-
var hostInjectedServersSchema =
|
|
2538
|
-
var turnRequestSchema =
|
|
2442
|
+
var resolvedMcpServersSchema = z7.record(z7.string(), resolvedMcpServerSchema);
|
|
2443
|
+
var hostInjectedServersSchema = z7.record(z7.string(), z7.unknown());
|
|
2444
|
+
var turnRequestSchema = z7.object({
|
|
2539
2445
|
// Server-composed system prompt (core + capability prose + adapter addendum +
|
|
2540
2446
|
// charter). One string to the adapter.
|
|
2541
|
-
systemPrompt:
|
|
2447
|
+
systemPrompt: z7.string(),
|
|
2542
2448
|
// Server-composed per-turn user text (anchor reminder + the triggering message).
|
|
2543
|
-
prompt:
|
|
2449
|
+
prompt: z7.string(),
|
|
2544
2450
|
// The multi-block user-message body (text + vision).
|
|
2545
|
-
content:
|
|
2451
|
+
content: z7.array(contentBlockSchema),
|
|
2546
2452
|
// Portable-or-dialect run-config (above).
|
|
2547
2453
|
config: resolvedRunConfigSchema,
|
|
2548
2454
|
// Abstract capability grants; the adapter maps them to tool names.
|
|
2549
2455
|
policy: hostPolicySchema,
|
|
2550
2456
|
// Prior opaque session state, or null for a fresh session.
|
|
2551
|
-
session:
|
|
2457
|
+
session: z7.string().nullable(),
|
|
2552
2458
|
// The cabane control-plane coordinates for this turn's MCP + post-back.
|
|
2553
|
-
cabane:
|
|
2554
|
-
mcpUrl:
|
|
2555
|
-
bearer:
|
|
2556
|
-
activeConversationId:
|
|
2459
|
+
cabane: z7.object({
|
|
2460
|
+
mcpUrl: z7.string(),
|
|
2461
|
+
bearer: z7.string(),
|
|
2462
|
+
activeConversationId: z7.string(),
|
|
2557
2463
|
// CT714: the scoped TURN-CONTROL MCP endpoint (`/api/turn-control`). The
|
|
2558
2464
|
// EXTERNAL adapters (Codex / opencode) mount it by URL under the key
|
|
2559
2465
|
// `cabane_companion` — using the same `bearer` (the turn token) and the same
|
|
@@ -2563,7 +2469,7 @@ var turnRequestSchema = z8.object({
|
|
|
2563
2469
|
// claude-code ignores it (it mounts the in-process instance instead), and
|
|
2564
2470
|
// every existing `cabane`-block fixture keeps parsing unchanged; the
|
|
2565
2471
|
// companion always populates it (`build-options.ts`).
|
|
2566
|
-
turnControlUrl:
|
|
2472
|
+
turnControlUrl: z7.string().optional(),
|
|
2567
2473
|
// CT598: the workspace this turn runs in. The claude-code/opencode/codex
|
|
2568
2474
|
// adapters never need it (they reach Cabane through the `cabane` MCP server,
|
|
2569
2475
|
// which takes `workspaceId` as a per-tool arg the model supplies); the
|
|
@@ -2573,53 +2479,39 @@ var turnRequestSchema = z8.object({
|
|
|
2573
2479
|
// adapters' conformance fixtures, tests) keeps parsing unchanged — the companion
|
|
2574
2480
|
// always populates it (`build-options.ts`), and the native adapter fails the
|
|
2575
2481
|
// turn loudly when it is somehow absent rather than guessing.
|
|
2576
|
-
workspaceId:
|
|
2482
|
+
workspaceId: z7.string().optional(),
|
|
2577
2483
|
// CT752: the server-resolved workspace surface this credential exposes.
|
|
2578
2484
|
// Readiness uses this explicit fact to require `sdk` for code mode and the
|
|
2579
2485
|
// granular floor for classic mode; inventory contents alone cannot infer it
|
|
2580
2486
|
// because `sdk` is intentionally also available on the classic surface.
|
|
2581
|
-
workspaceToolSurface:
|
|
2487
|
+
workspaceToolSurface: z7.enum(["code", "classic"]).optional()
|
|
2582
2488
|
}),
|
|
2583
2489
|
// Machine-local resolution (host-filled): the checkout cwd, extra env from a
|
|
2584
2490
|
// prepare hook, and the resolved user MCP servers.
|
|
2585
|
-
local:
|
|
2586
|
-
cwd:
|
|
2587
|
-
env:
|
|
2588
|
-
nativeWorkAssignment: z8.object({
|
|
2589
|
-
itemId: z8.string(),
|
|
2590
|
-
executionId: z8.string(),
|
|
2591
|
-
activationEpoch: z8.number().int().nonnegative()
|
|
2592
|
-
}).strict().optional(),
|
|
2491
|
+
local: z7.object({
|
|
2492
|
+
cwd: z7.string().optional(),
|
|
2493
|
+
env: z7.record(z7.string(), z7.string()).optional(),
|
|
2593
2494
|
mcpServers: resolvedMcpServersSchema.optional(),
|
|
2594
2495
|
// CT289: machine-local claude-code adapter knobs the operator sets on a
|
|
2595
2496
|
// companion they run themselves — the auto-memory escape hatch. `autoMemory:
|
|
2596
2497
|
// true` opts back into Claude Code's auto-memory (governed by the operator's
|
|
2597
2498
|
// own `.claude/settings.json`); absent/false leaves the adapter's force-off
|
|
2598
|
-
// default in place (see `buildClaudeCodeOptions`).
|
|
2599
|
-
|
|
2600
|
-
claudeCode: z8.object({ autoMemory: z8.boolean().optional() }).optional()
|
|
2499
|
+
// default in place (see `buildClaudeCodeOptions`).
|
|
2500
|
+
claudeCode: z7.object({ autoMemory: z7.boolean().optional() }).optional()
|
|
2601
2501
|
}),
|
|
2602
2502
|
// Host-owned injected servers (host-filled) — e.g. the summon server.
|
|
2603
|
-
extra:
|
|
2604
|
-
mcpServers: hostInjectedServersSchema
|
|
2605
|
-
// CT666: the per-turn turn-control HANDLER for the in-process cabane-native
|
|
2606
|
-
// runtime — the seam that gives it ask/wake_me/summon_agent/sub_agent/skip_turn
|
|
2607
|
-
// without a subprocess `cabane_companion` MCP server. Typed `unknown` for the same
|
|
2608
|
-
// reason as `mcpServers`: it's a live host object (a `NativeTurnControl` whose
|
|
2609
|
-
// methods close over the dispatcher's per-turn state), passed to the native
|
|
2610
|
-
// adapter WITHOUT the contract package inspecting it. The subprocess adapters
|
|
2611
|
-
// ignore it (they get the same verbs from the injected SDK server instead).
|
|
2612
|
-
turnControl: z8.unknown().optional()
|
|
2503
|
+
extra: z7.object({
|
|
2504
|
+
mcpServers: hostInjectedServersSchema
|
|
2613
2505
|
})
|
|
2614
2506
|
});
|
|
2615
2507
|
|
|
2616
2508
|
// packages/agent-runtime/src/conformance.ts
|
|
2617
|
-
import { z as
|
|
2618
|
-
var conformanceFixtureSchema =
|
|
2619
|
-
name:
|
|
2509
|
+
import { z as z8 } from "zod";
|
|
2510
|
+
var conformanceFixtureSchema = z8.object({
|
|
2511
|
+
name: z8.string(),
|
|
2620
2512
|
request: turnRequestSchema,
|
|
2621
|
-
nativeStream:
|
|
2622
|
-
expected:
|
|
2513
|
+
nativeStream: z8.array(z8.unknown()),
|
|
2514
|
+
expected: z8.array(turnEventSchema)
|
|
2623
2515
|
});
|
|
2624
2516
|
|
|
2625
2517
|
// packages/agent-runtime/src/transcript.ts
|
|
@@ -2629,8 +2521,8 @@ function createTerminalTextBuffer() {
|
|
|
2629
2521
|
async function safeEmit(emit, event, onError) {
|
|
2630
2522
|
try {
|
|
2631
2523
|
await emit(event);
|
|
2632
|
-
} catch (
|
|
2633
|
-
onError?.(
|
|
2524
|
+
} catch (err) {
|
|
2525
|
+
onError?.(err, event.type);
|
|
2634
2526
|
}
|
|
2635
2527
|
}
|
|
2636
2528
|
async function processAssistantMessage(msg, emit, pending, buffer, onError) {
|
|
@@ -2891,16 +2783,16 @@ var TurnPump = class {
|
|
|
2891
2783
|
// minimal note. Skipped when cancelled or already final. The held-text flush
|
|
2892
2784
|
// that precedes it is a classification concern, driven by the caller before
|
|
2893
2785
|
// this runs.
|
|
2894
|
-
async finalize(
|
|
2895
|
-
if (!
|
|
2786
|
+
async finalize(ok) {
|
|
2787
|
+
if (!ok || this.opts.signal.aborted || this.emittedFinal) return;
|
|
2896
2788
|
const body = this.lastProgressBody ?? this.opts.emptyFinalBody;
|
|
2897
2789
|
const seq = this.opts.nextSeq();
|
|
2898
2790
|
try {
|
|
2899
2791
|
await this.opts.commit.commitMessage({ body, kind: "final", seq });
|
|
2900
2792
|
this.emittedFinal = true;
|
|
2901
2793
|
this.finalReplyBody = body;
|
|
2902
|
-
} catch (
|
|
2903
|
-
this.opts.onError?.(
|
|
2794
|
+
} catch (err) {
|
|
2795
|
+
this.opts.onError?.(err, "empty-final");
|
|
2904
2796
|
}
|
|
2905
2797
|
}
|
|
2906
2798
|
// Whether the turn has committed its `final` row — read by the host to decide
|
|
@@ -2924,7 +2816,7 @@ import {
|
|
|
2924
2816
|
var CLAUDE_CODE_ADDENDUM = "";
|
|
2925
2817
|
|
|
2926
2818
|
// packages/agent-runtime/src/claude-code/policy.ts
|
|
2927
|
-
import { z as
|
|
2819
|
+
import { z as z9 } from "zod";
|
|
2928
2820
|
var HOST_FS_TOOLS = [
|
|
2929
2821
|
// shell + local filesystem
|
|
2930
2822
|
"Bash",
|
|
@@ -2974,30 +2866,20 @@ function withThinkingSummaries(thinking) {
|
|
|
2974
2866
|
if (thinking.type === "disabled") return thinking;
|
|
2975
2867
|
return { display: "summarized", ...thinking };
|
|
2976
2868
|
}
|
|
2977
|
-
var claudeCodeDialectSchema =
|
|
2978
|
-
thinking:
|
|
2979
|
-
|
|
2980
|
-
type:
|
|
2981
|
-
display:
|
|
2869
|
+
var claudeCodeDialectSchema = z9.object({
|
|
2870
|
+
thinking: z9.discriminatedUnion("type", [
|
|
2871
|
+
z9.object({
|
|
2872
|
+
type: z9.literal("adaptive"),
|
|
2873
|
+
display: z9.enum(["summarized", "omitted"]).optional()
|
|
2982
2874
|
}),
|
|
2983
|
-
|
|
2984
|
-
type:
|
|
2985
|
-
budgetTokens:
|
|
2986
|
-
display:
|
|
2875
|
+
z9.object({
|
|
2876
|
+
type: z9.literal("enabled"),
|
|
2877
|
+
budgetTokens: z9.number().int().positive().optional(),
|
|
2878
|
+
display: z9.enum(["summarized", "omitted"]).optional()
|
|
2987
2879
|
}),
|
|
2988
|
-
|
|
2880
|
+
z9.object({ type: z9.literal("disabled") })
|
|
2989
2881
|
]).optional(),
|
|
2990
|
-
|
|
2991
|
-
disallowedTools: z10.array(z10.string()).optional(),
|
|
2992
|
-
// Which claude-code harness shape to run. `coding` switches to the
|
|
2993
|
-
// `claude_code` preset + project settings + always-allow `PreToolUse` hook;
|
|
2994
|
-
// `custom`/`assistant` (or absent) use a plain-string prompt + bypass. This
|
|
2995
|
-
// is the claude-code-specific PRESET selector — kept distinct from
|
|
2996
|
-
// `policy.hostFs` (the host-fs BLOCK), because companion `custom` mode wants host
|
|
2997
|
-
// fs available (via its own allowlist) WITHOUT the coding harness, and in-app
|
|
2998
|
-
// `custom` wants host fs blocked — neither of which a single `hostFs` boolean
|
|
2999
|
-
// can express alongside the preset choice.
|
|
3000
|
-
mode: z10.enum(["assistant", "coding", "custom"]).optional()
|
|
2882
|
+
hostAccess: z9.boolean().optional()
|
|
3001
2883
|
}).loose();
|
|
3002
2884
|
function readThinking(runtimeOptions) {
|
|
3003
2885
|
const dialect = runtimeOptions?.["claude-code"];
|
|
@@ -3076,18 +2958,15 @@ function buildClaudeCodeOptions(req, augment) {
|
|
|
3076
2958
|
};
|
|
3077
2959
|
}
|
|
3078
2960
|
const dialect = claudeCodeDialectSchema.safeParse(config.runtimeOptions?.["claude-code"] ?? {});
|
|
3079
|
-
const
|
|
3080
|
-
const customDisallowed = dialect.success ? dialect.data.disallowedTools ?? [] : [];
|
|
3081
|
-
const useCodingPreset = (dialect.success ? dialect.data.mode : void 0) === "coding";
|
|
2961
|
+
const useCodingPreset = dialect.success ? dialect.data.hostAccess ?? false : false;
|
|
3082
2962
|
const cabaneGlob = `mcp__${CABANE_MCP_SERVER}__*`;
|
|
3083
2963
|
const extraServerGlobs = Object.keys(req.extra.mcpServers).map((name) => `mcp__${name}__*`);
|
|
3084
2964
|
const allowedTools = dedupe([
|
|
3085
2965
|
cabaneGlob,
|
|
3086
2966
|
...extraServerGlobs,
|
|
3087
|
-
...policy.web ? DEFAULT_WEB_TOOLS : []
|
|
3088
|
-
...customAllowed
|
|
2967
|
+
...policy.web ? DEFAULT_WEB_TOOLS : []
|
|
3089
2968
|
]);
|
|
3090
|
-
const disallowedTools = dedupe([...disallowedToolsFor(policy)
|
|
2969
|
+
const disallowedTools = dedupe([...disallowedToolsFor(policy)]);
|
|
3091
2970
|
const resumeDecision = decideResume(req.session, cwd);
|
|
3092
2971
|
const resume = "resume" in resumeDecision ? resumeDecision.resume : null;
|
|
3093
2972
|
const freshReason = "fresh" in resumeDecision ? resumeDecision.reason : void 0;
|
|
@@ -3153,7 +3032,7 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
3153
3032
|
out.push(event);
|
|
3154
3033
|
};
|
|
3155
3034
|
let sessionEmitted = false;
|
|
3156
|
-
let
|
|
3035
|
+
let ok = false;
|
|
3157
3036
|
let resultReason;
|
|
3158
3037
|
let sawResult = false;
|
|
3159
3038
|
let usage;
|
|
@@ -3199,8 +3078,8 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
3199
3078
|
if (typeof info.rateLimitType === "string") rateLimitType = info.rateLimitType;
|
|
3200
3079
|
}
|
|
3201
3080
|
} else if (msg.type === "auth_status") {
|
|
3202
|
-
const
|
|
3203
|
-
if (typeof
|
|
3081
|
+
const err = msg.error;
|
|
3082
|
+
if (typeof err === "string" && err.length > 0) authError = err;
|
|
3204
3083
|
} else if (msg.type === "result") {
|
|
3205
3084
|
sawResult = true;
|
|
3206
3085
|
usage = readSdkUsage(msg);
|
|
@@ -3212,7 +3091,7 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
3212
3091
|
}
|
|
3213
3092
|
const isError = msg.is_error === true;
|
|
3214
3093
|
if (msg.subtype === "success" && !isError) {
|
|
3215
|
-
|
|
3094
|
+
ok = true;
|
|
3216
3095
|
} else {
|
|
3217
3096
|
const resultText = msg.result ?? "";
|
|
3218
3097
|
const terminalReason = msg.terminal_reason;
|
|
@@ -3226,26 +3105,26 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
3226
3105
|
...rateLimitResetIso ? { resetsAt: rateLimitResetIso } : {}
|
|
3227
3106
|
} : classifyAssistantError(lastAssistantError) ?? classifyErrorText([authError, errorText].filter(Boolean).join(" "));
|
|
3228
3107
|
resultReason = failure ? encodeFailureReason(failure) : isError ? `error:${resultText.slice(0, 200) || "unknown"}` : `result_error:${msg.subtype}`;
|
|
3229
|
-
|
|
3108
|
+
ok = false;
|
|
3230
3109
|
}
|
|
3231
3110
|
break;
|
|
3232
3111
|
}
|
|
3233
3112
|
}
|
|
3234
|
-
} catch (
|
|
3235
|
-
if (ctx.signal.aborted) throw
|
|
3236
|
-
const failure = classifyErrorText(
|
|
3237
|
-
if (!failure) throw
|
|
3238
|
-
|
|
3113
|
+
} catch (err) {
|
|
3114
|
+
if (ctx.signal.aborted) throw err;
|
|
3115
|
+
const failure = classifyErrorText(err instanceof Error ? err.message : String(err));
|
|
3116
|
+
if (!failure) throw err;
|
|
3117
|
+
ok = false;
|
|
3239
3118
|
resultReason = encodeFailureReason(failure);
|
|
3240
3119
|
sawResult = true;
|
|
3241
3120
|
}
|
|
3242
3121
|
if (ctx.signal.aborted) return;
|
|
3243
|
-
await flushHeldText(buffer, emit,
|
|
3122
|
+
await flushHeldText(buffer, emit, ok);
|
|
3244
3123
|
yield* drain(out);
|
|
3245
|
-
if (!
|
|
3124
|
+
if (!ok && !resultReason && !sawResult) resultReason = "no_result";
|
|
3246
3125
|
yield {
|
|
3247
3126
|
type: "result",
|
|
3248
|
-
ok
|
|
3127
|
+
ok,
|
|
3249
3128
|
...resultReason ? { reason: resultReason } : {},
|
|
3250
3129
|
...usage ? { usage } : {},
|
|
3251
3130
|
...resolvedModel ? { resolvedModel } : {}
|
|
@@ -3808,9 +3687,9 @@ function readSessionId(properties) {
|
|
|
3808
3687
|
}
|
|
3809
3688
|
function readSessionError(properties) {
|
|
3810
3689
|
const props = asRecord(properties);
|
|
3811
|
-
const
|
|
3812
|
-
if (typeof
|
|
3813
|
-
const rec2 = asRecord(
|
|
3690
|
+
const err = props?.error;
|
|
3691
|
+
if (typeof err === "string") return err;
|
|
3692
|
+
const rec2 = asRecord(err);
|
|
3814
3693
|
if (!rec2) return "unknown";
|
|
3815
3694
|
const { name, message } = deepestError(rec2);
|
|
3816
3695
|
if (message && name && !isGenericErrorName(name)) return `${name}: ${message}`;
|
|
@@ -3846,7 +3725,7 @@ async function* decodeOpencodeStream(events, ctx) {
|
|
|
3846
3725
|
const pending = /* @__PURE__ */ new Map();
|
|
3847
3726
|
const startedTools = /* @__PURE__ */ new Set();
|
|
3848
3727
|
const finishedTools = /* @__PURE__ */ new Set();
|
|
3849
|
-
let
|
|
3728
|
+
let ok = false;
|
|
3850
3729
|
let reason;
|
|
3851
3730
|
let settled = false;
|
|
3852
3731
|
const userMessageIds = /* @__PURE__ */ new Set();
|
|
@@ -3909,7 +3788,7 @@ async function* decodeOpencodeStream(events, ctx) {
|
|
|
3909
3788
|
const sealed = sealHeld(held, true);
|
|
3910
3789
|
held = null;
|
|
3911
3790
|
if (sealed) yield sealed;
|
|
3912
|
-
|
|
3791
|
+
ok = true;
|
|
3913
3792
|
settled = true;
|
|
3914
3793
|
break;
|
|
3915
3794
|
} else if (ev.type === "session.error") {
|
|
@@ -3918,7 +3797,7 @@ async function* decodeOpencodeStream(events, ctx) {
|
|
|
3918
3797
|
const sealed = sealHeld(held, false);
|
|
3919
3798
|
held = null;
|
|
3920
3799
|
if (sealed) yield sealed;
|
|
3921
|
-
|
|
3800
|
+
ok = false;
|
|
3922
3801
|
const errorText = readSessionError(ev.properties);
|
|
3923
3802
|
const failure = classifyErrorText(errorText);
|
|
3924
3803
|
reason = failure ? encodeFailureReason(failure) : `error:${errorText.slice(0, 200)}`;
|
|
@@ -3933,7 +3812,7 @@ async function* decodeOpencodeStream(events, ctx) {
|
|
|
3933
3812
|
if (sealed) yield sealed;
|
|
3934
3813
|
reason = "no_terminal";
|
|
3935
3814
|
}
|
|
3936
|
-
yield { type: "result", ok
|
|
3815
|
+
yield { type: "result", ok, ...reason ? { reason } : {} };
|
|
3937
3816
|
}
|
|
3938
3817
|
function hasToolInput(input) {
|
|
3939
3818
|
return !!input && typeof input === "object" && Object.keys(input).length > 0;
|
|
@@ -3947,7 +3826,7 @@ function sealHeld(held, terminal) {
|
|
|
3947
3826
|
}
|
|
3948
3827
|
|
|
3949
3828
|
// packages/agent-runtime/src/opencode/policy.ts
|
|
3950
|
-
import { z as
|
|
3829
|
+
import { z as z10 } from "zod";
|
|
3951
3830
|
var OPENCODE_HOST_TOOLS = [
|
|
3952
3831
|
"bash",
|
|
3953
3832
|
"edit",
|
|
@@ -3974,8 +3853,8 @@ function opencodeToolPolicy(policy) {
|
|
|
3974
3853
|
deny(OPENCODE_UI_PROMPT_TOOLS);
|
|
3975
3854
|
return { tools, allowAllHostTools: policy.hostFs };
|
|
3976
3855
|
}
|
|
3977
|
-
var opencodeDialectSchema =
|
|
3978
|
-
agent:
|
|
3856
|
+
var opencodeDialectSchema = z10.object({
|
|
3857
|
+
agent: z10.string().min(1).optional()
|
|
3979
3858
|
}).loose();
|
|
3980
3859
|
function readOpencodeDialect(runtimeOptions) {
|
|
3981
3860
|
const parsed = opencodeDialectSchema.safeParse(runtimeOptions?.["opencode"] ?? {});
|
|
@@ -4196,9 +4075,9 @@ function createHttpOpencodeTransport(opts) {
|
|
|
4196
4075
|
// The lock is released when this stream finishes draining.
|
|
4197
4076
|
events: releaseAfter(parseSseStream(eventRes.body, sessionId, signal), release)
|
|
4198
4077
|
};
|
|
4199
|
-
} catch (
|
|
4078
|
+
} catch (err) {
|
|
4200
4079
|
release();
|
|
4201
|
-
throw
|
|
4080
|
+
throw err;
|
|
4202
4081
|
}
|
|
4203
4082
|
}
|
|
4204
4083
|
};
|
|
@@ -4791,9 +4670,9 @@ function readItemType(item) {
|
|
|
4791
4670
|
function readErrorMessage(ev) {
|
|
4792
4671
|
const direct = str(ev.message);
|
|
4793
4672
|
if (direct) return direct;
|
|
4794
|
-
const
|
|
4795
|
-
if (
|
|
4796
|
-
const m = str(
|
|
4673
|
+
const err = asRecord2(ev.error);
|
|
4674
|
+
if (err) {
|
|
4675
|
+
const m = str(err.message);
|
|
4797
4676
|
if (m) return m;
|
|
4798
4677
|
}
|
|
4799
4678
|
return "unknown";
|
|
@@ -4849,7 +4728,7 @@ async function* decodeCodexStream(events, ctx) {
|
|
|
4849
4728
|
const startedTools = /* @__PURE__ */ new Set();
|
|
4850
4729
|
const finishedTools = /* @__PURE__ */ new Set();
|
|
4851
4730
|
let sessionEmitted = false;
|
|
4852
|
-
let
|
|
4731
|
+
let ok = false;
|
|
4853
4732
|
let reason;
|
|
4854
4733
|
let usage;
|
|
4855
4734
|
let settled = false;
|
|
@@ -4880,7 +4759,7 @@ async function* decodeCodexStream(events, ctx) {
|
|
|
4880
4759
|
const message = readItemMessage(item);
|
|
4881
4760
|
if (isModelMetadataError(message)) {
|
|
4882
4761
|
yield* flushInterim();
|
|
4883
|
-
|
|
4762
|
+
ok = false;
|
|
4884
4763
|
reason = `model_unavailable:${message.slice(0, 200)}`;
|
|
4885
4764
|
settled = true;
|
|
4886
4765
|
break;
|
|
@@ -4935,13 +4814,13 @@ async function* decodeCodexStream(events, ctx) {
|
|
|
4935
4814
|
held = null;
|
|
4936
4815
|
if (sealed) yield sealed;
|
|
4937
4816
|
usage = readUsage(ev);
|
|
4938
|
-
|
|
4817
|
+
ok = true;
|
|
4939
4818
|
settled = true;
|
|
4940
4819
|
break;
|
|
4941
4820
|
}
|
|
4942
4821
|
if (ev.type === "turn.failed" || ev.type === "error") {
|
|
4943
4822
|
yield* flushInterim();
|
|
4944
|
-
|
|
4823
|
+
ok = false;
|
|
4945
4824
|
const text = readErrorMessage(ev);
|
|
4946
4825
|
const failure = classifyErrorText(text);
|
|
4947
4826
|
reason = failure ? encodeFailureReason(failure) : `error:${text.slice(0, 200)}`;
|
|
@@ -4959,7 +4838,7 @@ async function* decodeCodexStream(events, ctx) {
|
|
|
4959
4838
|
const resolvedConfig = ctx.resolvedReasoningEffort ? { reasoningEffort: ctx.resolvedReasoningEffort } : void 0;
|
|
4960
4839
|
yield {
|
|
4961
4840
|
type: "result",
|
|
4962
|
-
ok
|
|
4841
|
+
ok,
|
|
4963
4842
|
...reason ? { reason } : {},
|
|
4964
4843
|
...usage ? { usage } : {},
|
|
4965
4844
|
...ctx.resolvedModel ? { resolvedModel: ctx.resolvedModel } : {},
|
|
@@ -4977,7 +4856,7 @@ function sealHeld2(held, terminal) {
|
|
|
4977
4856
|
}
|
|
4978
4857
|
|
|
4979
4858
|
// packages/agent-runtime/src/codex/policy.ts
|
|
4980
|
-
import { z as
|
|
4859
|
+
import { z as z11 } from "zod";
|
|
4981
4860
|
function codexToolPolicy(policy) {
|
|
4982
4861
|
return policy.hostFs ? {
|
|
4983
4862
|
permissionProfile: "cabane-coding",
|
|
@@ -4992,8 +4871,8 @@ function codexToolPolicy(policy) {
|
|
|
4992
4871
|
};
|
|
4993
4872
|
}
|
|
4994
4873
|
var CODEX_REASONING_EFFORTS = ["minimal", "low", "medium", "high", "xhigh"];
|
|
4995
|
-
var codexDialectSchema =
|
|
4996
|
-
modelReasoningEffort:
|
|
4874
|
+
var codexDialectSchema = z11.object({
|
|
4875
|
+
modelReasoningEffort: z11.enum(CODEX_REASONING_EFFORTS).optional()
|
|
4997
4876
|
}).loose();
|
|
4998
4877
|
function readCodexDialect(runtimeOptions) {
|
|
4999
4878
|
const parsed = codexDialectSchema.safeParse(runtimeOptions?.["codex"] ?? {});
|
|
@@ -5001,7 +4880,7 @@ function readCodexDialect(runtimeOptions) {
|
|
|
5001
4880
|
}
|
|
5002
4881
|
|
|
5003
4882
|
// packages/agent-runtime/src/codex/model.ts
|
|
5004
|
-
var CODEX_DEFAULT_MODEL_BARES = /* @__PURE__ */ new Set(["default"
|
|
4883
|
+
var CODEX_DEFAULT_MODEL_BARES = /* @__PURE__ */ new Set(["default"]);
|
|
5005
4884
|
function parseCodexModel(model) {
|
|
5006
4885
|
const sep = model.indexOf("/");
|
|
5007
4886
|
const bare = sep === -1 || model.slice(0, sep) !== "openai" ? model : model.slice(sep + 1);
|
|
@@ -5088,9 +4967,11 @@ function buildConfig(req) {
|
|
|
5088
4967
|
};
|
|
5089
4968
|
}
|
|
5090
4969
|
const policy = codexToolPolicy(req.policy);
|
|
4970
|
+
const tmpDir = req.local.env?.TMPDIR;
|
|
5091
4971
|
return {
|
|
5092
4972
|
mcp_servers,
|
|
5093
4973
|
experimental_use_rmcp_client: true,
|
|
4974
|
+
...tmpDir ? { shell_environment_policy: { set: { TMPDIR: tmpDir } } } : {},
|
|
5094
4975
|
...policy.permissionProfile ? {
|
|
5095
4976
|
// CT733: named permission profiles are Codex's split-filesystem path.
|
|
5096
4977
|
// `:root = read` preserves coding-mode host reads; the one explicit
|
|
@@ -5368,7 +5249,7 @@ var CodexExec = class {
|
|
|
5368
5249
|
signal: args.signal
|
|
5369
5250
|
});
|
|
5370
5251
|
let spawnError = null;
|
|
5371
|
-
child.once("error", (
|
|
5252
|
+
child.once("error", (err) => spawnError = err);
|
|
5372
5253
|
if (!child.stdin) {
|
|
5373
5254
|
child.kill();
|
|
5374
5255
|
throw new Error("Child process has no stdin");
|
|
@@ -6272,1027 +6153,6 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6272
6153
|
}
|
|
6273
6154
|
];
|
|
6274
6155
|
|
|
6275
|
-
// packages/agent-runtime/src/cabane-native/addendum.ts
|
|
6276
|
-
var CABANE_NATIVE_ADDENDUM = `## Your tools (native runtime)
|
|
6277
|
-
|
|
6278
|
-
You are running on Cabane's own agent runtime. You have a small, curated set of workspace tools, all prefixed \`cabane_\`:
|
|
6279
|
-
|
|
6280
|
-
- \`cabane_list\` \u2014 list a folder's files and subfolders.
|
|
6281
|
-
- \`cabane_read\` \u2014 read one file's contents by path.
|
|
6282
|
-
- \`cabane_search\` \u2014 substring search across file/folder names, file contents, and conversations.
|
|
6283
|
-
- \`cabane_write\` \u2014 create or overwrite a file (pass \`overwrite: true\` to replace).
|
|
6284
|
-
- \`cabane_edit\` \u2014 find/replace inside an existing file.
|
|
6285
|
-
- \`cabane_mkdir\` \u2014 create a folder (pass \`recursive: true\` to also make missing parents).
|
|
6286
|
-
- \`cabane_move\` \u2014 move or rename a file or folder.
|
|
6287
|
-
- \`cabane_delete\` \u2014 delete a file or folder (folders need \`recursive: true\`; soft-delete, recoverable).
|
|
6288
|
-
- \`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.
|
|
6289
|
-
- \`cabane_messages\` \u2014 read a conversation's messages by id (from \`cabane_search\` / \`cabane_context\`).
|
|
6290
|
-
|
|
6291
|
-
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.`;
|
|
6292
|
-
var CABANE_NATIVE_ADDENDUM_CODE_MODE = `## Your tools (native runtime \u2014 code mode)
|
|
6293
|
-
|
|
6294
|
-
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.
|
|
6295
|
-
|
|
6296
|
-
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.
|
|
6297
|
-
|
|
6298
|
-
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):
|
|
6299
|
-
- \`ask\` \u2014 put a structured question to a human and END your turn (they may answer in days).
|
|
6300
|
-
- \`wake_me\` \u2014 end this turn now and be re-dispatched later to check a condition (a PR merging, a reply landing).
|
|
6301
|
-
- \`summon_agent\` \u2014 pull a peer agent into THIS conversation to reply on your turn.
|
|
6302
|
-
- \`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).
|
|
6303
|
-
- \`skip_turn\` \u2014 end your turn with NO reply, when the message doesn't need one from you.
|
|
6304
|
-
- \`mint_render_token\` \u2014 mint a short-lived credential to load a workspace HTML page in a browser.
|
|
6305
|
-
|
|
6306
|
-
Your reply text is streamed straight into the conversation; there is no separate send step.`;
|
|
6307
|
-
|
|
6308
|
-
// packages/agent-runtime/src/cabane-native/context.ts
|
|
6309
|
-
var DEFAULT_HISTORY_LIMIT = 20;
|
|
6310
|
-
var DEFAULT_MAX_HISTORY_CHARS = 24e3;
|
|
6311
|
-
async function assembleMessages(systemPrompt, content, fallbackPrompt, opts) {
|
|
6312
|
-
const messages = [{ role: "system", content: systemPrompt }];
|
|
6313
|
-
const history = await fetchRecentHistory(opts);
|
|
6314
|
-
for (const m of history) messages.push(m);
|
|
6315
|
-
messages.push({ role: "user", content: currentUserText(content, fallbackPrompt) });
|
|
6316
|
-
return messages;
|
|
6317
|
-
}
|
|
6318
|
-
function currentUserText(content, fallbackPrompt) {
|
|
6319
|
-
const text = content.filter((b) => b.type === "text").map((b) => b.text).join("\n").trim();
|
|
6320
|
-
return text.length > 0 ? text : fallbackPrompt;
|
|
6321
|
-
}
|
|
6322
|
-
async function fetchRecentHistory(opts) {
|
|
6323
|
-
const doFetch = opts.fetchImpl ?? fetch;
|
|
6324
|
-
const limit = opts.historyLimit ?? DEFAULT_HISTORY_LIMIT;
|
|
6325
|
-
const url = new URL(
|
|
6326
|
-
`${opts.apiRoot}/workspaces/${opts.workspaceId}/conversations/${opts.conversationId}/messages`
|
|
6327
|
-
);
|
|
6328
|
-
url.searchParams.set("limit", String(limit));
|
|
6329
|
-
url.searchParams.set("order", "desc");
|
|
6330
|
-
let rows;
|
|
6331
|
-
try {
|
|
6332
|
-
const res = await doFetch(url.toString(), {
|
|
6333
|
-
headers: { Authorization: `Bearer ${opts.bearer}` }
|
|
6334
|
-
});
|
|
6335
|
-
if (!res.ok) return [];
|
|
6336
|
-
const body = await res.json();
|
|
6337
|
-
rows = body.messages ?? [];
|
|
6338
|
-
} catch {
|
|
6339
|
-
return [];
|
|
6340
|
-
}
|
|
6341
|
-
const chronological = [...rows].reverse();
|
|
6342
|
-
while (chronological.length > 0 && chronological[chronological.length - 1].role === "user") {
|
|
6343
|
-
chronological.pop();
|
|
6344
|
-
}
|
|
6345
|
-
const mapped = [];
|
|
6346
|
-
for (const r of chronological) {
|
|
6347
|
-
const role = r.role === "agent" ? "assistant" : r.role === "user" ? "user" : null;
|
|
6348
|
-
if (!role) continue;
|
|
6349
|
-
const body = (r.body ?? "").trim();
|
|
6350
|
-
if (body.length === 0) continue;
|
|
6351
|
-
mapped.push({ role, content: body });
|
|
6352
|
-
}
|
|
6353
|
-
return capHistory(mapped, opts.maxHistoryChars ?? DEFAULT_MAX_HISTORY_CHARS);
|
|
6354
|
-
}
|
|
6355
|
-
function capHistory(messages, maxChars) {
|
|
6356
|
-
let total = messages.reduce((n, m) => n + m.content.length, 0);
|
|
6357
|
-
let start2 = 0;
|
|
6358
|
-
while (total > maxChars && start2 < messages.length) {
|
|
6359
|
-
total -= messages[start2].content.length;
|
|
6360
|
-
start2 += 1;
|
|
6361
|
-
}
|
|
6362
|
-
return messages.slice(start2);
|
|
6363
|
-
}
|
|
6364
|
-
|
|
6365
|
-
// packages/agent-runtime/src/cabane-native/model.ts
|
|
6366
|
-
var CABANE_NATIVE_MODEL_PREFIX = "cabane-native/";
|
|
6367
|
-
function parseCabaneNativeModel(model) {
|
|
6368
|
-
return model.startsWith(CABANE_NATIVE_MODEL_PREFIX) ? model.slice(CABANE_NATIVE_MODEL_PREFIX.length) : model;
|
|
6369
|
-
}
|
|
6370
|
-
|
|
6371
|
-
// packages/agent-runtime/src/cabane-native/policy.ts
|
|
6372
|
-
import { z as z13 } from "zod";
|
|
6373
|
-
var NATIVE_SURFACES = ["code", "classic"];
|
|
6374
|
-
var cabaneNativeDialectSchema = z13.object({ surface: z13.enum(NATIVE_SURFACES).optional() }).loose();
|
|
6375
|
-
function readCabaneNativeDialect(runtimeOptions) {
|
|
6376
|
-
const parsed = cabaneNativeDialectSchema.safeParse(runtimeOptions?.["cabane-native"] ?? {});
|
|
6377
|
-
return parsed.success ? parsed.data : {};
|
|
6378
|
-
}
|
|
6379
|
-
function cabaneNativeSurface(runtimeOptions) {
|
|
6380
|
-
return readCabaneNativeDialect(runtimeOptions).surface ?? "code";
|
|
6381
|
-
}
|
|
6382
|
-
|
|
6383
|
-
// packages/agent-runtime/src/cabane-native/tools.ts
|
|
6384
|
-
var TOOL_RESULT_MAX_CHARS = 8e3;
|
|
6385
|
-
var CABANE_NATIVE_TOOLS = [
|
|
6386
|
-
{
|
|
6387
|
-
type: "function",
|
|
6388
|
-
function: {
|
|
6389
|
-
name: "cabane_list",
|
|
6390
|
-
description: "List the files and subfolders at a workspace folder path. Omit `path` for the root.",
|
|
6391
|
-
parameters: {
|
|
6392
|
-
type: "object",
|
|
6393
|
-
properties: {
|
|
6394
|
-
path: {
|
|
6395
|
-
type: "string",
|
|
6396
|
-
description: "Workspace folder path, e.g. /notes. Defaults to /."
|
|
6397
|
-
}
|
|
6398
|
-
}
|
|
6399
|
-
}
|
|
6400
|
-
}
|
|
6401
|
-
},
|
|
6402
|
-
{
|
|
6403
|
-
type: "function",
|
|
6404
|
-
function: {
|
|
6405
|
-
name: "cabane_read",
|
|
6406
|
-
description: "Read the contents of one file at a workspace path.",
|
|
6407
|
-
parameters: {
|
|
6408
|
-
type: "object",
|
|
6409
|
-
properties: {
|
|
6410
|
-
path: { type: "string", description: "Workspace file path, e.g. /notes/todo.md." }
|
|
6411
|
-
},
|
|
6412
|
-
required: ["path"]
|
|
6413
|
-
}
|
|
6414
|
-
}
|
|
6415
|
-
},
|
|
6416
|
-
{
|
|
6417
|
-
type: "function",
|
|
6418
|
-
function: {
|
|
6419
|
-
name: "cabane_search",
|
|
6420
|
-
description: "Case-insensitive substring search across file names and file contents. Optionally scope to a subtree with `path`.",
|
|
6421
|
-
parameters: {
|
|
6422
|
-
type: "object",
|
|
6423
|
-
properties: {
|
|
6424
|
-
q: { type: "string", description: "The search string." },
|
|
6425
|
-
path: {
|
|
6426
|
-
type: "string",
|
|
6427
|
-
description: "Optional workspace subtree to scope the search to."
|
|
6428
|
-
}
|
|
6429
|
-
},
|
|
6430
|
-
required: ["q"]
|
|
6431
|
-
}
|
|
6432
|
-
}
|
|
6433
|
-
},
|
|
6434
|
-
{
|
|
6435
|
-
type: "function",
|
|
6436
|
-
function: {
|
|
6437
|
-
name: "cabane_write",
|
|
6438
|
-
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.",
|
|
6439
|
-
parameters: {
|
|
6440
|
-
type: "object",
|
|
6441
|
-
properties: {
|
|
6442
|
-
path: { type: "string", description: "Workspace file path, e.g. /notes/new.md." },
|
|
6443
|
-
content: { type: "string", description: "The file contents." },
|
|
6444
|
-
overwrite: { type: "boolean", description: "Replace an existing file (default false)." }
|
|
6445
|
-
},
|
|
6446
|
-
required: ["path", "content"]
|
|
6447
|
-
}
|
|
6448
|
-
}
|
|
6449
|
-
},
|
|
6450
|
-
{
|
|
6451
|
-
type: "function",
|
|
6452
|
-
function: {
|
|
6453
|
-
name: "cabane_edit",
|
|
6454
|
-
description: "Modify an existing file with a single find/replace. By default `find` must occur exactly once; set `replaceAll: true` to replace every occurrence.",
|
|
6455
|
-
parameters: {
|
|
6456
|
-
type: "object",
|
|
6457
|
-
properties: {
|
|
6458
|
-
path: { type: "string", description: "Workspace file path to edit." },
|
|
6459
|
-
find: { type: "string", description: "The substring to find." },
|
|
6460
|
-
replace: { type: "string", description: "The replacement." },
|
|
6461
|
-
replaceAll: { type: "boolean", description: "Replace every occurrence (default false)." }
|
|
6462
|
-
},
|
|
6463
|
-
required: ["path", "find", "replace"]
|
|
6464
|
-
}
|
|
6465
|
-
}
|
|
6466
|
-
},
|
|
6467
|
-
{
|
|
6468
|
-
type: "function",
|
|
6469
|
-
function: {
|
|
6470
|
-
name: "cabane_mkdir",
|
|
6471
|
-
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.",
|
|
6472
|
-
parameters: {
|
|
6473
|
-
type: "object",
|
|
6474
|
-
properties: {
|
|
6475
|
-
path: { type: "string", description: "Workspace folder path, e.g. /notes/archive." },
|
|
6476
|
-
recursive: {
|
|
6477
|
-
type: "boolean",
|
|
6478
|
-
description: "Create missing parent folders too (default false)."
|
|
6479
|
-
}
|
|
6480
|
-
},
|
|
6481
|
-
required: ["path"]
|
|
6482
|
-
}
|
|
6483
|
-
}
|
|
6484
|
-
},
|
|
6485
|
-
{
|
|
6486
|
-
type: "function",
|
|
6487
|
-
function: {
|
|
6488
|
-
name: "cabane_move",
|
|
6489
|
-
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.",
|
|
6490
|
-
parameters: {
|
|
6491
|
-
type: "object",
|
|
6492
|
-
properties: {
|
|
6493
|
-
fromPath: { type: "string", description: "The current file/folder path." },
|
|
6494
|
-
toPath: { type: "string", description: "The new full path (parent + name)." }
|
|
6495
|
-
},
|
|
6496
|
-
required: ["fromPath", "toPath"]
|
|
6497
|
-
}
|
|
6498
|
-
}
|
|
6499
|
-
},
|
|
6500
|
-
{
|
|
6501
|
-
type: "function",
|
|
6502
|
-
function: {
|
|
6503
|
-
name: "cabane_delete",
|
|
6504
|
-
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.",
|
|
6505
|
-
parameters: {
|
|
6506
|
-
type: "object",
|
|
6507
|
-
properties: {
|
|
6508
|
-
path: { type: "string", description: "Workspace file/folder path to delete." },
|
|
6509
|
-
recursive: {
|
|
6510
|
-
type: "boolean",
|
|
6511
|
-
description: "Required to delete a non-empty folder (default false)."
|
|
6512
|
-
}
|
|
6513
|
-
},
|
|
6514
|
-
required: ["path"]
|
|
6515
|
-
}
|
|
6516
|
-
}
|
|
6517
|
-
},
|
|
6518
|
-
{
|
|
6519
|
-
type: "function",
|
|
6520
|
-
function: {
|
|
6521
|
-
name: "cabane_context",
|
|
6522
|
-
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`.",
|
|
6523
|
-
parameters: {
|
|
6524
|
-
type: "object",
|
|
6525
|
-
properties: {
|
|
6526
|
-
kind: {
|
|
6527
|
-
type: "string",
|
|
6528
|
-
enum: ["conversation", "file", "folder", "user", "agent", "channel"],
|
|
6529
|
-
description: "The kind of object to orient around."
|
|
6530
|
-
},
|
|
6531
|
-
ref: {
|
|
6532
|
-
type: "string",
|
|
6533
|
-
description: "The object id (or the workspace path for a file/folder)."
|
|
6534
|
-
}
|
|
6535
|
-
},
|
|
6536
|
-
required: ["kind", "ref"]
|
|
6537
|
-
}
|
|
6538
|
-
}
|
|
6539
|
-
},
|
|
6540
|
-
{
|
|
6541
|
-
type: "function",
|
|
6542
|
-
function: {
|
|
6543
|
-
name: "cabane_messages",
|
|
6544
|
-
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.',
|
|
6545
|
-
parameters: {
|
|
6546
|
-
type: "object",
|
|
6547
|
-
properties: {
|
|
6548
|
-
conversationId: { type: "string", description: "The conversation id to read." },
|
|
6549
|
-
order: {
|
|
6550
|
-
type: "string",
|
|
6551
|
-
enum: ["asc", "desc"],
|
|
6552
|
-
description: "newest-first (desc, default) or oldest-first (asc)."
|
|
6553
|
-
},
|
|
6554
|
-
limit: {
|
|
6555
|
-
type: "number",
|
|
6556
|
-
description: "Max messages to return (default 50, max 100)."
|
|
6557
|
-
}
|
|
6558
|
-
},
|
|
6559
|
-
required: ["conversationId"]
|
|
6560
|
-
}
|
|
6561
|
-
}
|
|
6562
|
-
}
|
|
6563
|
-
];
|
|
6564
|
-
var CABANE_NATIVE_CODE_TOOL = {
|
|
6565
|
-
type: "function",
|
|
6566
|
-
function: {
|
|
6567
|
-
name: "sdk",
|
|
6568
|
-
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.",
|
|
6569
|
-
parameters: {
|
|
6570
|
-
type: "object",
|
|
6571
|
-
properties: {
|
|
6572
|
-
code: {
|
|
6573
|
-
type: "string",
|
|
6574
|
-
description: "The TypeScript program to run (an async function body)."
|
|
6575
|
-
}
|
|
6576
|
-
},
|
|
6577
|
-
required: ["code"]
|
|
6578
|
-
}
|
|
6579
|
-
}
|
|
6580
|
-
};
|
|
6581
|
-
var CABANE_NATIVE_RENDER_TOKEN_TOOL = {
|
|
6582
|
-
type: "function",
|
|
6583
|
-
function: {
|
|
6584
|
-
name: "mint_render_token",
|
|
6585
|
-
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.",
|
|
6586
|
-
parameters: {
|
|
6587
|
-
type: "object",
|
|
6588
|
-
properties: {
|
|
6589
|
-
pathPrefix: {
|
|
6590
|
-
type: "string",
|
|
6591
|
-
description: "Workspace-rooted path-prefix the token authorizes; `/` for whole-workspace."
|
|
6592
|
-
},
|
|
6593
|
-
ttlSeconds: {
|
|
6594
|
-
type: "number",
|
|
6595
|
-
description: "Override the default 30-minute TTL; capped at 24h (86400s)."
|
|
6596
|
-
}
|
|
6597
|
-
}
|
|
6598
|
-
}
|
|
6599
|
-
}
|
|
6600
|
-
};
|
|
6601
|
-
function summarizeCabaneToolArgs(name, args) {
|
|
6602
|
-
if (name === "mint_render_token") return str2(args.pathPrefix) ?? "/";
|
|
6603
|
-
if (name === "sdk") {
|
|
6604
|
-
const code = str2(args.code) ?? "";
|
|
6605
|
-
const firstLine = code.split("\n").find((l) => l.trim().length > 0)?.trim() ?? "";
|
|
6606
|
-
return firstLine.length > 80 ? `${firstLine.slice(0, 80)}\u2026` : firstLine;
|
|
6607
|
-
}
|
|
6608
|
-
if (name === "cabane_search") return str2(args.q) ?? "";
|
|
6609
|
-
if (name === "cabane_move") {
|
|
6610
|
-
const from = str2(args.fromPath) ?? "";
|
|
6611
|
-
const to = str2(args.toPath) ?? "";
|
|
6612
|
-
return from && to ? `${from} \u2192 ${to}` : from || to;
|
|
6613
|
-
}
|
|
6614
|
-
if (name === "cabane_context") return str2(args.ref) ?? "";
|
|
6615
|
-
if (name === "cabane_messages") return str2(args.conversationId) ?? "";
|
|
6616
|
-
return str2(args.path) ?? "";
|
|
6617
|
-
}
|
|
6618
|
-
async function executeCabaneNativeTool(name, args, ctx, signal) {
|
|
6619
|
-
const doFetch = ctx.fetchImpl ?? fetch;
|
|
6620
|
-
const wsBase = `${ctx.apiRoot}/workspaces/${ctx.workspaceId}`;
|
|
6621
|
-
const headers = { Authorization: `Bearer ${ctx.bearer}`, "Content-Type": "application/json" };
|
|
6622
|
-
const call = async (method, path3, init2) => {
|
|
6623
|
-
const url = new URL(`${wsBase}${path3}`);
|
|
6624
|
-
for (const [k, v] of Object.entries(init2?.query ?? {})) {
|
|
6625
|
-
if (v !== void 0) url.searchParams.set(k, v);
|
|
6626
|
-
}
|
|
6627
|
-
let res;
|
|
6628
|
-
try {
|
|
6629
|
-
res = await doFetch(url.toString(), {
|
|
6630
|
-
method,
|
|
6631
|
-
headers,
|
|
6632
|
-
...init2?.body !== void 0 ? { body: JSON.stringify(init2.body) } : {},
|
|
6633
|
-
signal
|
|
6634
|
-
});
|
|
6635
|
-
} catch (err2) {
|
|
6636
|
-
return {
|
|
6637
|
-
ok: false,
|
|
6638
|
-
result: `error: request failed: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
6639
|
-
};
|
|
6640
|
-
}
|
|
6641
|
-
const contentType = res.headers.get("content-type") ?? "";
|
|
6642
|
-
const text = contentType.includes("application/json") ? JSON.stringify(await res.json().catch(() => ({}))) : await res.text().catch(() => "");
|
|
6643
|
-
if (!res.ok) return { ok: false, result: truncate2(`error ${res.status}: ${text}`) };
|
|
6644
|
-
return { ok: true, result: truncate2(text) };
|
|
6645
|
-
};
|
|
6646
|
-
switch (name) {
|
|
6647
|
-
case "sdk":
|
|
6648
|
-
return call("POST", "/sdk", {
|
|
6649
|
-
body: {
|
|
6650
|
-
program: str2(args.code) ?? "",
|
|
6651
|
-
...ctx.conversationId ? { activeConversationId: ctx.conversationId } : {}
|
|
6652
|
-
}
|
|
6653
|
-
});
|
|
6654
|
-
case "mint_render_token":
|
|
6655
|
-
return call("POST", "/render-tokens", {
|
|
6656
|
-
body: {
|
|
6657
|
-
pathPrefix: str2(args.pathPrefix) ?? "/",
|
|
6658
|
-
...typeof args.ttlSeconds === "number" ? { ttlSeconds: args.ttlSeconds } : {}
|
|
6659
|
-
}
|
|
6660
|
-
});
|
|
6661
|
-
case "cabane_list":
|
|
6662
|
-
return call("GET", "/files/tree", { query: { path: str2(args.path) ?? "/" } });
|
|
6663
|
-
case "cabane_read":
|
|
6664
|
-
return call("GET", "/files/content", { query: { path: str2(args.path) } });
|
|
6665
|
-
case "cabane_search":
|
|
6666
|
-
return call("GET", "/search", { query: { q: str2(args.q), path: str2(args.path) } });
|
|
6667
|
-
case "cabane_write": {
|
|
6668
|
-
const overwrite = args.overwrite === true;
|
|
6669
|
-
return overwrite ? call("PUT", "/files/content", {
|
|
6670
|
-
body: { path: str2(args.path), content: str2(args.content) }
|
|
6671
|
-
}) : call("POST", "/files", {
|
|
6672
|
-
body: { path: str2(args.path), content: str2(args.content), mkdirs: true }
|
|
6673
|
-
});
|
|
6674
|
-
}
|
|
6675
|
-
case "cabane_edit":
|
|
6676
|
-
return call("PATCH", "/files", {
|
|
6677
|
-
body: {
|
|
6678
|
-
path: str2(args.path),
|
|
6679
|
-
find: str2(args.find),
|
|
6680
|
-
replace: str2(args.replace) ?? "",
|
|
6681
|
-
...args.replaceAll === true ? { replaceAll: true } : {}
|
|
6682
|
-
}
|
|
6683
|
-
});
|
|
6684
|
-
case "cabane_mkdir":
|
|
6685
|
-
return call("POST", "/folders", {
|
|
6686
|
-
body: {
|
|
6687
|
-
path: str2(args.path),
|
|
6688
|
-
...args.recursive === true ? { recursive: true } : {}
|
|
6689
|
-
}
|
|
6690
|
-
});
|
|
6691
|
-
case "cabane_move":
|
|
6692
|
-
return call("POST", "/entries/move", {
|
|
6693
|
-
body: { fromPath: str2(args.fromPath), toPath: str2(args.toPath) }
|
|
6694
|
-
});
|
|
6695
|
-
case "cabane_delete":
|
|
6696
|
-
return call("DELETE", "/entries", {
|
|
6697
|
-
query: {
|
|
6698
|
-
path: str2(args.path),
|
|
6699
|
-
recursive: args.recursive === true ? "true" : void 0
|
|
6700
|
-
}
|
|
6701
|
-
});
|
|
6702
|
-
case "cabane_context":
|
|
6703
|
-
return call("POST", "/graph/query", {
|
|
6704
|
-
body: { preset: "orient", args: { kind: str2(args.kind), ref: str2(args.ref) } }
|
|
6705
|
-
});
|
|
6706
|
-
case "cabane_messages":
|
|
6707
|
-
return call(
|
|
6708
|
-
"GET",
|
|
6709
|
-
`/conversations/${encodeURIComponent(str2(args.conversationId) ?? "")}/messages`,
|
|
6710
|
-
{
|
|
6711
|
-
query: {
|
|
6712
|
-
order: str2(args.order),
|
|
6713
|
-
limit: typeof args.limit === "number" ? String(args.limit) : void 0
|
|
6714
|
-
}
|
|
6715
|
-
}
|
|
6716
|
-
);
|
|
6717
|
-
default:
|
|
6718
|
-
return { ok: false, result: `error: unknown tool "${name}"` };
|
|
6719
|
-
}
|
|
6720
|
-
}
|
|
6721
|
-
function str2(v) {
|
|
6722
|
-
return typeof v === "string" ? v : void 0;
|
|
6723
|
-
}
|
|
6724
|
-
function truncate2(s) {
|
|
6725
|
-
return s.length > TOOL_RESULT_MAX_CHARS ? `${s.slice(0, TOOL_RESULT_MAX_CHARS)}
|
|
6726
|
-
\u2026 [truncated]` : s;
|
|
6727
|
-
}
|
|
6728
|
-
|
|
6729
|
-
// packages/agent-runtime/src/cabane-native/turn-control.ts
|
|
6730
|
-
function describeSubAgentError(status2, body) {
|
|
6731
|
-
const code = body && typeof body === "object" && "error" in body ? String(body.error) : void 0;
|
|
6732
|
-
switch (code) {
|
|
6733
|
-
case "callout_cap_exceeded":
|
|
6734
|
-
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.";
|
|
6735
|
-
case "callout_depth_exceeded":
|
|
6736
|
-
return "sub_agent: this would nest sub-agents too deep (max 3 levels). Have the current worker report back rather than spawning another layer.";
|
|
6737
|
-
case "dispatch_agent_not_found":
|
|
6738
|
-
return "sub_agent: no live agent in this workspace matches that `agentId`. Check `list_agents`, or omit `agentId` to spawn yourself.";
|
|
6739
|
-
case "dispatch_return_requires_turn":
|
|
6740
|
-
case "dispatch_return_requires_agent":
|
|
6741
|
-
case "dispatch_return_requires_dispatch":
|
|
6742
|
-
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.`;
|
|
6743
|
-
default:
|
|
6744
|
-
return `sub_agent: the spawn failed (${code ?? `HTTP ${status2}`}).`;
|
|
6745
|
-
}
|
|
6746
|
-
}
|
|
6747
|
-
var CABANE_NATIVE_TURN_CONTROL_TOOLS = [
|
|
6748
|
-
{
|
|
6749
|
-
type: "function",
|
|
6750
|
-
function: {
|
|
6751
|
-
name: "ask",
|
|
6752
|
-
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.",
|
|
6753
|
-
parameters: {
|
|
6754
|
-
type: "object",
|
|
6755
|
-
properties: {
|
|
6756
|
-
targetUserId: {
|
|
6757
|
-
type: "string",
|
|
6758
|
-
description: "The workspace member (human) to ask \u2014 a user id from the roster."
|
|
6759
|
-
},
|
|
6760
|
-
question: {
|
|
6761
|
-
type: "string",
|
|
6762
|
-
description: "Single-question form: a short body of framing (one or two sentences)."
|
|
6763
|
-
},
|
|
6764
|
-
headline: {
|
|
6765
|
-
type: "string",
|
|
6766
|
-
description: "Single-question form: the question itself as one clear capitalized sentence ending in `?`."
|
|
6767
|
-
},
|
|
6768
|
-
options: {
|
|
6769
|
-
type: "array",
|
|
6770
|
-
items: { type: "string" },
|
|
6771
|
-
description: "Single-question form: 2\u20134 suggested one-click answers."
|
|
6772
|
-
},
|
|
6773
|
-
questions: {
|
|
6774
|
-
type: "array",
|
|
6775
|
-
items: {
|
|
6776
|
-
type: "object",
|
|
6777
|
-
properties: {
|
|
6778
|
-
headline: { type: "string" },
|
|
6779
|
-
body: { type: "string" },
|
|
6780
|
-
options: { type: "array", items: { type: "string" } }
|
|
6781
|
-
},
|
|
6782
|
-
required: ["headline"]
|
|
6783
|
-
},
|
|
6784
|
-
description: "Multi-question form: 1\u20135 questions to ask at once."
|
|
6785
|
-
}
|
|
6786
|
-
},
|
|
6787
|
-
required: ["targetUserId"]
|
|
6788
|
-
}
|
|
6789
|
-
}
|
|
6790
|
-
},
|
|
6791
|
-
{
|
|
6792
|
-
type: "function",
|
|
6793
|
-
function: {
|
|
6794
|
-
name: "wake_me",
|
|
6795
|
-
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.',
|
|
6796
|
-
parameters: {
|
|
6797
|
-
type: "object",
|
|
6798
|
-
properties: {
|
|
6799
|
-
afterSeconds: {
|
|
6800
|
-
type: "number",
|
|
6801
|
-
description: "Relative delay in seconds from when this turn ends (floor 60)."
|
|
6802
|
-
},
|
|
6803
|
-
at: {
|
|
6804
|
-
type: "string",
|
|
6805
|
-
description: "Absolute ISO-8601 timestamp with a zone (e.g. 2026-07-16T09:00:00-07:00)."
|
|
6806
|
-
},
|
|
6807
|
-
note: {
|
|
6808
|
-
type: "string",
|
|
6809
|
-
description: "A note to your future self \u2014 becomes the wake body (the condition to re-check)."
|
|
6810
|
-
}
|
|
6811
|
-
},
|
|
6812
|
-
required: ["note"]
|
|
6813
|
-
}
|
|
6814
|
-
}
|
|
6815
|
-
},
|
|
6816
|
-
{
|
|
6817
|
-
type: "function",
|
|
6818
|
-
function: {
|
|
6819
|
-
name: "summon_agent",
|
|
6820
|
-
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.",
|
|
6821
|
-
parameters: {
|
|
6822
|
-
type: "object",
|
|
6823
|
-
properties: {
|
|
6824
|
-
agentId: {
|
|
6825
|
-
type: "string",
|
|
6826
|
-
description: "The peer agent to summon \u2014 a workspace agent id from the roster."
|
|
6827
|
-
}
|
|
6828
|
-
},
|
|
6829
|
-
required: ["agentId"]
|
|
6830
|
-
}
|
|
6831
|
-
}
|
|
6832
|
-
},
|
|
6833
|
-
{
|
|
6834
|
-
type: "function",
|
|
6835
|
-
function: {
|
|
6836
|
-
name: "sub_agent",
|
|
6837
|
-
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.",
|
|
6838
|
-
parameters: {
|
|
6839
|
-
type: "object",
|
|
6840
|
-
properties: {
|
|
6841
|
-
prompt: {
|
|
6842
|
-
type: "string",
|
|
6843
|
-
description: "The sub-agent's self-contained opening instruction."
|
|
6844
|
-
},
|
|
6845
|
-
agentId: {
|
|
6846
|
-
type: "string",
|
|
6847
|
-
description: "Optional peer to run the sub-agent as; omit to spawn yourself."
|
|
6848
|
-
},
|
|
6849
|
-
title: { type: "string", description: "Optional title for the child thread." }
|
|
6850
|
-
},
|
|
6851
|
-
required: ["prompt"]
|
|
6852
|
-
}
|
|
6853
|
-
}
|
|
6854
|
-
},
|
|
6855
|
-
{
|
|
6856
|
-
type: "function",
|
|
6857
|
-
function: {
|
|
6858
|
-
name: "skip_turn",
|
|
6859
|
-
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.",
|
|
6860
|
-
parameters: {
|
|
6861
|
-
type: "object",
|
|
6862
|
-
properties: {
|
|
6863
|
-
reason: {
|
|
6864
|
-
type: "string",
|
|
6865
|
-
description: "Short reason you are declining \u2014 used for telemetry."
|
|
6866
|
-
}
|
|
6867
|
-
},
|
|
6868
|
-
required: ["reason"]
|
|
6869
|
-
}
|
|
6870
|
-
}
|
|
6871
|
-
}
|
|
6872
|
-
];
|
|
6873
|
-
var CABANE_NATIVE_TURN_CONTROL_NAMES = new Set(
|
|
6874
|
-
CABANE_NATIVE_TURN_CONTROL_TOOLS.map((t) => t.function.name)
|
|
6875
|
-
);
|
|
6876
|
-
function summarizeTurnControlArgs(name, args) {
|
|
6877
|
-
if (name === "ask") return str3(args.headline) ?? str3(args.question) ?? "";
|
|
6878
|
-
if (name === "wake_me") return str3(args.note) ?? "";
|
|
6879
|
-
if (name === "summon_agent") return str3(args.agentId) ?? "";
|
|
6880
|
-
if (name === "sub_agent") return str3(args.title) ?? truncateHead(str3(args.prompt) ?? "", 60);
|
|
6881
|
-
if (name === "skip_turn") return str3(args.reason) ?? "";
|
|
6882
|
-
return "";
|
|
6883
|
-
}
|
|
6884
|
-
async function executeNativeTurnControl(name, args, tc) {
|
|
6885
|
-
switch (name) {
|
|
6886
|
-
case "summon_agent": {
|
|
6887
|
-
const agentId = str3(args.agentId);
|
|
6888
|
-
if (!agentId) return err("summon_agent: `agentId` is required.");
|
|
6889
|
-
tc.summon(agentId);
|
|
6890
|
-
return ok({ summoned: agentId });
|
|
6891
|
-
}
|
|
6892
|
-
case "skip_turn": {
|
|
6893
|
-
const reason = str3(args.reason);
|
|
6894
|
-
if (!reason) return err("skip_turn: `reason` is required.");
|
|
6895
|
-
tc.skip(reason);
|
|
6896
|
-
return ok({ skipped: true });
|
|
6897
|
-
}
|
|
6898
|
-
case "ask": {
|
|
6899
|
-
const targetUserId = str3(args.targetUserId);
|
|
6900
|
-
if (!targetUserId) return err("ask: `targetUserId` is required.");
|
|
6901
|
-
const hasSingle = args.question !== void 0;
|
|
6902
|
-
const hasArray = Array.isArray(args.questions) && args.questions.length > 0;
|
|
6903
|
-
if (hasSingle && hasArray)
|
|
6904
|
-
return err("ask: provide either `question` or `questions`, not both.");
|
|
6905
|
-
if (!hasSingle && !hasArray) return err("ask: provide `question` or `questions`.");
|
|
6906
|
-
const payload = hasArray ? { targetUserId, questions: args.questions } : {
|
|
6907
|
-
targetUserId,
|
|
6908
|
-
question: str3(args.question),
|
|
6909
|
-
...str3(args.headline) ? { headline: str3(args.headline) } : {},
|
|
6910
|
-
...Array.isArray(args.options) ? { options: args.options } : {}
|
|
6911
|
-
};
|
|
6912
|
-
tc.ask(payload);
|
|
6913
|
-
return ok({ asked: targetUserId });
|
|
6914
|
-
}
|
|
6915
|
-
case "wake_me": {
|
|
6916
|
-
const note = str3(args.note);
|
|
6917
|
-
if (!note) return err("wake_me: `note` is required.");
|
|
6918
|
-
const hasAfter = typeof args.afterSeconds === "number";
|
|
6919
|
-
const hasAt = typeof args.at === "string";
|
|
6920
|
-
if (hasAfter && hasAt)
|
|
6921
|
-
return err("wake_me: provide either `afterSeconds` or `at`, not both.");
|
|
6922
|
-
if (!hasAfter && !hasAt) return err("wake_me: provide `afterSeconds` or `at`.");
|
|
6923
|
-
tc.wake({
|
|
6924
|
-
...hasAfter ? { afterSeconds: args.afterSeconds } : {},
|
|
6925
|
-
...hasAt ? { at: args.at } : {},
|
|
6926
|
-
note
|
|
6927
|
-
});
|
|
6928
|
-
return ok({ armed: hasAt ? { at: args.at } : { afterSeconds: args.afterSeconds } });
|
|
6929
|
-
}
|
|
6930
|
-
case "sub_agent": {
|
|
6931
|
-
const prompt = str3(args.prompt);
|
|
6932
|
-
if (!prompt) return err("sub_agent: `prompt` is required.");
|
|
6933
|
-
const result = await tc.subAgent({
|
|
6934
|
-
prompt,
|
|
6935
|
-
...str3(args.agentId) ? { agentId: str3(args.agentId) } : {},
|
|
6936
|
-
...str3(args.title) ? { title: str3(args.title) } : {}
|
|
6937
|
-
});
|
|
6938
|
-
if (!result.ok) return err(result.error);
|
|
6939
|
-
return ok({
|
|
6940
|
-
conversationId: result.conversationId,
|
|
6941
|
-
note: "Spawned. Don't wait \u2014 finish what else this turn can do, then end your turn; the result posts back here."
|
|
6942
|
-
});
|
|
6943
|
-
}
|
|
6944
|
-
default:
|
|
6945
|
-
return err(`unknown turn-control tool "${name}"`);
|
|
6946
|
-
}
|
|
6947
|
-
}
|
|
6948
|
-
function ok(payload) {
|
|
6949
|
-
return { ok: true, result: JSON.stringify(payload) };
|
|
6950
|
-
}
|
|
6951
|
-
function err(message) {
|
|
6952
|
-
return { ok: false, result: `error: ${message}` };
|
|
6953
|
-
}
|
|
6954
|
-
function str3(v) {
|
|
6955
|
-
return typeof v === "string" ? v : void 0;
|
|
6956
|
-
}
|
|
6957
|
-
function truncateHead(s, n) {
|
|
6958
|
-
return s.length > n ? `${s.slice(0, n)}\u2026` : s;
|
|
6959
|
-
}
|
|
6960
|
-
|
|
6961
|
-
// packages/agent-runtime/src/cabane-native/loop.ts
|
|
6962
|
-
var DEFAULT_MAX_ITERATIONS = 12;
|
|
6963
|
-
async function* runCabaneNativeTurn(req, signal, deps) {
|
|
6964
|
-
if (!req.config.model) {
|
|
6965
|
-
yield { type: "result", ok: false, reason: "no_model" };
|
|
6966
|
-
return;
|
|
6967
|
-
}
|
|
6968
|
-
const model = parseCabaneNativeModel(req.config.model);
|
|
6969
|
-
const surface = cabaneNativeSurface(req.config.runtimeOptions);
|
|
6970
|
-
const turnControl = req.extra.turnControl;
|
|
6971
|
-
const tools = surface === "code" ? [
|
|
6972
|
-
CABANE_NATIVE_CODE_TOOL,
|
|
6973
|
-
CABANE_NATIVE_RENDER_TOKEN_TOOL,
|
|
6974
|
-
...turnControl ? CABANE_NATIVE_TURN_CONTROL_TOOLS : []
|
|
6975
|
-
] : CABANE_NATIVE_TOOLS;
|
|
6976
|
-
const toolCtx = {
|
|
6977
|
-
apiRoot: deps.apiRoot,
|
|
6978
|
-
workspaceId: deps.workspaceId,
|
|
6979
|
-
bearer: deps.bearer,
|
|
6980
|
-
// CT666: the origin conversation, forwarded by the `sdk` tool to the `/sdk`
|
|
6981
|
-
// endpoint so a code-mode program's returning callout binds to this turn.
|
|
6982
|
-
...deps.conversationId ? { conversationId: deps.conversationId } : {},
|
|
6983
|
-
...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
|
|
6984
|
-
};
|
|
6985
|
-
const messages = await assembleMessages(req.systemPrompt, req.content, req.prompt, {
|
|
6986
|
-
apiRoot: deps.apiRoot,
|
|
6987
|
-
workspaceId: deps.workspaceId,
|
|
6988
|
-
bearer: deps.bearer,
|
|
6989
|
-
conversationId: deps.conversationId,
|
|
6990
|
-
...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {},
|
|
6991
|
-
...deps.historyLimit !== void 0 ? { historyLimit: deps.historyLimit } : {}
|
|
6992
|
-
});
|
|
6993
|
-
if (signal.aborted) return;
|
|
6994
|
-
const maxIterations = deps.maxIterations ?? DEFAULT_MAX_ITERATIONS;
|
|
6995
|
-
let usage;
|
|
6996
|
-
let resolvedModel = model;
|
|
6997
|
-
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
6998
|
-
let text = "";
|
|
6999
|
-
const toolAcc = /* @__PURE__ */ new Map();
|
|
7000
|
-
let finishReason;
|
|
7001
|
-
let errored2;
|
|
7002
|
-
for await (const ev of deps.provider.stream({ model, messages, tools }, signal)) {
|
|
7003
|
-
if (signal.aborted) return;
|
|
7004
|
-
switch (ev.type) {
|
|
7005
|
-
case "text":
|
|
7006
|
-
text += ev.delta;
|
|
7007
|
-
break;
|
|
7008
|
-
case "tool_call": {
|
|
7009
|
-
const cur = toolAcc.get(ev.index) ?? { id: `call_${ev.index}`, name: "", args: "" };
|
|
7010
|
-
if (ev.id) cur.id = ev.id;
|
|
7011
|
-
if (ev.name) cur.name = ev.name;
|
|
7012
|
-
if (ev.argumentsDelta) cur.args += ev.argumentsDelta;
|
|
7013
|
-
toolAcc.set(ev.index, cur);
|
|
7014
|
-
break;
|
|
7015
|
-
}
|
|
7016
|
-
case "usage":
|
|
7017
|
-
usage = {
|
|
7018
|
-
inputTokens: ev.inputTokens,
|
|
7019
|
-
outputTokens: ev.outputTokens,
|
|
7020
|
-
contextTokens: ev.inputTokens
|
|
7021
|
-
};
|
|
7022
|
-
break;
|
|
7023
|
-
case "model":
|
|
7024
|
-
resolvedModel = ev.model;
|
|
7025
|
-
break;
|
|
7026
|
-
case "error":
|
|
7027
|
-
errored2 = ev.message;
|
|
7028
|
-
break;
|
|
7029
|
-
case "done":
|
|
7030
|
-
finishReason = ev.finishReason;
|
|
7031
|
-
break;
|
|
7032
|
-
}
|
|
7033
|
-
}
|
|
7034
|
-
if (signal.aborted) return;
|
|
7035
|
-
if (errored2 !== void 0) {
|
|
7036
|
-
const sealed = sealText(text, false);
|
|
7037
|
-
if (sealed) yield sealed;
|
|
7038
|
-
const failure = classifyErrorText(errored2);
|
|
7039
|
-
yield {
|
|
7040
|
-
type: "result",
|
|
7041
|
-
ok: false,
|
|
7042
|
-
reason: failure ? encodeFailureReason(failure) : `error:${errored2.slice(0, 200)}`,
|
|
7043
|
-
...usage ? { usage } : {},
|
|
7044
|
-
...resolvedModel ? { resolvedModel } : {}
|
|
7045
|
-
};
|
|
7046
|
-
return;
|
|
7047
|
-
}
|
|
7048
|
-
const toolCalls = [...toolAcc.entries()].sort((a, b) => a[0] - b[0]).map(([, v]) => v);
|
|
7049
|
-
if (toolCalls.length === 0) {
|
|
7050
|
-
const sealed = sealText(text, true);
|
|
7051
|
-
if (sealed) yield sealed;
|
|
7052
|
-
yield {
|
|
7053
|
-
type: "result",
|
|
7054
|
-
ok: true,
|
|
7055
|
-
...usage ? { usage } : {},
|
|
7056
|
-
...resolvedModel ? { resolvedModel } : {}
|
|
7057
|
-
};
|
|
7058
|
-
return;
|
|
7059
|
-
}
|
|
7060
|
-
const sealedInterim = sealText(text, false);
|
|
7061
|
-
if (sealedInterim) yield sealedInterim;
|
|
7062
|
-
const assistantToolCalls = toolCalls.map((t) => ({
|
|
7063
|
-
id: t.id,
|
|
7064
|
-
type: "function",
|
|
7065
|
-
function: { name: t.name, arguments: t.args || "{}" }
|
|
7066
|
-
}));
|
|
7067
|
-
messages.push({ role: "assistant", content: text, tool_calls: assistantToolCalls });
|
|
7068
|
-
for (const t of toolCalls) {
|
|
7069
|
-
if (signal.aborted) return;
|
|
7070
|
-
const args = parseArgs(t.args);
|
|
7071
|
-
const displayName = prettyToolName(t.name);
|
|
7072
|
-
const isTurnControl = CABANE_NATIVE_TURN_CONTROL_NAMES.has(t.name);
|
|
7073
|
-
const summary = isTurnControl ? summarizeTurnControlArgs(t.name, args) : summarizeCabaneToolArgs(t.name, args);
|
|
7074
|
-
yield { type: "tool", id: t.id, name: displayName, phase: "start", summary, input: args };
|
|
7075
|
-
const result = isTurnControl && turnControl ? await executeNativeTurnControl(t.name, args, turnControl) : await executeCabaneNativeTool(t.name, args, toolCtx, signal);
|
|
7076
|
-
if (signal.aborted) return;
|
|
7077
|
-
yield {
|
|
7078
|
-
type: "tool",
|
|
7079
|
-
id: t.id,
|
|
7080
|
-
name: displayName,
|
|
7081
|
-
phase: result.ok ? "done" : "error",
|
|
7082
|
-
summary,
|
|
7083
|
-
input: args,
|
|
7084
|
-
result: result.result
|
|
7085
|
-
};
|
|
7086
|
-
messages.push({ role: "tool", tool_call_id: t.id, content: result.result });
|
|
7087
|
-
}
|
|
7088
|
-
}
|
|
7089
|
-
deps.onWarn?.("cabane-native: turn hit the tool-iteration cap; force-settling", {
|
|
7090
|
-
maxIterations
|
|
7091
|
-
});
|
|
7092
|
-
yield {
|
|
7093
|
-
type: "text",
|
|
7094
|
-
body: `(Stopped after ${maxIterations} tool steps without a final answer.)`,
|
|
7095
|
-
terminal: true
|
|
7096
|
-
};
|
|
7097
|
-
yield {
|
|
7098
|
-
type: "result",
|
|
7099
|
-
ok: true,
|
|
7100
|
-
...usage ? { usage } : {},
|
|
7101
|
-
...resolvedModel ? { resolvedModel } : {}
|
|
7102
|
-
};
|
|
7103
|
-
}
|
|
7104
|
-
function sealText(text, terminal) {
|
|
7105
|
-
const body = text.trim();
|
|
7106
|
-
if (body.length === 0) return null;
|
|
7107
|
-
return { type: "text", body, terminal };
|
|
7108
|
-
}
|
|
7109
|
-
function parseArgs(raw) {
|
|
7110
|
-
if (!raw.trim()) return {};
|
|
7111
|
-
try {
|
|
7112
|
-
const parsed = JSON.parse(raw);
|
|
7113
|
-
return parsed && typeof parsed === "object" ? parsed : {};
|
|
7114
|
-
} catch {
|
|
7115
|
-
return {};
|
|
7116
|
-
}
|
|
7117
|
-
}
|
|
7118
|
-
|
|
7119
|
-
// packages/agent-runtime/src/cabane-native/provider.ts
|
|
7120
|
-
var DEFAULT_OPENROUTER_BASE = "https://openrouter.ai/api/v1";
|
|
7121
|
-
function createOpenRouterProvider(opts) {
|
|
7122
|
-
const base = (opts.baseUrl ?? DEFAULT_OPENROUTER_BASE).replace(/\/$/, "");
|
|
7123
|
-
const doFetch = opts.fetchImpl ?? fetch;
|
|
7124
|
-
return {
|
|
7125
|
-
async *stream(req, signal) {
|
|
7126
|
-
let res;
|
|
7127
|
-
try {
|
|
7128
|
-
res = await doFetch(`${base}/chat/completions`, {
|
|
7129
|
-
method: "POST",
|
|
7130
|
-
headers: {
|
|
7131
|
-
Authorization: `Bearer ${opts.apiKey}`,
|
|
7132
|
-
"Content-Type": "application/json",
|
|
7133
|
-
// OpenRouter attribution headers (optional, but polite + used for
|
|
7134
|
-
// routing/analytics on their side).
|
|
7135
|
-
"HTTP-Referer": "https://cabane.ai",
|
|
7136
|
-
"X-Title": "Cabane"
|
|
7137
|
-
},
|
|
7138
|
-
body: JSON.stringify({
|
|
7139
|
-
model: req.model,
|
|
7140
|
-
messages: req.messages,
|
|
7141
|
-
...req.tools.length > 0 ? { tools: req.tools } : {},
|
|
7142
|
-
stream: true,
|
|
7143
|
-
// Ask OpenRouter to append a trailing usage chunk to the stream.
|
|
7144
|
-
stream_options: { include_usage: true }
|
|
7145
|
-
}),
|
|
7146
|
-
signal
|
|
7147
|
-
});
|
|
7148
|
-
} catch (err2) {
|
|
7149
|
-
if (signal.aborted) return;
|
|
7150
|
-
yield { type: "error", message: `request failed: ${errText(err2)}` };
|
|
7151
|
-
return;
|
|
7152
|
-
}
|
|
7153
|
-
if (!res.ok || !res.body) {
|
|
7154
|
-
const bodyText = await res.text().catch(() => "");
|
|
7155
|
-
yield { type: "error", message: providerErrorMessage(res.status, bodyText) };
|
|
7156
|
-
return;
|
|
7157
|
-
}
|
|
7158
|
-
const decoder = new TextDecoder();
|
|
7159
|
-
const reader = res.body.getReader();
|
|
7160
|
-
let buffer = "";
|
|
7161
|
-
let finishReason;
|
|
7162
|
-
let modelEmitted = false;
|
|
7163
|
-
try {
|
|
7164
|
-
for (; ; ) {
|
|
7165
|
-
if (signal.aborted) return;
|
|
7166
|
-
const { value, done } = await reader.read();
|
|
7167
|
-
if (done) break;
|
|
7168
|
-
buffer += decoder.decode(value, { stream: true });
|
|
7169
|
-
let nl;
|
|
7170
|
-
while ((nl = buffer.indexOf("\n")) !== -1) {
|
|
7171
|
-
const line = buffer.slice(0, nl).trim();
|
|
7172
|
-
buffer = buffer.slice(nl + 1);
|
|
7173
|
-
if (!line || line.startsWith(":")) continue;
|
|
7174
|
-
if (!line.startsWith("data:")) continue;
|
|
7175
|
-
const data = line.slice("data:".length).trim();
|
|
7176
|
-
if (data === "[DONE]") {
|
|
7177
|
-
yield { type: "done", ...finishReason ? { finishReason } : {} };
|
|
7178
|
-
return;
|
|
7179
|
-
}
|
|
7180
|
-
let chunk;
|
|
7181
|
-
try {
|
|
7182
|
-
chunk = JSON.parse(data);
|
|
7183
|
-
} catch {
|
|
7184
|
-
continue;
|
|
7185
|
-
}
|
|
7186
|
-
if (chunk.error) {
|
|
7187
|
-
yield { type: "error", message: chunk.error.message ?? "provider error" };
|
|
7188
|
-
return;
|
|
7189
|
-
}
|
|
7190
|
-
if (!modelEmitted && chunk.model) {
|
|
7191
|
-
modelEmitted = true;
|
|
7192
|
-
yield { type: "model", model: chunk.model };
|
|
7193
|
-
}
|
|
7194
|
-
const choice = chunk.choices?.[0];
|
|
7195
|
-
if (choice) {
|
|
7196
|
-
const delta = choice.delta;
|
|
7197
|
-
if (delta?.content) yield { type: "text", delta: delta.content };
|
|
7198
|
-
if (delta?.tool_calls) {
|
|
7199
|
-
for (const tc of delta.tool_calls) {
|
|
7200
|
-
yield {
|
|
7201
|
-
type: "tool_call",
|
|
7202
|
-
index: tc.index,
|
|
7203
|
-
...tc.id ? { id: tc.id } : {},
|
|
7204
|
-
...tc.function?.name ? { name: tc.function.name } : {},
|
|
7205
|
-
...tc.function?.arguments !== void 0 ? { argumentsDelta: tc.function.arguments } : {}
|
|
7206
|
-
};
|
|
7207
|
-
}
|
|
7208
|
-
}
|
|
7209
|
-
if (choice.finish_reason) finishReason = choice.finish_reason;
|
|
7210
|
-
}
|
|
7211
|
-
if (chunk.usage) {
|
|
7212
|
-
yield {
|
|
7213
|
-
type: "usage",
|
|
7214
|
-
inputTokens: chunk.usage.prompt_tokens ?? 0,
|
|
7215
|
-
outputTokens: chunk.usage.completion_tokens ?? 0
|
|
7216
|
-
};
|
|
7217
|
-
}
|
|
7218
|
-
}
|
|
7219
|
-
}
|
|
7220
|
-
} catch (err2) {
|
|
7221
|
-
if (signal.aborted) return;
|
|
7222
|
-
yield { type: "error", message: `stream read failed: ${errText(err2)}` };
|
|
7223
|
-
return;
|
|
7224
|
-
}
|
|
7225
|
-
yield { type: "done", ...finishReason ? { finishReason } : {} };
|
|
7226
|
-
}
|
|
7227
|
-
};
|
|
7228
|
-
}
|
|
7229
|
-
function providerErrorMessage(status2, body) {
|
|
7230
|
-
let detail = body.slice(0, 300);
|
|
7231
|
-
try {
|
|
7232
|
-
const parsed = JSON.parse(body);
|
|
7233
|
-
if (parsed.error?.message) detail = parsed.error.message;
|
|
7234
|
-
} catch {
|
|
7235
|
-
}
|
|
7236
|
-
return `HTTP ${status2}: ${detail}`;
|
|
7237
|
-
}
|
|
7238
|
-
function errText(err2) {
|
|
7239
|
-
return err2 instanceof Error ? err2.message : String(err2);
|
|
7240
|
-
}
|
|
7241
|
-
|
|
7242
|
-
// packages/agent-runtime/src/cabane-native/index.ts
|
|
7243
|
-
var CABANE_NATIVE_RUNTIME_NAME = "cabane-native";
|
|
7244
|
-
function createCabaneNativeAdapter(deps = {}) {
|
|
7245
|
-
const provider = deps.provider ?? (deps.apiKey ? createOpenRouterProvider({
|
|
7246
|
-
apiKey: deps.apiKey,
|
|
7247
|
-
...deps.baseUrl ? { baseUrl: deps.baseUrl } : {},
|
|
7248
|
-
...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
|
|
7249
|
-
}) : null);
|
|
7250
|
-
return {
|
|
7251
|
-
name: CABANE_NATIVE_RUNTIME_NAME,
|
|
7252
|
-
// CT666: the native runtime's addendum is now SURFACE-aware. Code mode (the
|
|
7253
|
-
// default In-Cabane surface) mounts the single `sdk` tool, so it's taught the
|
|
7254
|
-
// code-mode addendum (act by writing a `cabane` SDK program; the `cabane_*`
|
|
7255
|
-
// names don't exist here); classic (the retained CT663 10-tool fallback) is
|
|
7256
|
-
// taught the granular `cabane_*` listing. The caller (turn-context) passes
|
|
7257
|
-
// `codeMode` off the SAME lifted surface value the loop mounts tools from, so
|
|
7258
|
-
// the prompt and the mounted surface can't drift. (Supersedes CT614's
|
|
7259
|
-
// flag-independent addendum: native no longer ignores this arg.)
|
|
7260
|
-
promptAddendum: (codeMode) => codeMode ? CABANE_NATIVE_ADDENDUM_CODE_MODE : CABANE_NATIVE_ADDENDUM,
|
|
7261
|
-
dialectSchema: cabaneNativeDialectSchema,
|
|
7262
|
-
async *runTurn(req, signal) {
|
|
7263
|
-
if (!provider) {
|
|
7264
|
-
yield {
|
|
7265
|
-
type: "result",
|
|
7266
|
-
ok: false,
|
|
7267
|
-
reason: "cabane_native_unavailable:no OPENROUTER_API_KEY configured on this device"
|
|
7268
|
-
};
|
|
7269
|
-
return;
|
|
7270
|
-
}
|
|
7271
|
-
const workspaceId = req.cabane.workspaceId;
|
|
7272
|
-
if (!workspaceId) {
|
|
7273
|
-
yield {
|
|
7274
|
-
type: "result",
|
|
7275
|
-
ok: false,
|
|
7276
|
-
reason: "cabane_native_unavailable:turn carried no workspaceId"
|
|
7277
|
-
};
|
|
7278
|
-
return;
|
|
7279
|
-
}
|
|
7280
|
-
const apiRoot = req.cabane.mcpUrl.replace(/\/mcp\/?$/, "");
|
|
7281
|
-
yield* runCabaneNativeTurn(req, signal, {
|
|
7282
|
-
provider,
|
|
7283
|
-
apiRoot,
|
|
7284
|
-
workspaceId,
|
|
7285
|
-
bearer: req.cabane.bearer,
|
|
7286
|
-
conversationId: req.cabane.activeConversationId,
|
|
7287
|
-
...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {},
|
|
7288
|
-
...deps.onWarn ? { onWarn: deps.onWarn } : {},
|
|
7289
|
-
...deps.maxIterations !== void 0 ? { maxIterations: deps.maxIterations } : {}
|
|
7290
|
-
});
|
|
7291
|
-
}
|
|
7292
|
-
};
|
|
7293
|
-
}
|
|
7294
|
-
var cabaneNativeAdapter = createCabaneNativeAdapter();
|
|
7295
|
-
|
|
7296
6156
|
// packages/agent-runtime/src/claude-code/sdk.ts
|
|
7297
6157
|
import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
7298
6158
|
|
|
@@ -7350,7 +6210,7 @@ import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync
|
|
|
7350
6210
|
import { join as join12 } from "path";
|
|
7351
6211
|
|
|
7352
6212
|
// src/summon.ts
|
|
7353
|
-
import { z as
|
|
6213
|
+
import { z as z12 } from "zod";
|
|
7354
6214
|
var COMPANION_LOCAL_MCP_SERVER = "cabane_companion";
|
|
7355
6215
|
var SUMMON_AGENT_TOOL = "summon_agent";
|
|
7356
6216
|
var SUMMON_AGENT_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${SUMMON_AGENT_TOOL}`;
|
|
@@ -7384,7 +6244,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
7384
6244
|
SUMMON_AGENT_TOOL,
|
|
7385
6245
|
"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.",
|
|
7386
6246
|
{
|
|
7387
|
-
agentId:
|
|
6247
|
+
agentId: z12.string().uuid().describe("The peer agent to summon \u2014 a workspace agent id from `list_agents`.")
|
|
7388
6248
|
},
|
|
7389
6249
|
async (args) => {
|
|
7390
6250
|
summonState.agentId = args.agentId;
|
|
@@ -7399,7 +6259,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
7399
6259
|
SKIP_TURN_TOOL,
|
|
7400
6260
|
`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.`,
|
|
7401
6261
|
{
|
|
7402
|
-
reason:
|
|
6262
|
+
reason: z12.string().min(1).max(500).describe("Short reason you are declining \u2014 used for telemetry/debugging.")
|
|
7403
6263
|
},
|
|
7404
6264
|
async (args) => {
|
|
7405
6265
|
skipState.skipped = true;
|
|
@@ -7416,21 +6276,21 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
7416
6276
|
ASK_TOOL,
|
|
7417
6277
|
"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.",
|
|
7418
6278
|
{
|
|
7419
|
-
targetUserId:
|
|
7420
|
-
question:
|
|
6279
|
+
targetUserId: z12.string().uuid().describe("The workspace member (human) to ask \u2014 a user id from `list_members`."),
|
|
6280
|
+
question: z12.string().min(1).max(400).optional().describe(
|
|
7421
6281
|
"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`."
|
|
7422
6282
|
),
|
|
7423
|
-
headline:
|
|
6283
|
+
headline: z12.string().min(1).max(120).optional().describe(
|
|
7424
6284
|
'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.'
|
|
7425
6285
|
),
|
|
7426
|
-
options:
|
|
7427
|
-
questions:
|
|
7428
|
-
|
|
7429
|
-
headline:
|
|
6286
|
+
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."),
|
|
6287
|
+
questions: z12.array(
|
|
6288
|
+
z12.object({
|
|
6289
|
+
headline: z12.string().min(1).max(120).describe(
|
|
7430
6290
|
'The one-sentence question ("Do we go to prod?") \u2014 required for each item.'
|
|
7431
6291
|
),
|
|
7432
|
-
body:
|
|
7433
|
-
options:
|
|
6292
|
+
body: z12.string().min(1).max(400).optional().describe("Optional short framing beneath the headline. NOT a report."),
|
|
6293
|
+
options: z12.array(z12.string().min(1).max(200)).min(2).max(4).optional().describe("Optional 2\u20134 one-click answers for this question.")
|
|
7434
6294
|
})
|
|
7435
6295
|
).min(1).max(5).optional().describe(
|
|
7436
6296
|
"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."
|
|
@@ -7487,13 +6347,13 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
7487
6347
|
SUB_AGENT_TOOL,
|
|
7488
6348
|
"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.",
|
|
7489
6349
|
{
|
|
7490
|
-
prompt:
|
|
6350
|
+
prompt: z12.string().min(1).max(65536).describe(
|
|
7491
6351
|
"The sub-agent's opening instruction \u2014 self-contained (it starts with a fresh context window; only this prompt + the thread it lands in)."
|
|
7492
6352
|
),
|
|
7493
|
-
agentId:
|
|
6353
|
+
agentId: z12.string().uuid().optional().describe(
|
|
7494
6354
|
"Optional peer to run the sub-agent as (a workspace agent id from `list_agents`); omit to spawn yourself with a fresh context window."
|
|
7495
6355
|
),
|
|
7496
|
-
title:
|
|
6356
|
+
title: z12.string().max(200).optional().describe("Optional title for the child thread (result chips link it).")
|
|
7497
6357
|
},
|
|
7498
6358
|
async (args) => {
|
|
7499
6359
|
const result = await subAgentCreate(args);
|
|
@@ -7523,13 +6383,13 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
7523
6383
|
WAKE_ME_TOOL,
|
|
7524
6384
|
'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.',
|
|
7525
6385
|
{
|
|
7526
|
-
afterSeconds:
|
|
6386
|
+
afterSeconds: z12.number().int().positive().optional().describe(
|
|
7527
6387
|
"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."
|
|
7528
6388
|
),
|
|
7529
|
-
at:
|
|
6389
|
+
at: z12.string().datetime({ offset: true }).optional().describe(
|
|
7530
6390
|
"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."
|
|
7531
6391
|
),
|
|
7532
|
-
note:
|
|
6392
|
+
note: z12.string().min(1).max(2e3).describe(
|
|
7533
6393
|
'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").'
|
|
7534
6394
|
)
|
|
7535
6395
|
},
|
|
@@ -7609,7 +6469,6 @@ function buildCompanionTurnRequest(params) {
|
|
|
7609
6469
|
local: {
|
|
7610
6470
|
...params.cwd ? { cwd: params.cwd } : {},
|
|
7611
6471
|
...params.env ? { env: params.env } : {},
|
|
7612
|
-
...params.nativeWorkAssignment ? { nativeWorkAssignment: params.nativeWorkAssignment } : {},
|
|
7613
6472
|
// User MCP servers (already `${PLACEHOLDER}`-resolved). Structurally the
|
|
7614
6473
|
// adapter's `ResolvedMcpServers`.
|
|
7615
6474
|
...Object.keys(params.mcpServers).length > 0 ? { mcpServers: params.mcpServers } : {},
|
|
@@ -7617,10 +6476,9 @@ function buildCompanionTurnRequest(params) {
|
|
|
7617
6476
|
...params.claudeCode ? { claudeCode: params.claudeCode } : {}
|
|
7618
6477
|
},
|
|
7619
6478
|
// Host-injected: the companion-local summon server (for the subprocess adapters,
|
|
7620
|
-
// under its own namespace)
|
|
6479
|
+
// under its own namespace).
|
|
7621
6480
|
extra: {
|
|
7622
|
-
mcpServers: { [COMPANION_LOCAL_MCP_SERVER]: params.summonServer }
|
|
7623
|
-
turnControl: params.turnControl
|
|
6481
|
+
mcpServers: { [COMPANION_LOCAL_MCP_SERVER]: params.summonServer }
|
|
7624
6482
|
}
|
|
7625
6483
|
};
|
|
7626
6484
|
}
|
|
@@ -7629,7 +6487,7 @@ function trimSlash3(s) {
|
|
|
7629
6487
|
}
|
|
7630
6488
|
|
|
7631
6489
|
// src/prepared.ts
|
|
7632
|
-
import { mkdirSync as mkdirSync8, readFileSync as
|
|
6490
|
+
import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync7 } from "fs";
|
|
7633
6491
|
import { join as join9 } from "path";
|
|
7634
6492
|
function dirFor(workspaceId) {
|
|
7635
6493
|
return join9(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
|
|
@@ -7637,23 +6495,18 @@ function dirFor(workspaceId) {
|
|
|
7637
6495
|
function conversationDir(workspaceId, conversationId) {
|
|
7638
6496
|
return join9(dirFor(workspaceId), encodeURIComponent(conversationId));
|
|
7639
6497
|
}
|
|
7640
|
-
function pathFor3(workspaceId, conversationId, agentId
|
|
7641
|
-
|
|
7642
|
-
return join9(
|
|
7643
|
-
conversationDir(workspaceId, conversationId),
|
|
7644
|
-
`${encodeURIComponent(agentId)}${suffix}.json`
|
|
7645
|
-
);
|
|
6498
|
+
function pathFor3(workspaceId, conversationId, agentId) {
|
|
6499
|
+
return join9(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
|
|
7646
6500
|
}
|
|
7647
|
-
function readPrepared(workspaceId, conversationId, agentId
|
|
7648
|
-
const path3 = pathFor3(workspaceId, conversationId, agentId
|
|
6501
|
+
function readPrepared(workspaceId, conversationId, agentId) {
|
|
6502
|
+
const path3 = pathFor3(workspaceId, conversationId, agentId);
|
|
7649
6503
|
if (!existsSync7(path3)) return null;
|
|
7650
6504
|
try {
|
|
7651
|
-
const parsed = JSON.parse(
|
|
6505
|
+
const parsed = JSON.parse(readFileSync6(path3, "utf8"));
|
|
7652
6506
|
if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
|
|
7653
6507
|
return {
|
|
7654
6508
|
cwd: parsed.cwd,
|
|
7655
|
-
...parsed.env && typeof parsed.env === "object" ? { env: parsed.env } : {}
|
|
7656
|
-
...parsed.nativeWorkAssignment && typeof parsed.nativeWorkAssignment === "object" ? { nativeWorkAssignment: parsed.nativeWorkAssignment } : {}
|
|
6509
|
+
...parsed.env && typeof parsed.env === "object" ? { env: parsed.env } : {}
|
|
7657
6510
|
};
|
|
7658
6511
|
}
|
|
7659
6512
|
return null;
|
|
@@ -7661,42 +6514,42 @@ function readPrepared(workspaceId, conversationId, agentId, assignmentKey) {
|
|
|
7661
6514
|
return null;
|
|
7662
6515
|
}
|
|
7663
6516
|
}
|
|
7664
|
-
function writePrepared(workspaceId, conversationId, agentId, result
|
|
6517
|
+
function writePrepared(workspaceId, conversationId, agentId, result) {
|
|
7665
6518
|
mkdirSync8(conversationDir(workspaceId, conversationId), { recursive: true });
|
|
7666
6519
|
writeFileSync6(
|
|
7667
|
-
pathFor3(workspaceId, conversationId, agentId
|
|
6520
|
+
pathFor3(workspaceId, conversationId, agentId),
|
|
7668
6521
|
JSON.stringify(result) + "\n",
|
|
7669
6522
|
"utf8"
|
|
7670
6523
|
);
|
|
7671
6524
|
}
|
|
7672
6525
|
|
|
7673
6526
|
// src/secrets.ts
|
|
7674
|
-
import { existsSync as existsSync8, readFileSync as
|
|
6527
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
7675
6528
|
import { join as join10 } from "path";
|
|
7676
|
-
import { z as
|
|
6529
|
+
import { z as z13 } from "zod";
|
|
7677
6530
|
var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
7678
6531
|
function secretsPath() {
|
|
7679
6532
|
return join10(cabaneDir(), "secrets.json");
|
|
7680
6533
|
}
|
|
7681
|
-
var secretStoreSchema =
|
|
6534
|
+
var secretStoreSchema = z13.record(z13.string(), z13.string());
|
|
7682
6535
|
function loadSecretStore() {
|
|
7683
6536
|
const path3 = secretsPath();
|
|
7684
6537
|
if (!existsSync8(path3)) return makeStore({});
|
|
7685
6538
|
let raw;
|
|
7686
6539
|
try {
|
|
7687
|
-
raw =
|
|
7688
|
-
} catch (
|
|
6540
|
+
raw = readFileSync7(path3, "utf8");
|
|
6541
|
+
} catch (err) {
|
|
7689
6542
|
throw new ConfigError(
|
|
7690
|
-
`couldn't read ${path3}: ${
|
|
6543
|
+
`couldn't read ${path3}: ${err instanceof Error ? err.message : String(err)}`
|
|
7691
6544
|
);
|
|
7692
6545
|
}
|
|
7693
6546
|
if (raw.trim().length === 0) return makeStore({});
|
|
7694
6547
|
let parsed;
|
|
7695
6548
|
try {
|
|
7696
6549
|
parsed = JSON.parse(raw);
|
|
7697
|
-
} catch (
|
|
6550
|
+
} catch (err) {
|
|
7698
6551
|
throw new ConfigError(
|
|
7699
|
-
`${path3} is not valid JSON: ${
|
|
6552
|
+
`${path3} is not valid JSON: ${err instanceof Error ? err.message : String(err)}. It must be a flat object of "NAME": "value" secret pairs.`
|
|
7700
6553
|
);
|
|
7701
6554
|
}
|
|
7702
6555
|
const result = secretStoreSchema.safeParse(parsed);
|
|
@@ -7710,8 +6563,8 @@ function loadSecretStore() {
|
|
|
7710
6563
|
function loadSecretStoreTolerant(onWarn) {
|
|
7711
6564
|
try {
|
|
7712
6565
|
return loadSecretStore();
|
|
7713
|
-
} catch (
|
|
7714
|
-
onWarn?.(
|
|
6566
|
+
} catch (err) {
|
|
6567
|
+
onWarn?.(err instanceof Error ? err.message : String(err));
|
|
7715
6568
|
return makeStore({});
|
|
7716
6569
|
}
|
|
7717
6570
|
}
|
|
@@ -7782,8 +6635,8 @@ var TranscriptWriter = class {
|
|
|
7782
6635
|
} catch {
|
|
7783
6636
|
}
|
|
7784
6637
|
pruneOld(dir2, RETAIN);
|
|
7785
|
-
} catch (
|
|
7786
|
-
this.fail(
|
|
6638
|
+
} catch (err) {
|
|
6639
|
+
this.fail(err);
|
|
7787
6640
|
}
|
|
7788
6641
|
this.line({ type: "_meta", ...meta });
|
|
7789
6642
|
}
|
|
@@ -7799,14 +6652,14 @@ var TranscriptWriter = class {
|
|
|
7799
6652
|
if (this.broken) return;
|
|
7800
6653
|
try {
|
|
7801
6654
|
appendFileSync(this.path, JSON.stringify(obj) + "\n", { mode: 384 });
|
|
7802
|
-
} catch (
|
|
7803
|
-
this.fail(
|
|
6655
|
+
} catch (err) {
|
|
6656
|
+
this.fail(err);
|
|
7804
6657
|
}
|
|
7805
6658
|
}
|
|
7806
|
-
fail(
|
|
6659
|
+
fail(err) {
|
|
7807
6660
|
if (this.broken) return;
|
|
7808
6661
|
this.broken = true;
|
|
7809
|
-
this.onWarn?.(`transcript write failed (${
|
|
6662
|
+
this.onWarn?.(`transcript write failed (${err instanceof Error ? err.message : String(err)})`);
|
|
7810
6663
|
}
|
|
7811
6664
|
};
|
|
7812
6665
|
function fileName(meta) {
|
|
@@ -7844,9 +6697,9 @@ var EMPTY_FINAL_BODY = "Done \u2014 see the changes above.";
|
|
|
7844
6697
|
var TurnCommitter = class {
|
|
7845
6698
|
constructor(deps) {
|
|
7846
6699
|
this.deps = deps;
|
|
7847
|
-
this.onError = (
|
|
6700
|
+
this.onError = (err, hook) => {
|
|
7848
6701
|
deps.log.warn(
|
|
7849
|
-
{ err:
|
|
6702
|
+
{ err: err instanceof Error ? err.message : String(err), hook },
|
|
7850
6703
|
"dispatcher: transcript callback failed"
|
|
7851
6704
|
);
|
|
7852
6705
|
};
|
|
@@ -7911,9 +6764,9 @@ var TurnCommitter = class {
|
|
|
7911
6764
|
signal: deps.signal,
|
|
7912
6765
|
nextSeq: deps.nextSeq,
|
|
7913
6766
|
emptyFinalBody: EMPTY_FINAL_BODY,
|
|
7914
|
-
onError: (
|
|
6767
|
+
onError: (err) => {
|
|
7915
6768
|
deps.log.warn(
|
|
7916
|
-
{ err:
|
|
6769
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
7917
6770
|
"dispatcher: empty-final commit failed"
|
|
7918
6771
|
);
|
|
7919
6772
|
}
|
|
@@ -7936,8 +6789,8 @@ var TurnCommitter = class {
|
|
|
7936
6789
|
if (event.type === "session" || event.type === "result") return;
|
|
7937
6790
|
try {
|
|
7938
6791
|
await this.emit(event);
|
|
7939
|
-
} catch (
|
|
7940
|
-
this.onError(
|
|
6792
|
+
} catch (err) {
|
|
6793
|
+
this.onError(err, event.type);
|
|
7941
6794
|
}
|
|
7942
6795
|
}
|
|
7943
6796
|
// End-of-turn empty-final promotion. The held-text flush is now the adapter's
|
|
@@ -8133,10 +6986,27 @@ var UNEXPECTED_ROLE_REASON = `refused the wake trigger (role "system" isn't disp
|
|
|
8133
6986
|
var SKIPPED_MARKER_BODY = "(skipped)";
|
|
8134
6987
|
var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
|
|
8135
6988
|
var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
|
|
8136
|
-
var DEFAULT_AGENT_TOTAL_TIMEOUT_MS =
|
|
6989
|
+
var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
|
|
8137
6990
|
function runKey(conversationId, agentId) {
|
|
8138
6991
|
return `${conversationId}|${agentId}`;
|
|
8139
6992
|
}
|
|
6993
|
+
function describeSubAgentError(status2, body) {
|
|
6994
|
+
const code = body && typeof body === "object" && "error" in body ? String(body.error) : void 0;
|
|
6995
|
+
switch (code) {
|
|
6996
|
+
case "callout_cap_exceeded":
|
|
6997
|
+
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.";
|
|
6998
|
+
case "callout_depth_exceeded":
|
|
6999
|
+
return "sub_agent: this would nest sub-agents too deep (max 3 levels). Have the current worker report back rather than spawning another layer.";
|
|
7000
|
+
case "dispatch_agent_not_found":
|
|
7001
|
+
return "sub_agent: no live agent in this workspace matches that `agentId`. Check `list_agents`, or omit `agentId` to spawn yourself.";
|
|
7002
|
+
case "dispatch_return_requires_turn":
|
|
7003
|
+
case "dispatch_return_requires_agent":
|
|
7004
|
+
case "dispatch_return_requires_dispatch":
|
|
7005
|
+
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.`;
|
|
7006
|
+
default:
|
|
7007
|
+
return `sub_agent: the spawn failed (${code ?? `HTTP ${status2}`}).`;
|
|
7008
|
+
}
|
|
7009
|
+
}
|
|
8140
7010
|
var Dispatcher = class {
|
|
8141
7011
|
constructor(opts) {
|
|
8142
7012
|
this.opts = opts;
|
|
@@ -8163,7 +7033,7 @@ var Dispatcher = class {
|
|
|
8163
7033
|
// clear it — the SJ383 `finally` after the SDK loop is the one clear, and
|
|
8164
7034
|
// every pre-run exit returns before reaching it. So each pre-run failure has
|
|
8165
7035
|
// to clear `active_run_started_at` itself, mirroring that `finally`, or the
|
|
8166
|
-
// indicator strands until the
|
|
7036
|
+
// indicator strands until the 12h age sweep.
|
|
8167
7037
|
//
|
|
8168
7038
|
// `errorReason` controls the server's duplicate-notice rule (the active-run
|
|
8169
7039
|
// PATCH handler in conversations.ts): a clear carrying `errorReason` makes the
|
|
@@ -8183,9 +7053,9 @@ var Dispatcher = class {
|
|
|
8183
7053
|
payload.agentId,
|
|
8184
7054
|
body
|
|
8185
7055
|
);
|
|
8186
|
-
} catch (
|
|
7056
|
+
} catch (err) {
|
|
8187
7057
|
turnLog.warn(
|
|
8188
|
-
{ err:
|
|
7058
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
8189
7059
|
"dispatcher: pre-run active-run clear failed terminally; server age-sweep is the backstop"
|
|
8190
7060
|
);
|
|
8191
7061
|
}
|
|
@@ -8211,16 +7081,16 @@ var Dispatcher = class {
|
|
|
8211
7081
|
payload.messageId,
|
|
8212
7082
|
turnId
|
|
8213
7083
|
);
|
|
8214
|
-
} catch (
|
|
8215
|
-
const status2 =
|
|
7084
|
+
} catch (err) {
|
|
7085
|
+
const status2 = err instanceof ApiError ? err.status : 0;
|
|
8216
7086
|
if (status2 === 404) {
|
|
8217
7087
|
turnLog.warn(
|
|
8218
|
-
{ err:
|
|
7088
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
8219
7089
|
"dispatcher: turn-context 404 (stale event \u2014 conversation/message/participant gone); skipping"
|
|
8220
7090
|
);
|
|
8221
7091
|
return this.concludeBeforeRun(payload, turnLog, startedAt, "turn_context_not_found");
|
|
8222
7092
|
}
|
|
8223
|
-
const reason =
|
|
7093
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
8224
7094
|
turnLog.error({ err: reason }, "dispatcher: failed to fetch turn context");
|
|
8225
7095
|
const fetchReason = `fetch_failed: ${reason}`;
|
|
8226
7096
|
return this.concludeBeforeRun(payload, turnLog, startedAt, fetchReason, fetchReason);
|
|
@@ -8283,20 +7153,11 @@ var Dispatcher = class {
|
|
|
8283
7153
|
effectiveCwd = void 0;
|
|
8284
7154
|
}
|
|
8285
7155
|
let hookEnv;
|
|
8286
|
-
let preparedNativeAssignment;
|
|
8287
7156
|
if (prepareHook) {
|
|
8288
|
-
const
|
|
8289
|
-
const assignmentKey = assignment ? `${assignment.executionId}:${assignment.activationEpoch}` : void 0;
|
|
8290
|
-
const cached2 = readPrepared(
|
|
8291
|
-
workspaceId,
|
|
8292
|
-
payload.conversationId,
|
|
8293
|
-
payload.agentId,
|
|
8294
|
-
assignmentKey
|
|
8295
|
-
);
|
|
7157
|
+
const cached2 = readPrepared(workspaceId, payload.conversationId, payload.agentId);
|
|
8296
7158
|
if (cached2) {
|
|
8297
7159
|
effectiveCwd = cached2.cwd;
|
|
8298
7160
|
hookEnv = cached2.env;
|
|
8299
|
-
preparedNativeAssignment = cached2.nativeWorkAssignment;
|
|
8300
7161
|
} else {
|
|
8301
7162
|
const delayMs = this.opts.preparingRowDelayMs ?? DEFAULT_PREPARING_ROW_DELAY_MS;
|
|
8302
7163
|
let preparingStarted = false;
|
|
@@ -8311,9 +7172,9 @@ var Dispatcher = class {
|
|
|
8311
7172
|
summary: "",
|
|
8312
7173
|
phase,
|
|
8313
7174
|
seq
|
|
8314
|
-
}).catch((
|
|
7175
|
+
}).catch((err) => {
|
|
8315
7176
|
turnLog.warn(
|
|
8316
|
-
{ err:
|
|
7177
|
+
{ err: err instanceof Error ? err.message : String(err), phase },
|
|
8317
7178
|
"dispatcher: preparing-activity report failed (continuing with the hook)"
|
|
8318
7179
|
);
|
|
8319
7180
|
});
|
|
@@ -8335,25 +7196,17 @@ var Dispatcher = class {
|
|
|
8335
7196
|
// tasker prepare hook keys its per-task env off. Defaults to `[]` for
|
|
8336
7197
|
// an older API. The conversation anchor is gone (CT319).
|
|
8337
7198
|
triggerEntryPaths: turnContext.conversation.triggerEntryPaths ?? [],
|
|
8338
|
-
...turnContext.conversation.nativeWorkAssignment ? { nativeWorkAssignment: turnContext.conversation.nativeWorkAssignment } : {},
|
|
8339
7199
|
title: turnContext.conversation.title
|
|
8340
7200
|
});
|
|
8341
7201
|
clearTimeout(preparingTimer);
|
|
8342
7202
|
if (preparingStarted) reportPreparing("done");
|
|
8343
|
-
writePrepared(
|
|
8344
|
-
workspaceId,
|
|
8345
|
-
payload.conversationId,
|
|
8346
|
-
payload.agentId,
|
|
8347
|
-
result,
|
|
8348
|
-
assignmentKey
|
|
8349
|
-
);
|
|
7203
|
+
writePrepared(workspaceId, payload.conversationId, payload.agentId, result);
|
|
8350
7204
|
effectiveCwd = result.cwd;
|
|
8351
7205
|
hookEnv = result.env;
|
|
8352
|
-
|
|
8353
|
-
} catch (err2) {
|
|
7206
|
+
} catch (err) {
|
|
8354
7207
|
clearTimeout(preparingTimer);
|
|
8355
7208
|
if (preparingStarted) reportPreparing("error");
|
|
8356
|
-
const reason =
|
|
7209
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
8357
7210
|
turnLog.error({ err: reason }, "dispatcher: prepare hook failed");
|
|
8358
7211
|
try {
|
|
8359
7212
|
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
@@ -8376,6 +7229,19 @@ ${reason}`,
|
|
|
8376
7229
|
}
|
|
8377
7230
|
}
|
|
8378
7231
|
}
|
|
7232
|
+
let turnEnv = hookEnv;
|
|
7233
|
+
if (effectiveCwd && turnContext.runtime === "codex") {
|
|
7234
|
+
const tmpDir = join12(effectiveCwd, "node_modules", ".cache", "cabane-tmp", turnId);
|
|
7235
|
+
try {
|
|
7236
|
+
mkdirSync10(tmpDir, { recursive: true });
|
|
7237
|
+
turnEnv = { ...hookEnv, TMPDIR: tmpDir };
|
|
7238
|
+
} catch (err) {
|
|
7239
|
+
turnLog.warn(
|
|
7240
|
+
{ err: err instanceof Error ? err.message : String(err), tmpDir },
|
|
7241
|
+
"dispatcher: failed to create per-turn TMPDIR \u2014 proceeding with the inherited temp dir"
|
|
7242
|
+
);
|
|
7243
|
+
}
|
|
7244
|
+
}
|
|
8379
7245
|
const key = runKey(payload.conversationId, payload.agentId);
|
|
8380
7246
|
const abortController = new AbortController();
|
|
8381
7247
|
this.aborts.set(key, abortController);
|
|
@@ -8390,9 +7256,9 @@ ${reason}`,
|
|
|
8390
7256
|
// this live turn for an abandoned one and close it with a `stopped`.
|
|
8391
7257
|
turnId
|
|
8392
7258
|
});
|
|
8393
|
-
} catch (
|
|
7259
|
+
} catch (err) {
|
|
8394
7260
|
turnLog.warn(
|
|
8395
|
-
{ err:
|
|
7261
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
8396
7262
|
"dispatcher: active-run flag set failed terminally; proceeding"
|
|
8397
7263
|
);
|
|
8398
7264
|
}
|
|
@@ -8429,35 +7295,6 @@ ${reason}`,
|
|
|
8429
7295
|
subAgentCreate,
|
|
8430
7296
|
wakeState
|
|
8431
7297
|
);
|
|
8432
|
-
const nativeTurnControl = {
|
|
8433
|
-
summon: (agentId) => {
|
|
8434
|
-
summonState.agentId = agentId;
|
|
8435
|
-
},
|
|
8436
|
-
skip: (reason) => {
|
|
8437
|
-
skipState.skipped = true;
|
|
8438
|
-
skipState.reason = reason;
|
|
8439
|
-
},
|
|
8440
|
-
ask: (p) => {
|
|
8441
|
-
askState.targetUserId = p.targetUserId;
|
|
8442
|
-
if (p.questions && p.questions.length > 0) {
|
|
8443
|
-
askState.questions = p.questions;
|
|
8444
|
-
askState.question = null;
|
|
8445
|
-
askState.headline = null;
|
|
8446
|
-
askState.options = null;
|
|
8447
|
-
} else {
|
|
8448
|
-
askState.question = p.question ?? null;
|
|
8449
|
-
askState.headline = p.headline ?? null;
|
|
8450
|
-
askState.options = p.options ?? null;
|
|
8451
|
-
askState.questions = null;
|
|
8452
|
-
}
|
|
8453
|
-
},
|
|
8454
|
-
wake: (p) => {
|
|
8455
|
-
wakeState.afterSeconds = p.afterSeconds ?? null;
|
|
8456
|
-
wakeState.at = p.at ?? null;
|
|
8457
|
-
wakeState.note = p.note;
|
|
8458
|
-
},
|
|
8459
|
-
subAgent: subAgentCreate
|
|
8460
|
-
};
|
|
8461
7298
|
const request = buildCompanionTurnRequest({
|
|
8462
7299
|
turnContext,
|
|
8463
7300
|
baseUrl: this.opts.baseUrl,
|
|
@@ -8467,11 +7304,11 @@ ${reason}`,
|
|
|
8467
7304
|
...turnContext.turnToken ? { turnToken: turnContext.turnToken } : {},
|
|
8468
7305
|
// SJ524: the hook-resolved cwd overrides the static local cwd.
|
|
8469
7306
|
...effectiveCwd ? { cwd: effectiveCwd } : {},
|
|
8470
|
-
|
|
8471
|
-
|
|
7307
|
+
// CT804: `turnEnv` = the prepare-hook env plus the per-turn checkout-local
|
|
7308
|
+
// TMPDIR (falls back to `hookEnv` when no cwd was resolved).
|
|
7309
|
+
...turnEnv ? { env: turnEnv } : {},
|
|
8472
7310
|
mcpServers: resolvedMcpServers,
|
|
8473
7311
|
summonServer,
|
|
8474
|
-
turnControl: nativeTurnControl,
|
|
8475
7312
|
// CT238: this turn's conversation, forwarded as the active-conversation
|
|
8476
7313
|
// header so a cross-thread post/spawn stamps its origin.
|
|
8477
7314
|
activeConversationId: payload.conversationId,
|
|
@@ -8489,22 +7326,19 @@ ${reason}`,
|
|
|
8489
7326
|
if (this.opts.codexEnabled) {
|
|
8490
7327
|
adapters.push(createCodexAdapter({ enabled: true, onWarn }));
|
|
8491
7328
|
}
|
|
8492
|
-
if (this.opts.cabaneNativeApiKey) {
|
|
8493
|
-
adapters.push(createCabaneNativeAdapter({ apiKey: this.opts.cabaneNativeApiKey, onWarn }));
|
|
8494
|
-
}
|
|
8495
7329
|
const registry = createAdapterRegistry(adapters);
|
|
8496
7330
|
let adapter;
|
|
8497
7331
|
try {
|
|
8498
7332
|
adapter = selectAdapter(registry, turnContext.runtime);
|
|
8499
|
-
} catch (
|
|
8500
|
-
if (!(
|
|
7333
|
+
} catch (err) {
|
|
7334
|
+
if (!(err instanceof RuntimeUnavailableError)) throw err;
|
|
8501
7335
|
turnLog.error(
|
|
8502
|
-
{ runtime:
|
|
7336
|
+
{ runtime: err.runtime, available: err.available },
|
|
8503
7337
|
"dispatcher: turn runtime not available on this device"
|
|
8504
7338
|
);
|
|
8505
7339
|
try {
|
|
8506
7340
|
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
8507
|
-
body: `${RUNTIME_UNAVAILABLE_PREFIX} ${
|
|
7341
|
+
body: `${RUNTIME_UNAVAILABLE_PREFIX} ${err.message}`,
|
|
8508
7342
|
kind: "final",
|
|
8509
7343
|
turnId,
|
|
8510
7344
|
parentMessageId: payload.messageId
|
|
@@ -8519,7 +7353,7 @@ ${reason}`,
|
|
|
8519
7353
|
payload,
|
|
8520
7354
|
turnLog,
|
|
8521
7355
|
startedAt,
|
|
8522
|
-
`runtime_unavailable:${
|
|
7356
|
+
`runtime_unavailable:${err.runtime}`
|
|
8523
7357
|
);
|
|
8524
7358
|
}
|
|
8525
7359
|
if (prepareHook && hookEnv?.CABANE_TASK_ID) {
|
|
@@ -8653,9 +7487,9 @@ ${reason}`,
|
|
|
8653
7487
|
skipState.skipped = true;
|
|
8654
7488
|
skipState.reason = intent.skipReason;
|
|
8655
7489
|
}
|
|
8656
|
-
} catch (
|
|
7490
|
+
} catch (err) {
|
|
8657
7491
|
turnLog.warn(
|
|
8658
|
-
{ err:
|
|
7492
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
8659
7493
|
"dispatcher: turn-control intent fetch failed; turn-control effects for this turn are dropped"
|
|
8660
7494
|
);
|
|
8661
7495
|
}
|
|
@@ -8701,9 +7535,9 @@ ${reason}`,
|
|
|
8701
7535
|
payload.agentId,
|
|
8702
7536
|
{ agentSessionId: event.state }
|
|
8703
7537
|
);
|
|
8704
|
-
} catch (
|
|
7538
|
+
} catch (err) {
|
|
8705
7539
|
turnLog.warn(
|
|
8706
|
-
{ err:
|
|
7540
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
8707
7541
|
"dispatcher: session-id write failed (will retry next turn)"
|
|
8708
7542
|
);
|
|
8709
7543
|
}
|
|
@@ -8752,18 +7586,18 @@ ${reason}`,
|
|
|
8752
7586
|
parentMessageId: payload.messageId,
|
|
8753
7587
|
...skipWake ? { wake: skipWake } : {}
|
|
8754
7588
|
});
|
|
8755
|
-
} catch (
|
|
7589
|
+
} catch (err) {
|
|
8756
7590
|
turnLog.warn(
|
|
8757
|
-
{ err:
|
|
7591
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
8758
7592
|
"dispatcher: skipped-marker commit failed"
|
|
8759
7593
|
);
|
|
8760
7594
|
}
|
|
8761
7595
|
} else {
|
|
8762
7596
|
await committer.finalize(okResult);
|
|
8763
7597
|
}
|
|
8764
|
-
} catch (
|
|
7598
|
+
} catch (err) {
|
|
8765
7599
|
okResult = false;
|
|
8766
|
-
resultReason =
|
|
7600
|
+
resultReason = err instanceof Error ? err.message : String(err);
|
|
8767
7601
|
turnLog.error({ err: resultReason }, "dispatcher: SDK query threw");
|
|
8768
7602
|
} finally {
|
|
8769
7603
|
if (idleTimer) clearTimeout(idleTimer);
|
|
@@ -8799,9 +7633,9 @@ ${reason}`,
|
|
|
8799
7633
|
// CT113: the stopped marker is still "about" the triggering message.
|
|
8800
7634
|
parentMessageId: payload.messageId
|
|
8801
7635
|
});
|
|
8802
|
-
} catch (
|
|
7636
|
+
} catch (err) {
|
|
8803
7637
|
turnLog.warn(
|
|
8804
|
-
{ err:
|
|
7638
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
8805
7639
|
"dispatcher: stopped-marker commit failed"
|
|
8806
7640
|
);
|
|
8807
7641
|
}
|
|
@@ -8842,9 +7676,9 @@ ${reason}`,
|
|
|
8842
7676
|
payload.agentId,
|
|
8843
7677
|
body
|
|
8844
7678
|
);
|
|
8845
|
-
} catch (
|
|
7679
|
+
} catch (err) {
|
|
8846
7680
|
turnLog.warn(
|
|
8847
|
-
{ err:
|
|
7681
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
8848
7682
|
"dispatcher: active-run clear failed terminally; server age-sweep is the backstop"
|
|
8849
7683
|
);
|
|
8850
7684
|
}
|
|
@@ -8912,7 +7746,6 @@ function buildCompanionManifest(opts) {
|
|
|
8912
7746
|
if (opts.claudeCode) runtimes.push({ name: "claude-code", version: v.claudeCode ?? null });
|
|
8913
7747
|
if (opts.opencode) runtimes.push({ name: "opencode", version: v.opencode ?? null });
|
|
8914
7748
|
if (opts.codex) runtimes.push({ name: "codex", version: v.codex ?? null });
|
|
8915
|
-
if (opts.cabaneNative) runtimes.push({ name: "cabane-native", version: null });
|
|
8916
7749
|
return { runtimes, capabilities: { ...DEVICE_MANIFEST.capabilities } };
|
|
8917
7750
|
}
|
|
8918
7751
|
|
|
@@ -9116,7 +7949,7 @@ import {
|
|
|
9116
7949
|
existsSync as existsSync10,
|
|
9117
7950
|
mkdirSync as mkdirSync11,
|
|
9118
7951
|
readdirSync as readdirSync2,
|
|
9119
|
-
readFileSync as
|
|
7952
|
+
readFileSync as readFileSync8,
|
|
9120
7953
|
renameSync as renameSync3,
|
|
9121
7954
|
rmSync as rmSync5,
|
|
9122
7955
|
writeFileSync as writeFileSync7
|
|
@@ -9150,13 +7983,13 @@ var Outbox = class {
|
|
|
9150
7983
|
try {
|
|
9151
7984
|
writeFileSync7(tmp, JSON.stringify(entry) + "\n", "utf8");
|
|
9152
7985
|
renameSync3(tmp, target);
|
|
9153
|
-
} catch (
|
|
7986
|
+
} catch (err) {
|
|
9154
7987
|
try {
|
|
9155
7988
|
rmSync5(tmp, { force: true });
|
|
9156
7989
|
} catch {
|
|
9157
7990
|
}
|
|
9158
7991
|
this.log?.warn(
|
|
9159
|
-
{ workspaceId: this.workspaceId, err:
|
|
7992
|
+
{ workspaceId: this.workspaceId, err: err instanceof Error ? err.message : String(err) },
|
|
9160
7993
|
"companion outbox: failed to persist entry"
|
|
9161
7994
|
);
|
|
9162
7995
|
return;
|
|
@@ -9181,7 +8014,7 @@ var Outbox = class {
|
|
|
9181
8014
|
if (!name.endsWith(".json")) continue;
|
|
9182
8015
|
const full = join13(dir2, name);
|
|
9183
8016
|
try {
|
|
9184
|
-
const parsed = JSON.parse(
|
|
8017
|
+
const parsed = JSON.parse(readFileSync8(full, "utf8"));
|
|
9185
8018
|
if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
|
|
9186
8019
|
entries.push(parsed);
|
|
9187
8020
|
} else {
|
|
@@ -9253,40 +8086,43 @@ var Outbox = class {
|
|
|
9253
8086
|
};
|
|
9254
8087
|
|
|
9255
8088
|
// src/run-config.ts
|
|
9256
|
-
import { z as
|
|
9257
|
-
var mcpStdioServerSchema =
|
|
9258
|
-
type:
|
|
9259
|
-
command:
|
|
9260
|
-
args:
|
|
9261
|
-
env:
|
|
8089
|
+
import { z as z14 } from "zod";
|
|
8090
|
+
var mcpStdioServerSchema = z14.object({
|
|
8091
|
+
type: z14.literal("stdio").optional(),
|
|
8092
|
+
command: z14.string().min(1),
|
|
8093
|
+
args: z14.array(z14.string()).optional(),
|
|
8094
|
+
env: z14.record(z14.string(), z14.string()).optional()
|
|
9262
8095
|
});
|
|
9263
|
-
var mcpHttpServerSchema =
|
|
9264
|
-
type:
|
|
9265
|
-
url:
|
|
9266
|
-
headers:
|
|
8096
|
+
var mcpHttpServerSchema = z14.object({
|
|
8097
|
+
type: z14.literal("http"),
|
|
8098
|
+
url: z14.string().url(),
|
|
8099
|
+
headers: z14.record(z14.string(), z14.string()).optional()
|
|
9267
8100
|
});
|
|
9268
|
-
var mcpSseServerSchema =
|
|
9269
|
-
type:
|
|
9270
|
-
url:
|
|
9271
|
-
headers:
|
|
8101
|
+
var mcpSseServerSchema = z14.object({
|
|
8102
|
+
type: z14.literal("sse"),
|
|
8103
|
+
url: z14.string().url(),
|
|
8104
|
+
headers: z14.record(z14.string(), z14.string()).optional()
|
|
9272
8105
|
});
|
|
9273
|
-
var mcpServerDefSchema =
|
|
8106
|
+
var mcpServerDefSchema = z14.union([
|
|
9274
8107
|
mcpHttpServerSchema,
|
|
9275
8108
|
mcpSseServerSchema,
|
|
9276
8109
|
mcpStdioServerSchema
|
|
9277
8110
|
]);
|
|
9278
|
-
var thinkingConfigSchema =
|
|
9279
|
-
|
|
9280
|
-
|
|
9281
|
-
|
|
8111
|
+
var thinkingConfigSchema = z14.discriminatedUnion("type", [
|
|
8112
|
+
z14.object({ type: z14.literal("adaptive") }),
|
|
8113
|
+
z14.object({ type: z14.literal("enabled"), budgetTokens: z14.number().int().positive().optional() }),
|
|
8114
|
+
z14.object({ type: z14.literal("disabled") })
|
|
9282
8115
|
]);
|
|
9283
|
-
var effortSchema =
|
|
9284
|
-
var runConfigSchema =
|
|
9285
|
-
|
|
9286
|
-
|
|
9287
|
-
|
|
9288
|
-
|
|
9289
|
-
|
|
8116
|
+
var effortSchema = z14.enum(["low", "medium", "high", "xhigh", "max"]);
|
|
8117
|
+
var runConfigSchema = z14.object({
|
|
8118
|
+
// CT788: the host-access binary replaced the `assistant`/`coding`/`custom` mode
|
|
8119
|
+
// trio + its custom tool lists — `true` grants the host filesystem/shell, absent
|
|
8120
|
+
// is the locked surface. Kept in lockstep with `@cabane/shared`'s
|
|
8121
|
+
// `agentRunConfigSchema` (independent zod, forward-compatible: unknown keys are
|
|
8122
|
+
// stripped, so an older companion riding a newer server never rejects the config).
|
|
8123
|
+
hostAccess: z14.boolean().optional(),
|
|
8124
|
+
mcpServers: z14.record(z14.string(), mcpServerDefSchema).optional(),
|
|
8125
|
+
model: z14.string().min(1).optional(),
|
|
9290
8126
|
thinking: thinkingConfigSchema.optional(),
|
|
9291
8127
|
effort: effortSchema.optional()
|
|
9292
8128
|
});
|
|
@@ -9331,20 +8167,20 @@ var SseSubscriber = class {
|
|
|
9331
8167
|
try {
|
|
9332
8168
|
await this.connect();
|
|
9333
8169
|
backoff = 500;
|
|
9334
|
-
} catch (
|
|
8170
|
+
} catch (err) {
|
|
9335
8171
|
if (this.aborted) return;
|
|
9336
|
-
if (
|
|
8172
|
+
if (err instanceof ApiError && (err.status === 401 || err.status === 403)) {
|
|
9337
8173
|
this.opts.log.error(
|
|
9338
|
-
{ workspaceId: this.opts.workspaceId, status:
|
|
8174
|
+
{ workspaceId: this.opts.workspaceId, status: err.status },
|
|
9339
8175
|
"SSE auth failed \u2014 tearing down this workspace subscriber"
|
|
9340
8176
|
);
|
|
9341
|
-
this.opts.onAuthFailure(
|
|
8177
|
+
this.opts.onAuthFailure(err.status);
|
|
9342
8178
|
return;
|
|
9343
8179
|
}
|
|
9344
8180
|
this.opts.log.warn(
|
|
9345
8181
|
{
|
|
9346
8182
|
workspaceId: this.opts.workspaceId,
|
|
9347
|
-
err:
|
|
8183
|
+
err: err instanceof Error ? err.message : String(err),
|
|
9348
8184
|
backoff
|
|
9349
8185
|
},
|
|
9350
8186
|
"SSE disconnected; reconnecting"
|
|
@@ -9487,7 +8323,7 @@ var CompanionSupervisor = class {
|
|
|
9487
8323
|
if (!this.config.deviceToken) {
|
|
9488
8324
|
this.log.warn("companion: not paired (no device token) \u2014 run `cabane-companion pair`");
|
|
9489
8325
|
process.stdout.write(
|
|
9490
|
-
"companion: this device is not paired \u2014 run `cabane-companion pair
|
|
8326
|
+
"companion: this device is not paired \u2014 run `cabane-companion pair`, then confirm the short code in Settings \u2192 Connectors.\n"
|
|
9491
8327
|
);
|
|
9492
8328
|
return;
|
|
9493
8329
|
}
|
|
@@ -9542,9 +8378,6 @@ var CompanionSupervisor = class {
|
|
|
9542
8378
|
// like opencode — the CLI's presence is the operator's responsibility;
|
|
9543
8379
|
// a misconfigured device fails the turn loudly, never silently).
|
|
9544
8380
|
codex: isCodexEnabled(this.config),
|
|
9545
|
-
// CT598: advertise the native runtime when an OpenRouter key is set. Key
|
|
9546
|
-
// absent → not advertised, so a native turn never routes here.
|
|
9547
|
-
cabaneNative: isCabaneNativeEnabled(),
|
|
9548
8381
|
// CT571/CT586: each runtime's `version` from the latest harness probe
|
|
9549
8382
|
// (fail-soft to null). Informational only — the server matches on name.
|
|
9550
8383
|
versions: this.harnessVersions
|
|
@@ -9562,9 +8395,9 @@ var CompanionSupervisor = class {
|
|
|
9562
8395
|
this.hub.setDevice({ deviceId: res.deviceId });
|
|
9563
8396
|
this.deviceId = res.deviceId;
|
|
9564
8397
|
this.checkVersionSkew(res.serverVersion);
|
|
9565
|
-
} catch (
|
|
8398
|
+
} catch (err) {
|
|
9566
8399
|
this.log.warn(
|
|
9567
|
-
{ err:
|
|
8400
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
9568
8401
|
"companion: device heartbeat failed (will retry on next tick)"
|
|
9569
8402
|
);
|
|
9570
8403
|
}
|
|
@@ -9600,12 +8433,12 @@ var CompanionSupervisor = class {
|
|
|
9600
8433
|
const resp = await this.deviceApi.getAssignments();
|
|
9601
8434
|
items = resp.assignments;
|
|
9602
8435
|
device = resp.device;
|
|
9603
|
-
} catch (
|
|
8436
|
+
} catch (err) {
|
|
9604
8437
|
this.log.error(
|
|
9605
|
-
{ err:
|
|
8438
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
9606
8439
|
"companion: assignments pull failed \u2014 check the device is still active in the cabane app"
|
|
9607
8440
|
);
|
|
9608
|
-
this.hub.setDeviceError(
|
|
8441
|
+
this.hub.setDeviceError(err instanceof Error ? err.message : String(err));
|
|
9609
8442
|
return;
|
|
9610
8443
|
}
|
|
9611
8444
|
this.hub.setDevice({ deviceId: device.id, deviceLabel: device.label });
|
|
@@ -9682,7 +8515,7 @@ var CompanionSupervisor = class {
|
|
|
9682
8515
|
agentId: it.agentId,
|
|
9683
8516
|
username: it.agentUsername,
|
|
9684
8517
|
displayName: it.agentDisplayName,
|
|
9685
|
-
mode: runConfig.
|
|
8518
|
+
mode: runConfig.hostAccess ? "full" : "none",
|
|
9686
8519
|
hasCredential: false,
|
|
9687
8520
|
missingSecrets: missing
|
|
9688
8521
|
});
|
|
@@ -9702,7 +8535,7 @@ var CompanionSupervisor = class {
|
|
|
9702
8535
|
agentId: it.agentId,
|
|
9703
8536
|
username: it.agentUsername,
|
|
9704
8537
|
displayName: it.agentDisplayName,
|
|
9705
|
-
mode: runConfig.
|
|
8538
|
+
mode: runConfig.hostAccess ? "full" : "none",
|
|
9706
8539
|
hasCredential: true,
|
|
9707
8540
|
missingSecrets: missing
|
|
9708
8541
|
});
|
|
@@ -9786,12 +8619,9 @@ var CompanionSupervisor = class {
|
|
|
9786
8619
|
// CT481: register the codex adapter when this device offers codex; unset
|
|
9787
8620
|
// leaves an `openai/…` turn to fail loudly (no silent claude-code fallback).
|
|
9788
8621
|
...isCodexEnabled(this.config) ? { codexEnabled: true } : {},
|
|
9789
|
-
// CT598: register the cabane-native adapter when an OpenRouter key is set;
|
|
9790
|
-
// unset leaves a `cabane-native/…` turn to fail loudly (no silent fallback).
|
|
9791
|
-
...cabaneNativeApiKey() ? { cabaneNativeApiKey: cabaneNativeApiKey() } : {},
|
|
9792
8622
|
// CT556: per-turn timeout watchdog windows, from the companion's own env
|
|
9793
8623
|
// (`AGENT_IDLE_TIMEOUT_MS` / `AGENT_TOTAL_TIMEOUT_MS`). Unset → the
|
|
9794
|
-
// dispatcher's baked-in defaults (10 min idle /
|
|
8624
|
+
// dispatcher's baked-in defaults (10 min idle / 6h total).
|
|
9795
8625
|
...positiveIntEnv("AGENT_IDLE_TIMEOUT_MS") !== void 0 ? { idleTimeoutMs: positiveIntEnv("AGENT_IDLE_TIMEOUT_MS") } : {},
|
|
9796
8626
|
...positiveIntEnv("AGENT_TOTAL_TIMEOUT_MS") !== void 0 ? { totalTimeoutMs: positiveIntEnv("AGENT_TOTAL_TIMEOUT_MS") } : {},
|
|
9797
8627
|
observer: this.hub.observerFor(ctx.workspaceId, ctx.agentId, ctx.workspaceSlug),
|
|
@@ -9858,9 +8688,9 @@ var CompanionSupervisor = class {
|
|
|
9858
8688
|
let wire;
|
|
9859
8689
|
try {
|
|
9860
8690
|
wire = JSON.parse(ev.data);
|
|
9861
|
-
} catch (
|
|
8691
|
+
} catch (err) {
|
|
9862
8692
|
this.log.warn(
|
|
9863
|
-
{ err:
|
|
8693
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
9864
8694
|
"malformed SSE payload"
|
|
9865
8695
|
);
|
|
9866
8696
|
return;
|
|
@@ -9907,13 +8737,13 @@ var CompanionSupervisor = class {
|
|
|
9907
8737
|
}
|
|
9908
8738
|
const chainKey = `${payload.conversationId}|${payload.agentId}`;
|
|
9909
8739
|
const prev = wr.chains.get(chainKey) ?? Promise.resolve();
|
|
9910
|
-
const tail = prev.then(() => this.runDispatch(wr, ev, payload, agent)).catch((
|
|
8740
|
+
const tail = prev.then(() => this.runDispatch(wr, ev, payload, agent)).catch((err) => {
|
|
9911
8741
|
this.log.warn(
|
|
9912
8742
|
{
|
|
9913
8743
|
workspaceId: wr.workspaceId,
|
|
9914
8744
|
conversationId: payload.conversationId,
|
|
9915
8745
|
agentId: payload.agentId,
|
|
9916
|
-
err:
|
|
8746
|
+
err: err instanceof Error ? err.message : String(err)
|
|
9917
8747
|
},
|
|
9918
8748
|
"companion: conversation turn handler threw"
|
|
9919
8749
|
);
|
|
@@ -10005,10 +8835,10 @@ var CompanionSupervisor = class {
|
|
|
10005
8835
|
} else {
|
|
10006
8836
|
drainDelay = DRAIN_BASE_MS;
|
|
10007
8837
|
}
|
|
10008
|
-
} catch (
|
|
8838
|
+
} catch (err) {
|
|
10009
8839
|
drainDelay = Math.min(drainDelay * 2, DRAIN_MAX_MS);
|
|
10010
8840
|
this.log.warn(
|
|
10011
|
-
{ agentId, err:
|
|
8841
|
+
{ agentId, err: err instanceof Error ? err.message : String(err) },
|
|
10012
8842
|
"companion: outbox drain pass threw (will retry with backoff)"
|
|
10013
8843
|
);
|
|
10014
8844
|
} finally {
|
|
@@ -10075,9 +8905,9 @@ var CompanionSupervisor = class {
|
|
|
10075
8905
|
codex: signals.codexVersion
|
|
10076
8906
|
};
|
|
10077
8907
|
this.hub.setHarnesses(deriveHarnessSnapshot(signals).harnesses);
|
|
10078
|
-
} catch (
|
|
8908
|
+
} catch (err) {
|
|
10079
8909
|
this.log.warn(
|
|
10080
|
-
{ err:
|
|
8910
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
10081
8911
|
"companion: harness probe failed (will retry on next beat)"
|
|
10082
8912
|
);
|
|
10083
8913
|
}
|
|
@@ -10175,10 +9005,13 @@ var CompanionSupervisor = class {
|
|
|
10175
9005
|
await Promise.race([
|
|
10176
9006
|
Promise.allSettled(turns),
|
|
10177
9007
|
new Promise((resolve) => {
|
|
10178
|
-
timer = setTimeout(
|
|
10179
|
-
|
|
10180
|
-
|
|
10181
|
-
|
|
9008
|
+
timer = setTimeout(
|
|
9009
|
+
() => {
|
|
9010
|
+
timedOut = true;
|
|
9011
|
+
resolve();
|
|
9012
|
+
},
|
|
9013
|
+
Math.max(0, graceMs)
|
|
9014
|
+
);
|
|
10182
9015
|
timer.unref?.();
|
|
10183
9016
|
})
|
|
10184
9017
|
]);
|
|
@@ -10232,17 +9065,17 @@ var RECOVERABLE_CODES = /* @__PURE__ */ new Set([
|
|
|
10232
9065
|
"ERR_STREAM_DESTROYED",
|
|
10233
9066
|
"ERR_STREAM_WRITE_AFTER_END"
|
|
10234
9067
|
]);
|
|
10235
|
-
function errorCode(
|
|
10236
|
-
if (
|
|
10237
|
-
const code =
|
|
9068
|
+
function errorCode(err) {
|
|
9069
|
+
if (err && typeof err === "object" && "code" in err) {
|
|
9070
|
+
const code = err.code;
|
|
10238
9071
|
if (typeof code === "string") return code;
|
|
10239
9072
|
}
|
|
10240
9073
|
return void 0;
|
|
10241
9074
|
}
|
|
10242
|
-
function isRecoverableSocketError(
|
|
10243
|
-
const code = errorCode(
|
|
9075
|
+
function isRecoverableSocketError(err) {
|
|
9076
|
+
const code = errorCode(err);
|
|
10244
9077
|
if (code && RECOVERABLE_CODES.has(code)) return true;
|
|
10245
|
-
const message =
|
|
9078
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
10246
9079
|
return /\bEPIPE\b|\bECONNRESET\b/.test(message);
|
|
10247
9080
|
}
|
|
10248
9081
|
function installProcessSafetyNet(log, opts = {}) {
|
|
@@ -10251,16 +9084,16 @@ function installProcessSafetyNet(log, opts = {}) {
|
|
|
10251
9084
|
stream.on("error", () => {
|
|
10252
9085
|
});
|
|
10253
9086
|
}
|
|
10254
|
-
proc.on("uncaughtException", (
|
|
9087
|
+
proc.on("uncaughtException", (err) => handleUncaught(log, err, "uncaughtException"));
|
|
10255
9088
|
proc.on(
|
|
10256
9089
|
"unhandledRejection",
|
|
10257
9090
|
(reason) => handleUncaught(log, reason, "unhandledRejection")
|
|
10258
9091
|
);
|
|
10259
9092
|
}
|
|
10260
|
-
function handleUncaught(log,
|
|
10261
|
-
const message =
|
|
10262
|
-
const code = errorCode(
|
|
10263
|
-
if (isRecoverableSocketError(
|
|
9093
|
+
function handleUncaught(log, err, origin) {
|
|
9094
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
9095
|
+
const code = errorCode(err);
|
|
9096
|
+
if (isRecoverableSocketError(err)) {
|
|
10264
9097
|
log.warn(
|
|
10265
9098
|
{ origin, code, err: message },
|
|
10266
9099
|
"companion: recovered from a socket error (kept running \u2014 a broken pipe never kills the companion)"
|
|
@@ -10268,13 +9101,13 @@ function handleUncaught(log, err2, origin) {
|
|
|
10268
9101
|
return;
|
|
10269
9102
|
}
|
|
10270
9103
|
log.error(
|
|
10271
|
-
{ origin, code, err: message, stack:
|
|
9104
|
+
{ origin, code, err: message, stack: err instanceof Error ? err.stack : void 0 },
|
|
10272
9105
|
"companion: uncaught error (kept running \u2014 see the stack above)"
|
|
10273
9106
|
);
|
|
10274
9107
|
}
|
|
10275
9108
|
|
|
10276
9109
|
// src/crash-marker.ts
|
|
10277
|
-
import { existsSync as existsSync11, mkdirSync as mkdirSync12, readFileSync as
|
|
9110
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync12, readFileSync as readFileSync9, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
|
|
10278
9111
|
import { join as join14 } from "path";
|
|
10279
9112
|
function crashMarkerPath() {
|
|
10280
9113
|
return join14(cabaneDir(), "last-error.json");
|
|
@@ -10305,14 +9138,14 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
10305
9138
|
cfg = requireConfig();
|
|
10306
9139
|
claudeCode = await probeClaude();
|
|
10307
9140
|
await ensureRuntimeAvailable(cfg, { probeClaude: async () => claudeCode });
|
|
10308
|
-
} catch (
|
|
9141
|
+
} catch (err) {
|
|
10309
9142
|
recordCrash({
|
|
10310
|
-
reason:
|
|
10311
|
-
...errorCode(
|
|
9143
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
9144
|
+
...errorCode(err) ? { code: errorCode(err) } : {},
|
|
10312
9145
|
origin: "startup",
|
|
10313
9146
|
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
10314
9147
|
});
|
|
10315
|
-
throw
|
|
9148
|
+
throw err;
|
|
10316
9149
|
}
|
|
10317
9150
|
if (cfg.logLevel) log.level = cfg.logLevel;
|
|
10318
9151
|
const harnessVersions = await probeHarnessVersions({
|
|
@@ -10403,6 +9236,8 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
10403
9236
|
|
|
10404
9237
|
// src/commands/start.ts
|
|
10405
9238
|
var FORCE_EXIT_MS = 4e3;
|
|
9239
|
+
var DEPLOY_REEXEC_EXIT = 75;
|
|
9240
|
+
var DEPLOY_GRACE_MS = 6.5 * 60 * 60 * 1e3;
|
|
10406
9241
|
async function start(opts = {}) {
|
|
10407
9242
|
const result = await createCompanionRuntime({
|
|
10408
9243
|
...opts.port !== void 0 ? { port: opts.port } : {}
|
|
@@ -10426,7 +9261,7 @@ Cabane Companion is running.
|
|
|
10426
9261
|
`);
|
|
10427
9262
|
if (!runtime.config.deviceToken) {
|
|
10428
9263
|
process.stdout.write(
|
|
10429
|
-
`This device isn't paired yet \u2014 run \`cabane-companion pair
|
|
9264
|
+
`This device isn't paired yet \u2014 run \`cabane-companion pair\`, then confirm the short code in Settings \u2192 Connectors.
|
|
10430
9265
|
|
|
10431
9266
|
`
|
|
10432
9267
|
);
|
|
@@ -10466,25 +9301,25 @@ companion: received ${signal}, shutting down\u2026
|
|
|
10466
9301
|
if (shuttingDown) return;
|
|
10467
9302
|
shuttingDown = true;
|
|
10468
9303
|
const configuredGrace = Number.parseInt(
|
|
10469
|
-
process.env.CABANE_COMPANION_DEPLOY_GRACE_MS ??
|
|
9304
|
+
process.env.CABANE_COMPANION_DEPLOY_GRACE_MS ?? String(DEPLOY_GRACE_MS),
|
|
10470
9305
|
10
|
|
10471
9306
|
);
|
|
10472
|
-
const graceMs = Number.isFinite(configuredGrace) ? configuredGrace :
|
|
9307
|
+
const graceMs = Number.isFinite(configuredGrace) ? configuredGrace : DEPLOY_GRACE_MS;
|
|
10473
9308
|
process.stdout.write(`
|
|
10474
9309
|
companion: deploy drain requested (${graceMs}ms grace)\u2026
|
|
10475
9310
|
`);
|
|
10476
9311
|
void runtime.drainForRestart(graceMs).then(({ drained }) => {
|
|
10477
9312
|
process.stdout.write(
|
|
10478
|
-
drained ? "companion: deploy drain complete.\n" : "companion: deploy grace expired; unfinished turns
|
|
9313
|
+
drained ? "companion: deploy drain complete; re-execing onto the new dist.\n" : "companion: deploy grace expired; re-execing \u2014 unfinished turns resume after restart.\n"
|
|
10479
9314
|
);
|
|
10480
9315
|
resolve();
|
|
10481
|
-
process.exit(
|
|
10482
|
-
}).catch((
|
|
9316
|
+
process.exit(DEPLOY_REEXEC_EXIT);
|
|
9317
|
+
}).catch((err) => {
|
|
10483
9318
|
process.stderr.write(
|
|
10484
|
-
`companion: deploy drain failed: ${
|
|
9319
|
+
`companion: deploy drain failed: ${err instanceof Error ? err.message : String(err)}
|
|
10485
9320
|
`
|
|
10486
9321
|
);
|
|
10487
|
-
process.exit(
|
|
9322
|
+
process.exit(DEPLOY_REEXEC_EXIT);
|
|
10488
9323
|
});
|
|
10489
9324
|
});
|
|
10490
9325
|
});
|
|
@@ -10496,7 +9331,7 @@ async function status() {
|
|
|
10496
9331
|
const cfg = loadConfig();
|
|
10497
9332
|
if (!cfg || !cfg.deviceToken) {
|
|
10498
9333
|
process.stdout.write(
|
|
10499
|
-
"companion: not paired.
|
|
9334
|
+
"companion: not paired. Run `cabane-companion pair`, then confirm the short code in Settings \u2192 Connectors.\n"
|
|
10500
9335
|
);
|
|
10501
9336
|
process.exitCode = 1;
|
|
10502
9337
|
return;
|
|
@@ -10628,7 +9463,7 @@ function isAlive(kill, pid) {
|
|
|
10628
9463
|
}
|
|
10629
9464
|
|
|
10630
9465
|
// src/commands/transcript.ts
|
|
10631
|
-
import { existsSync as existsSync12, readFileSync as
|
|
9466
|
+
import { existsSync as existsSync12, readFileSync as readFileSync10, readdirSync as readdirSync4 } from "fs";
|
|
10632
9467
|
import { isAbsolute, join as join15 } from "path";
|
|
10633
9468
|
async function transcript(opts = {}) {
|
|
10634
9469
|
const dir2 = transcriptsDir();
|
|
@@ -10722,14 +9557,14 @@ var TranscriptFollower = class {
|
|
|
10722
9557
|
function isComplete(content) {
|
|
10723
9558
|
for (const line of content.split("\n")) {
|
|
10724
9559
|
if (!line.trim()) continue;
|
|
10725
|
-
if (
|
|
9560
|
+
if (str2(rec(safeParse(line))?.type) === "_outcome") return true;
|
|
10726
9561
|
}
|
|
10727
9562
|
return false;
|
|
10728
9563
|
}
|
|
10729
9564
|
async function followTranscripts(dir2) {
|
|
10730
9565
|
const follower = new TranscriptFollower({
|
|
10731
9566
|
listFiles: () => listFiles(dir2),
|
|
10732
|
-
read: (f) =>
|
|
9567
|
+
read: (f) => readFileSync10(join15(dir2, f), "utf8"),
|
|
10733
9568
|
write: (s) => process.stdout.write(s),
|
|
10734
9569
|
// CSI: cursor up `n` lines, then erase from cursor to end of screen.
|
|
10735
9570
|
clearLines: (n) => process.stdout.write(`\x1B[${n}A\x1B[0J`),
|
|
@@ -10771,13 +9606,13 @@ function printList(dir2) {
|
|
|
10771
9606
|
for (const f of files.slice(0, 20)) {
|
|
10772
9607
|
const { meta, outcome } = peek(join15(dir2, f));
|
|
10773
9608
|
const when = fmtTime(rec(meta)?.ts);
|
|
10774
|
-
const ws =
|
|
9609
|
+
const ws = str2(rec(meta)?.workspaceSlug);
|
|
10775
9610
|
const o = rec(outcome);
|
|
10776
|
-
const verdict = o ? o.ok === true ? "ok" : `ERROR${
|
|
9611
|
+
const verdict = o ? o.ok === true ? "ok" : `ERROR${str2(o.reason) ? ` (${str2(o.reason)})` : ""}` : "\u2026";
|
|
10777
9612
|
process.stdout.write(
|
|
10778
9613
|
` ${f}
|
|
10779
9614
|
${when} \xB7 ${ws} \xB7 ${verdict}
|
|
10780
|
-
\u201C${excerpt(
|
|
9615
|
+
\u201C${excerpt(str2(rec(meta)?.message), 70)}\u201D
|
|
10781
9616
|
|
|
10782
9617
|
`
|
|
10783
9618
|
);
|
|
@@ -10790,10 +9625,10 @@ function peek(path3) {
|
|
|
10790
9625
|
let meta;
|
|
10791
9626
|
let outcome;
|
|
10792
9627
|
try {
|
|
10793
|
-
for (const line of
|
|
9628
|
+
for (const line of readFileSync10(path3, "utf8").split("\n")) {
|
|
10794
9629
|
if (!line.trim()) continue;
|
|
10795
9630
|
const o = safeParse(line);
|
|
10796
|
-
const t =
|
|
9631
|
+
const t = str2(rec(o)?.type);
|
|
10797
9632
|
if (t === "_meta") meta = o;
|
|
10798
9633
|
else if (t === "_outcome") outcome = o;
|
|
10799
9634
|
}
|
|
@@ -10823,10 +9658,10 @@ function resolveTarget(dir2, target) {
|
|
|
10823
9658
|
function renderFile(path3) {
|
|
10824
9659
|
let content;
|
|
10825
9660
|
try {
|
|
10826
|
-
content =
|
|
10827
|
-
} catch (
|
|
9661
|
+
content = readFileSync10(path3, "utf8");
|
|
9662
|
+
} catch (err) {
|
|
10828
9663
|
throw new CompanionError(
|
|
10829
|
-
`couldn't read ${path3}: ${
|
|
9664
|
+
`couldn't read ${path3}: ${err instanceof Error ? err.message : String(err)}`
|
|
10830
9665
|
);
|
|
10831
9666
|
}
|
|
10832
9667
|
return renderTranscript(content.split("\n"));
|
|
@@ -10838,19 +9673,19 @@ function renderTranscript(jsonlLines) {
|
|
|
10838
9673
|
if (!raw.trim()) continue;
|
|
10839
9674
|
const obj = rec(safeParse(raw));
|
|
10840
9675
|
if (!obj) continue;
|
|
10841
|
-
switch (
|
|
9676
|
+
switch (str2(obj.type)) {
|
|
10842
9677
|
case "_meta":
|
|
10843
9678
|
out.push(
|
|
10844
|
-
`${fmtTime(obj.ts)} \xB7 workspace=${
|
|
9679
|
+
`${fmtTime(obj.ts)} \xB7 workspace=${str2(obj.workspaceSlug)} \xB7 conversation=${str2(obj.conversationId)}`
|
|
10845
9680
|
);
|
|
10846
|
-
out.push("", `> USER: ${
|
|
9681
|
+
out.push("", `> USER: ${str2(obj.message)}`, "");
|
|
10847
9682
|
break;
|
|
10848
9683
|
case "system":
|
|
10849
|
-
if (
|
|
10850
|
-
out.push(`[session ${
|
|
9684
|
+
if (str2(obj.subtype) === "init") {
|
|
9685
|
+
out.push(`[session ${str2(obj.session_id) || "?"} \xB7 model ${str2(obj.model) || "?"}]`);
|
|
10851
9686
|
const servers = Array.isArray(obj.mcp_servers) ? obj.mcp_servers.map((s) => {
|
|
10852
9687
|
const r = rec(s);
|
|
10853
|
-
return r ? `${
|
|
9688
|
+
return r ? `${str2(r.name) || "?"}${str2(r.status) ? `(${str2(r.status)})` : ""}` : "";
|
|
10854
9689
|
}).filter(Boolean).join(", ") : "";
|
|
10855
9690
|
if (servers) out.push(` MCP servers: ${servers}`);
|
|
10856
9691
|
if (Array.isArray(obj.tools)) out.push(` tools: ${obj.tools.length} available`);
|
|
@@ -10876,8 +9711,8 @@ function renderTranscript(jsonlLines) {
|
|
|
10876
9711
|
if (!Array.isArray(content)) break;
|
|
10877
9712
|
for (const b of content) {
|
|
10878
9713
|
const block = rec(b);
|
|
10879
|
-
if (!block ||
|
|
10880
|
-
const id =
|
|
9714
|
+
if (!block || str2(block.type) !== "tool_result") continue;
|
|
9715
|
+
const id = str2(block.tool_use_id);
|
|
10881
9716
|
const label = pending.get(id) ?? "tool";
|
|
10882
9717
|
pending.delete(id);
|
|
10883
9718
|
const tag = block.is_error === true ? "ERROR" : "ok";
|
|
@@ -10886,18 +9721,18 @@ function renderTranscript(jsonlLines) {
|
|
|
10886
9721
|
break;
|
|
10887
9722
|
}
|
|
10888
9723
|
case "result": {
|
|
10889
|
-
const isErr = obj.is_error === true ||
|
|
9724
|
+
const isErr = obj.is_error === true || str2(obj.subtype) !== "success";
|
|
10890
9725
|
const dur = typeof obj.duration_ms === "number" ? ` \xB7 ${obj.duration_ms}ms` : "";
|
|
10891
9726
|
out.push(
|
|
10892
|
-
`[result ${isErr ? "error" : "ok"}${
|
|
9727
|
+
`[result ${isErr ? "error" : "ok"}${str2(obj.subtype) ? ` \xB7 ${str2(obj.subtype)}` : ""}${dur}]`
|
|
10893
9728
|
);
|
|
10894
|
-
if (isErr &&
|
|
9729
|
+
if (isErr && str2(obj.result).trim()) out.push(` ${indent(str2(obj.result))}`);
|
|
10895
9730
|
break;
|
|
10896
9731
|
}
|
|
10897
9732
|
case "_outcome": {
|
|
10898
9733
|
const dur = typeof obj.durationMs === "number" ? ` \xB7 ${obj.durationMs}ms` : "";
|
|
10899
9734
|
out.push(
|
|
10900
|
-
`[outcome ${obj.ok === true ? "ok" : "error"}${
|
|
9735
|
+
`[outcome ${obj.ok === true ? "ok" : "error"}${str2(obj.reason) ? ` \xB7 ${str2(obj.reason)}` : ""}${dur}]`
|
|
10901
9736
|
);
|
|
10902
9737
|
break;
|
|
10903
9738
|
}
|
|
@@ -10915,7 +9750,7 @@ function safeParse(s) {
|
|
|
10915
9750
|
function rec(v) {
|
|
10916
9751
|
return v && typeof v === "object" ? v : null;
|
|
10917
9752
|
}
|
|
10918
|
-
function
|
|
9753
|
+
function str2(v) {
|
|
10919
9754
|
return typeof v === "string" ? v : "";
|
|
10920
9755
|
}
|
|
10921
9756
|
function fmtTime(ts) {
|
|
@@ -10954,22 +9789,14 @@ program.name("cabane-companion").description(
|
|
|
10954
9789
|
).version(COMPANION_VERSION);
|
|
10955
9790
|
program.command("pair").description(
|
|
10956
9791
|
"pair this device with cabane \u2014 shows a short code you confirm in Settings \u2192 Devices."
|
|
10957
|
-
).
|
|
10958
|
-
|
|
10959
|
-
|
|
10960
|
-
)
|
|
10961
|
-
|
|
10962
|
-
|
|
10963
|
-
)
|
|
10964
|
-
|
|
10965
|
-
await pair({
|
|
10966
|
-
...pairingString !== void 0 ? { arg: pairingString } : {},
|
|
10967
|
-
...opts.server !== void 0 ? { server: opts.server } : {},
|
|
10968
|
-
...opts.legacy ? { legacy: true } : {},
|
|
10969
|
-
...opts.file !== void 0 ? { file: opts.file } : {}
|
|
10970
|
-
});
|
|
10971
|
-
}
|
|
10972
|
-
);
|
|
9792
|
+
).allowExcessArguments(false).option("--server <url>", "the cabane instance to pair with (default https://cabane.ai)").action(async (opts) => {
|
|
9793
|
+
await pair({
|
|
9794
|
+
...opts.server !== void 0 ? { server: opts.server } : {}
|
|
9795
|
+
});
|
|
9796
|
+
});
|
|
9797
|
+
program.command("write-paired-config", { hidden: true }).description("persist an already-completed device enrollment payload from stdin.").action(() => {
|
|
9798
|
+
writeCompletedPairing(readFileSync11(0, "utf8"));
|
|
9799
|
+
});
|
|
10973
9800
|
program.command("start").description("pull this device\u2019s assigned agents from cabane and run them.").option("--open", "open the dashboard in a browser on startup (default: off)").option("--no-open", "don't auto-open the dashboard (overrides config.autoOpen)").option("--daemon", "run detached in the background (terminal returns; replies keep landing)").option("--port <port>", "dashboard port (default 7474; falls through if taken)", parsePort).action(async (opts) => {
|
|
10974
9801
|
if (opts.daemon) {
|
|
10975
9802
|
await startDaemon({
|
|
@@ -11015,19 +9842,19 @@ function parsePort(raw) {
|
|
|
11015
9842
|
}
|
|
11016
9843
|
return n;
|
|
11017
9844
|
}
|
|
11018
|
-
program.parseAsync(process.argv).catch((
|
|
11019
|
-
if (
|
|
11020
|
-
process.stderr.write(`error: ${
|
|
9845
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
9846
|
+
if (err instanceof CompanionError) {
|
|
9847
|
+
process.stderr.write(`error: ${err.message}
|
|
11021
9848
|
`);
|
|
11022
9849
|
process.exitCode = 1;
|
|
11023
9850
|
return;
|
|
11024
9851
|
}
|
|
11025
|
-
if (
|
|
9852
|
+
if (err && typeof err === "object" && "name" in err && err.name === "ExitPromptError") {
|
|
11026
9853
|
process.stderr.write("cancelled\n");
|
|
11027
9854
|
process.exitCode = 130;
|
|
11028
9855
|
return;
|
|
11029
9856
|
}
|
|
11030
|
-
process.stderr.write(`${
|
|
9857
|
+
process.stderr.write(`${err instanceof Error ? err.stack ?? err.message : String(err)}
|
|
11031
9858
|
`);
|
|
11032
9859
|
process.exitCode = 1;
|
|
11033
9860
|
});
|