@cabane/companion 0.6.202 → 0.6.203
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/dist/cli.js +429 -113
- package/dist/runtime.js +264 -28
- package/package.json +1 -1
package/dist/runtime.js
CHANGED
|
@@ -1950,7 +1950,7 @@ var CabaneApi = class {
|
|
|
1950
1950
|
// text use (turnId, seq) within each kind for nonzero seq; tool activity
|
|
1951
1951
|
// uses (turnId, toolUseId). Legacy zero/missing seq bypasses row deduplication.
|
|
1952
1952
|
// Server fences and the outbox's discard/eviction bounds still apply.
|
|
1953
|
-
async durableCommit(kind, path, body, turnId, seq, signal) {
|
|
1953
|
+
async durableCommit(kind, path, body, turnId, seq, signal, trailPath) {
|
|
1954
1954
|
try {
|
|
1955
1955
|
await this.request("POST", path, body, {
|
|
1956
1956
|
retry: true,
|
|
@@ -1961,7 +1961,16 @@ var CabaneApi = class {
|
|
|
1961
1961
|
if (!outbox) throw err;
|
|
1962
1962
|
if (signal?.aborted || isAbortError(err)) throw err;
|
|
1963
1963
|
if (!isRetryable(err)) throw err;
|
|
1964
|
-
outbox.persist({
|
|
1964
|
+
outbox.persist({
|
|
1965
|
+
enqueuedAt: Date.now(),
|
|
1966
|
+
turnId,
|
|
1967
|
+
seq,
|
|
1968
|
+
method: "POST",
|
|
1969
|
+
path,
|
|
1970
|
+
body,
|
|
1971
|
+
kind,
|
|
1972
|
+
...trailPath ? { trailPath } : {}
|
|
1973
|
+
});
|
|
1965
1974
|
this.opts.log?.debug(
|
|
1966
1975
|
{ kind, turnId, seq, err: err instanceof Error ? err.message : String(err) },
|
|
1967
1976
|
"companion: commit queued to outbox after transient failure (will drain when the API returns)"
|
|
@@ -1986,6 +1995,29 @@ var CabaneApi = class {
|
|
|
1986
1995
|
progressed = true;
|
|
1987
1996
|
} catch (err) {
|
|
1988
1997
|
if (err instanceof ApiError && err.status >= 400 && err.status < 500) {
|
|
1998
|
+
if (entry.kind === "message" && entry.trailPath && isAlreadyPublished(err)) {
|
|
1999
|
+
try {
|
|
2000
|
+
await this.request("POST", entry.trailPath, trailOf(entry.body), {
|
|
2001
|
+
retry: true
|
|
2002
|
+
});
|
|
2003
|
+
} catch (trailErr) {
|
|
2004
|
+
if (!(trailErr instanceof ApiError && trailErr.status >= 400 && trailErr.status < 500)) {
|
|
2005
|
+
break;
|
|
2006
|
+
}
|
|
2007
|
+
this.opts.log?.warn(
|
|
2008
|
+
{
|
|
2009
|
+
kind: entry.kind,
|
|
2010
|
+
turnId: entry.turnId,
|
|
2011
|
+
seq: entry.seq,
|
|
2012
|
+
...apiErrorLogFields(trailErr)
|
|
2013
|
+
},
|
|
2014
|
+
`Cabane rejected a postscript; it could not be filed. ${mayNeedUpdate()}`
|
|
2015
|
+
);
|
|
2016
|
+
}
|
|
2017
|
+
outbox.remove(entry.turnId, entry.seq);
|
|
2018
|
+
progressed = true;
|
|
2019
|
+
continue;
|
|
2020
|
+
}
|
|
1989
2021
|
if (entry.kind === "active-run" && isTurnEndedRefusal(err)) {
|
|
1990
2022
|
this.opts.log?.debug(
|
|
1991
2023
|
{ kind: entry.kind, turnId: entry.turnId, ...apiErrorLogFields(err) },
|
|
@@ -2061,6 +2093,19 @@ var CabaneApi = class {
|
|
|
2061
2093
|
(e) => e.turnId === turnId && e.kind === "message" && e.body?.kind !== "runtime_notice"
|
|
2062
2094
|
);
|
|
2063
2095
|
}
|
|
2096
|
+
// The `seq` of this turn's SELECTED ANSWER still waiting in the outbox — the
|
|
2097
|
+
// `declared` commit the server's `answerArmed` cannot see — or null when none
|
|
2098
|
+
// is queued. A resumed run that re-armed over it would let a newer text
|
|
2099
|
+
// consume the arm first; with the selection queued, the resumed run starts
|
|
2100
|
+
// declared, its texts are trail, and the drain delivers the selection.
|
|
2101
|
+
outboxDeclaredSeqForTurn(turnId) {
|
|
2102
|
+
const outbox = this.opts.outbox;
|
|
2103
|
+
if (!outbox) return null;
|
|
2104
|
+
const queued = outbox.list().find(
|
|
2105
|
+
(e) => e.turnId === turnId && e.kind === "message" && e.body?.declared === true
|
|
2106
|
+
);
|
|
2107
|
+
return queued ? queued.seq : null;
|
|
2108
|
+
}
|
|
2064
2109
|
// ADOPT a turn context's issuance as the execution source of its work, once,
|
|
2065
2110
|
// before preparation emits or queues anything, so every row can name a source
|
|
2066
2111
|
// the server durably recorded and will never reap. Adoption is idempotent, so
|
|
@@ -2234,15 +2279,36 @@ var CabaneApi = class {
|
|
|
2234
2279
|
// seq also orders speech against the turn's tool/thinking/text trail.
|
|
2235
2280
|
// Transport/5xx failures get bounded retries and optional outbox delivery;
|
|
2236
2281
|
// terminal 4xx and cancellation stop delivery rather than proving no commit.
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2282
|
+
//
|
|
2283
|
+
// A turn's answer can be out before a later speech commit arrives — the
|
|
2284
|
+
// declaration published it, or another host's selection drained first — and
|
|
2285
|
+
// the server refuses the commit `409 already_published`. The text is a
|
|
2286
|
+
// postscript then: with `trail` (the committing agent, which names the text
|
|
2287
|
+
// route), it is filed as trail at the same seq, live here and from the outbox
|
|
2288
|
+
// on a drain. Without `trail` (a runtime notice) the refusal is terminal.
|
|
2289
|
+
async postTurnMessage(workspaceId, conversationId, body, signal, trail) {
|
|
2290
|
+
const trailPath = trail ? `/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${trail.agentId}/text` : void 0;
|
|
2291
|
+
try {
|
|
2292
|
+
await this.durableCommit(
|
|
2293
|
+
"message",
|
|
2294
|
+
`/api/workspaces/${workspaceId}/conversations/${conversationId}/messages`,
|
|
2295
|
+
body,
|
|
2296
|
+
body.turnId,
|
|
2297
|
+
body.seq ?? 0,
|
|
2298
|
+
signal,
|
|
2299
|
+
trailPath
|
|
2300
|
+
);
|
|
2301
|
+
} catch (err) {
|
|
2302
|
+
if (!trailPath || !isAlreadyPublished(err)) throw err;
|
|
2303
|
+
await this.durableCommit(
|
|
2304
|
+
"text",
|
|
2305
|
+
trailPath,
|
|
2306
|
+
trailOf(body),
|
|
2307
|
+
body.turnId,
|
|
2308
|
+
body.seq ?? 0,
|
|
2309
|
+
signal
|
|
2310
|
+
);
|
|
2311
|
+
}
|
|
2246
2312
|
}
|
|
2247
2313
|
// Report a tool transition (start/done/error). The server checks span
|
|
2248
2314
|
// ownership, upserts by (turnId, toolUseId), and publishes agent_activity SSE.
|
|
@@ -2288,6 +2354,18 @@ var CabaneApi = class {
|
|
|
2288
2354
|
);
|
|
2289
2355
|
}
|
|
2290
2356
|
};
|
|
2357
|
+
function isAlreadyPublished(err) {
|
|
2358
|
+
return err instanceof ApiError && err.status === 409 && apiErrorCode(err) === "already_published";
|
|
2359
|
+
}
|
|
2360
|
+
function trailOf(body) {
|
|
2361
|
+
const b = body;
|
|
2362
|
+
return {
|
|
2363
|
+
turnId: b.turnId,
|
|
2364
|
+
text: b.body,
|
|
2365
|
+
...b.seq !== void 0 ? { seq: b.seq } : {},
|
|
2366
|
+
...b.executionSourceId !== void 0 ? { executionSourceId: b.executionSourceId } : {}
|
|
2367
|
+
};
|
|
2368
|
+
}
|
|
2291
2369
|
function isRetryable(err) {
|
|
2292
2370
|
if (err instanceof ApiError) return err.status >= 500;
|
|
2293
2371
|
if (isAbortError(err)) return false;
|
|
@@ -2778,6 +2856,8 @@ var turnOutcomes = ["success", "failure", "cancelled", "skipped"];
|
|
|
2778
2856
|
var turnSessionModes = ["fresh", "resumed", "degraded"];
|
|
2779
2857
|
var turnRuntimeResultKinds = ["success", "error", "no_terminal"];
|
|
2780
2858
|
var turnFinalSources = [
|
|
2859
|
+
// The agent declared its answer with the `answer` turn act (CT1973).
|
|
2860
|
+
"declared",
|
|
2781
2861
|
"runtime_text",
|
|
2782
2862
|
"progress_promotion",
|
|
2783
2863
|
"host_fallback",
|
|
@@ -3063,11 +3143,6 @@ function truncate(s, max) {
|
|
|
3063
3143
|
return `${s.slice(0, max)}\u2026[+${s.length - max} chars]`;
|
|
3064
3144
|
}
|
|
3065
3145
|
|
|
3066
|
-
// packages/agent-runtime/src/claude-code/index.ts
|
|
3067
|
-
import {
|
|
3068
|
-
query as defaultQuery
|
|
3069
|
-
} from "@anthropic-ai/claude-agent-sdk";
|
|
3070
|
-
|
|
3071
3146
|
// packages/agent-runtime/src/turn-control/descriptions.ts
|
|
3072
3147
|
var MAX_ASK_QUESTION_BYTES = 400;
|
|
3073
3148
|
var MAX_ASK_HEADLINE_BYTES = 120;
|
|
@@ -3078,7 +3153,9 @@ var MAX_ASK_OPTIONS = 4;
|
|
|
3078
3153
|
var MAX_WAKE_NOTE_BYTES = 2e3;
|
|
3079
3154
|
var MAX_WITHDRAW_REASON_BYTES = 400;
|
|
3080
3155
|
var MAX_SKIP_REASON_BYTES = 500;
|
|
3156
|
+
var TURN_CONTROL_MCP_SERVER = "cabane_companion";
|
|
3081
3157
|
var TURN_CONTROL_TOOLS = [
|
|
3158
|
+
"answer",
|
|
3082
3159
|
"ask",
|
|
3083
3160
|
"cancel_wake",
|
|
3084
3161
|
"skip_turn",
|
|
@@ -3086,6 +3163,7 @@ var TURN_CONTROL_TOOLS = [
|
|
|
3086
3163
|
"withdraw_ask"
|
|
3087
3164
|
];
|
|
3088
3165
|
var TURN_CONTROL_TOOL_DESCRIPTIONS = {
|
|
3166
|
+
answer: `Call this just before you write your answer: the text you write next is what the conversation keeps as your message. Call it once.`,
|
|
3089
3167
|
ask: `Ask a person a structured question you need answered to continue, then end your turn; don't wait for the reply. \`targetUserId\` is a person on your roster, by their \`@handle\`; to bring in an agent, name them with \`@handle\` in your reply.
|
|
3090
3168
|
- Form: one question (\`headline\`, optional \`question\` body, \`options\`) or \`questions\`, 1\u2013${MAX_ASK_ITEMS} \`{ headline, body?, options? }\`; never both. Decisions that arise together go in ONE call, in the order you want them answered. A second \`ask\` is a separate item, never an edit; raise one only for a genuinely new question.
|
|
3091
3169
|
- \`headline\`: one sentence ending in \`?\`. Body (\`question\` or each item's \`body\`): a sentence or two of framing, not a report; inline Markdown only, no lists or headings. \`options\`: ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} one-click answers (always for a yes/no go-ahead), each a distinct course of action you can carry out without asking back.
|
|
@@ -3100,6 +3178,7 @@ var TURN_CONTROL_TOOL_DESCRIPTIONS = {
|
|
|
3100
3178
|
withdraw_ask: `Withdraw an open ask \u2014 yours or anyone's in this conversation \u2014 because the question is moot, was wrong, or has been overtaken. \`reason\` is required and is shown to the person and, if it wasn't your ask, to the agent who asked it. If there is a current question to put in its place, don't call this \u2014 call \`ask\` with \`replaces\` so the two land as one and the person never sees neither or both. Never withdraw to hurry an answer; to act on a deadline, arm a \`wake_me\` and decide then.`
|
|
3101
3179
|
};
|
|
3102
3180
|
var TURN_CONTROL_FIELD_DESCRIPTIONS = {
|
|
3181
|
+
answer: {},
|
|
3103
3182
|
ask: {
|
|
3104
3183
|
targetUserId: "The person to ask: their `@handle` from your turn context's roster (preferred), or their user id.",
|
|
3105
3184
|
question: `SINGLE-question form: a short body \u2014 one or two sentences of framing the headline can't hold. NOT a report (up to ${MAX_ASK_QUESTION_BYTES} characters, inline markdown only). Provide EITHER this or \`questions\`, not both. Put the crisp one-sentence question in \`headline\`.`,
|
|
@@ -3128,8 +3207,14 @@ var TURN_CONTROL_FIELD_DESCRIPTIONS = {
|
|
|
3128
3207
|
}
|
|
3129
3208
|
};
|
|
3130
3209
|
|
|
3210
|
+
// packages/agent-runtime/src/claude-code/index.ts
|
|
3211
|
+
import {
|
|
3212
|
+
query as defaultQuery
|
|
3213
|
+
} from "@anthropic-ai/claude-agent-sdk";
|
|
3214
|
+
|
|
3131
3215
|
// packages/agent-runtime/src/turn-control/prompt-guidance.ts
|
|
3132
3216
|
var MEANINGS = {
|
|
3217
|
+
answer: "declare the text you write next as your answer",
|
|
3133
3218
|
ask: "put a structured question to a person",
|
|
3134
3219
|
cancel_wake: "cancel your own armed wake in this conversation",
|
|
3135
3220
|
skip_turn: "end without posting a reply",
|
|
@@ -4527,6 +4612,15 @@ function deepestError(node) {
|
|
|
4527
4612
|
}
|
|
4528
4613
|
|
|
4529
4614
|
// packages/agent-runtime/src/opencode/loop.ts
|
|
4615
|
+
function opencodeMcpServerOf(tool2, servers) {
|
|
4616
|
+
let best = null;
|
|
4617
|
+
for (const server of servers ?? []) {
|
|
4618
|
+
if (tool2.length > server.length + 1 && tool2.startsWith(`${server}_`)) {
|
|
4619
|
+
if (best === null || server.length > best.length) best = server;
|
|
4620
|
+
}
|
|
4621
|
+
}
|
|
4622
|
+
return best;
|
|
4623
|
+
}
|
|
4530
4624
|
async function* decodeOpencodeStream(events, ctx) {
|
|
4531
4625
|
let held = null;
|
|
4532
4626
|
const pending = /* @__PURE__ */ new Map();
|
|
@@ -4553,12 +4647,14 @@ async function* decodeOpencodeStream(events, ctx) {
|
|
|
4553
4647
|
if (sealed) yield sealed;
|
|
4554
4648
|
const key = part.callID ?? part.id;
|
|
4555
4649
|
const input = part.state.input;
|
|
4650
|
+
const server = opencodeMcpServerOf(part.tool, ctx.mcpServers);
|
|
4651
|
+
const origin = server ? { mcpServer: server } : {};
|
|
4556
4652
|
if (!startedTools.has(key)) {
|
|
4557
4653
|
const name = prettyToolName(part.tool);
|
|
4558
4654
|
const summary = summarizeToolInput(input);
|
|
4559
4655
|
pending.set(key, { name, summary, input });
|
|
4560
4656
|
startedTools.add(key);
|
|
4561
|
-
yield { type: "tool", id: key, name, phase: "start", summary, input };
|
|
4657
|
+
yield { type: "tool", id: key, name, phase: "start", summary, input, ...origin };
|
|
4562
4658
|
}
|
|
4563
4659
|
const entry = pending.get(key);
|
|
4564
4660
|
if (entry && hasToolInput(input)) {
|
|
@@ -4578,7 +4674,8 @@ async function* decodeOpencodeStream(events, ctx) {
|
|
|
4578
4674
|
phase,
|
|
4579
4675
|
summary: started?.summary ?? "",
|
|
4580
4676
|
input: started?.input ?? input,
|
|
4581
|
-
result
|
|
4677
|
+
result,
|
|
4678
|
+
...origin
|
|
4582
4679
|
};
|
|
4583
4680
|
}
|
|
4584
4681
|
} else {
|
|
@@ -4662,7 +4759,6 @@ function parseOpencodeModel(model) {
|
|
|
4662
4759
|
|
|
4663
4760
|
// packages/agent-runtime/src/opencode/run-spec.ts
|
|
4664
4761
|
var CABANE_MCP_SERVER2 = "cabane";
|
|
4665
|
-
var TURN_CONTROL_MCP_SERVER = "cabane_companion";
|
|
4666
4762
|
var ACTIVE_CONVERSATION_HEADER2 = "x-cabane-active-conversation";
|
|
4667
4763
|
function buildRunSpec(req, resumeSessionId) {
|
|
4668
4764
|
const { config } = req;
|
|
@@ -5056,7 +5152,10 @@ function createOpencodeAdapter(deps = {}) {
|
|
|
5056
5152
|
}
|
|
5057
5153
|
yield* decodeOpencodeStream(result.events, {
|
|
5058
5154
|
signal,
|
|
5059
|
-
sessionId: result.sessionId
|
|
5155
|
+
sessionId: result.sessionId,
|
|
5156
|
+
// The keys the turn's servers were installed under: what the decoder
|
|
5157
|
+
// splits a raw `<server>_<tool>` id on.
|
|
5158
|
+
mcpServers: Object.keys(spec.mcp)
|
|
5060
5159
|
});
|
|
5061
5160
|
}
|
|
5062
5161
|
};
|
|
@@ -5192,7 +5291,15 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
5192
5291
|
expected: [
|
|
5193
5292
|
sessionEvent2(NEW_SESSION_ID),
|
|
5194
5293
|
// start: fires off the first (empty) snapshot — empty input + summary, by design.
|
|
5195
|
-
{
|
|
5294
|
+
{
|
|
5295
|
+
type: "tool",
|
|
5296
|
+
id: "call_1",
|
|
5297
|
+
name: "cabane_list",
|
|
5298
|
+
phase: "start",
|
|
5299
|
+
summary: "",
|
|
5300
|
+
input: {},
|
|
5301
|
+
mcpServer: "cabane"
|
|
5302
|
+
},
|
|
5196
5303
|
// done: carries the full args captured from the later snapshot, and the
|
|
5197
5304
|
// summary recomputed from them — NOT the first-seen `{}`.
|
|
5198
5305
|
{
|
|
@@ -5202,11 +5309,63 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
5202
5309
|
phase: "done",
|
|
5203
5310
|
summary: "/product/tasks",
|
|
5204
5311
|
input: { path: "/product/tasks" },
|
|
5205
|
-
result: "CT301.md"
|
|
5312
|
+
result: "CT301.md",
|
|
5313
|
+
mcpServer: "cabane"
|
|
5206
5314
|
},
|
|
5207
5315
|
{ type: "result", ok: true }
|
|
5208
5316
|
]
|
|
5209
5317
|
},
|
|
5318
|
+
{
|
|
5319
|
+
// The turn-control server's `answer` (CT1973). opencode names an MCP tool `<server>_<tool>`,
|
|
5320
|
+
// so the installed keys split the raw id: `cabane_companion_answer` is the turn-control
|
|
5321
|
+
// server's, never the workspace server's `companion_answer`. The pump binds on that.
|
|
5322
|
+
name: "turn-control answer is read as the turn-control server\u2019s, not cabane\u2019s",
|
|
5323
|
+
request: makeRequest2({
|
|
5324
|
+
cabane: {
|
|
5325
|
+
mcpUrl: "https://cabane.test/api/mcp",
|
|
5326
|
+
turnControlUrl: "https://cabane.test/api/turn-control",
|
|
5327
|
+
bearer: "tok",
|
|
5328
|
+
activeConversationId: "conv"
|
|
5329
|
+
}
|
|
5330
|
+
}),
|
|
5331
|
+
nativeStream: [
|
|
5332
|
+
textPart("p1", "On it."),
|
|
5333
|
+
toolPart("t1", "cabane_companion_answer", { status: "running", input: {} }, "call_1"),
|
|
5334
|
+
toolPart(
|
|
5335
|
+
"t1",
|
|
5336
|
+
"cabane_companion_answer",
|
|
5337
|
+
{ status: "completed", input: {}, output: '{"ok":true,"kind":"answer"}' },
|
|
5338
|
+
"call_1"
|
|
5339
|
+
),
|
|
5340
|
+
textPart("p2", "The answer."),
|
|
5341
|
+
idle()
|
|
5342
|
+
],
|
|
5343
|
+
expected: [
|
|
5344
|
+
sessionEvent2(NEW_SESSION_ID),
|
|
5345
|
+
{ type: "text", body: "On it.", terminal: false },
|
|
5346
|
+
{
|
|
5347
|
+
type: "tool",
|
|
5348
|
+
id: "call_1",
|
|
5349
|
+
name: "cabane_companion_answer",
|
|
5350
|
+
phase: "start",
|
|
5351
|
+
summary: "",
|
|
5352
|
+
input: {},
|
|
5353
|
+
mcpServer: "cabane_companion"
|
|
5354
|
+
},
|
|
5355
|
+
{
|
|
5356
|
+
type: "tool",
|
|
5357
|
+
id: "call_1",
|
|
5358
|
+
name: "cabane_companion_answer",
|
|
5359
|
+
phase: "done",
|
|
5360
|
+
summary: "",
|
|
5361
|
+
input: {},
|
|
5362
|
+
result: '{"ok":true,"kind":"answer"}',
|
|
5363
|
+
mcpServer: "cabane_companion"
|
|
5364
|
+
},
|
|
5365
|
+
{ type: "text", body: "The answer.", terminal: true },
|
|
5366
|
+
{ type: "result", ok: true }
|
|
5367
|
+
]
|
|
5368
|
+
},
|
|
5210
5369
|
{
|
|
5211
5370
|
// Highest risk: cancel mid-stream. The abort lands after the tool result; the adapter stops before
|
|
5212
5371
|
// the closing reply — no final text, no `result` event (the server writes the stopped marker).
|
|
@@ -5561,6 +5720,7 @@ function readToolItem(item, eventType) {
|
|
|
5561
5720
|
name: server && server !== "cabane" ? `${server}.${tool2}` : tool2,
|
|
5562
5721
|
input: item.arguments,
|
|
5563
5722
|
status: toStatus(item.status),
|
|
5723
|
+
...server ? { server } : {},
|
|
5564
5724
|
...item.status === "completed" ? { result } : item.status === "failed" ? { result: hasMcpReceipt(result) ? result : readMcpError(item.error) } : {}
|
|
5565
5725
|
};
|
|
5566
5726
|
}
|
|
@@ -5789,11 +5949,20 @@ async function* decodeCodexStream(events, ctx) {
|
|
|
5789
5949
|
if (!tool2) continue;
|
|
5790
5950
|
yield* flushInterim();
|
|
5791
5951
|
const name = prettyToolName(tool2.name);
|
|
5952
|
+
const origin = tool2.server ? { mcpServer: tool2.server } : {};
|
|
5792
5953
|
if (!startedTools.has(tool2.id)) {
|
|
5793
5954
|
const summary = summarizeToolInput(tool2.input);
|
|
5794
5955
|
pending.set(tool2.id, { name, summary, input: tool2.input });
|
|
5795
5956
|
startedTools.add(tool2.id);
|
|
5796
|
-
yield {
|
|
5957
|
+
yield {
|
|
5958
|
+
type: "tool",
|
|
5959
|
+
id: tool2.id,
|
|
5960
|
+
name,
|
|
5961
|
+
phase: "start",
|
|
5962
|
+
summary,
|
|
5963
|
+
input: tool2.input,
|
|
5964
|
+
...origin
|
|
5965
|
+
};
|
|
5797
5966
|
}
|
|
5798
5967
|
const entry = pending.get(tool2.id);
|
|
5799
5968
|
if (entry && hasToolInput2(tool2.input)) {
|
|
@@ -5811,7 +5980,8 @@ async function* decodeCodexStream(events, ctx) {
|
|
|
5811
5980
|
phase: tool2.status === "completed" ? "done" : "error",
|
|
5812
5981
|
summary: started?.summary ?? "",
|
|
5813
5982
|
input: started?.input ?? tool2.input,
|
|
5814
|
-
result: tool2.result
|
|
5983
|
+
result: tool2.result,
|
|
5984
|
+
...origin
|
|
5815
5985
|
};
|
|
5816
5986
|
}
|
|
5817
5987
|
continue;
|
|
@@ -5902,7 +6072,6 @@ function parseCodexModel(model) {
|
|
|
5902
6072
|
|
|
5903
6073
|
// packages/agent-runtime/src/codex/run-spec.ts
|
|
5904
6074
|
var CABANE_MCP_SERVER3 = "cabane";
|
|
5905
|
-
var TURN_CONTROL_MCP_SERVER2 = "cabane_companion";
|
|
5906
6075
|
var ACTIVE_CONVERSATION_HEADER3 = "x-cabane-active-conversation";
|
|
5907
6076
|
var ENV_ENVELOPE_KEYS = [
|
|
5908
6077
|
"CABANE_CONVERSATION_ID",
|
|
@@ -6031,7 +6200,7 @@ function buildConfig(req) {
|
|
|
6031
6200
|
default_tools_approval_mode: "approve"
|
|
6032
6201
|
};
|
|
6033
6202
|
if (req.cabane.turnControlUrl) {
|
|
6034
|
-
mcp_servers[
|
|
6203
|
+
mcp_servers[TURN_CONTROL_MCP_SERVER] = {
|
|
6035
6204
|
url: req.cabane.turnControlUrl,
|
|
6036
6205
|
http_headers: {
|
|
6037
6206
|
Authorization: `Bearer ${req.cabane.bearer}`,
|
|
@@ -6481,6 +6650,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6481
6650
|
type: "tool",
|
|
6482
6651
|
id: "t1",
|
|
6483
6652
|
name: "read",
|
|
6653
|
+
mcpServer: "cabane",
|
|
6484
6654
|
phase: "start",
|
|
6485
6655
|
summary: "sleep.md",
|
|
6486
6656
|
input: { path: "sleep.md" }
|
|
@@ -6489,6 +6659,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6489
6659
|
type: "tool",
|
|
6490
6660
|
id: "t1",
|
|
6491
6661
|
name: "read",
|
|
6662
|
+
mcpServer: "cabane",
|
|
6492
6663
|
phase: "done",
|
|
6493
6664
|
summary: "sleep.md",
|
|
6494
6665
|
input: { path: "sleep.md" },
|
|
@@ -6534,6 +6705,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6534
6705
|
type: "tool",
|
|
6535
6706
|
id: "query-success",
|
|
6536
6707
|
name: "query",
|
|
6708
|
+
mcpServer: "cabane",
|
|
6537
6709
|
phase: "start",
|
|
6538
6710
|
summary: "",
|
|
6539
6711
|
input: { op: "files.read", args: { path: "/ABOUT.md" } }
|
|
@@ -6542,6 +6714,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6542
6714
|
type: "tool",
|
|
6543
6715
|
id: "query-success",
|
|
6544
6716
|
name: "query",
|
|
6717
|
+
mcpServer: "cabane",
|
|
6545
6718
|
phase: "done",
|
|
6546
6719
|
summary: "",
|
|
6547
6720
|
input: { op: "files.read", args: { path: "/ABOUT.md" } },
|
|
@@ -6588,6 +6761,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6588
6761
|
type: "tool",
|
|
6589
6762
|
id: "query-failed",
|
|
6590
6763
|
name: "query",
|
|
6764
|
+
mcpServer: "cabane",
|
|
6591
6765
|
phase: "start",
|
|
6592
6766
|
summary: "",
|
|
6593
6767
|
input: { op: "files.read", args: { path: "/ABOUT.md" } }
|
|
@@ -6596,6 +6770,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6596
6770
|
type: "tool",
|
|
6597
6771
|
id: "query-failed",
|
|
6598
6772
|
name: "query",
|
|
6773
|
+
mcpServer: "cabane",
|
|
6599
6774
|
phase: "error",
|
|
6600
6775
|
summary: "",
|
|
6601
6776
|
input: { op: "files.read", args: { path: "/ABOUT.md" } },
|
|
@@ -6605,6 +6780,54 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6605
6780
|
{ type: "result", ok: true }
|
|
6606
6781
|
]
|
|
6607
6782
|
},
|
|
6783
|
+
{
|
|
6784
|
+
// The turn-control server's `answer` (CT1973): the tool event carries the server Codex names
|
|
6785
|
+
// beside the tool, which is what the pump binds the declaration to. A non-cabane server keeps
|
|
6786
|
+
// its prefix on the card as before.
|
|
6787
|
+
name: "turn-control answer carries its server",
|
|
6788
|
+
request: makeRequest3(),
|
|
6789
|
+
nativeStream: [
|
|
6790
|
+
threadStarted(NEW_THREAD_ID),
|
|
6791
|
+
agentMessage("m1", "On it."),
|
|
6792
|
+
toolFrame("item.completed", {
|
|
6793
|
+
id: "answer-1",
|
|
6794
|
+
type: "mcp_tool_call",
|
|
6795
|
+
server: "cabane_companion",
|
|
6796
|
+
tool: "answer",
|
|
6797
|
+
arguments: {},
|
|
6798
|
+
status: "completed",
|
|
6799
|
+
result: { content: [{ type: "text", text: '{"ok":true,"kind":"answer"}' }] }
|
|
6800
|
+
}),
|
|
6801
|
+
agentMessage("m2", "The answer."),
|
|
6802
|
+
turnCompleted()
|
|
6803
|
+
],
|
|
6804
|
+
expected: [
|
|
6805
|
+
startedSession(NEW_THREAD_ID),
|
|
6806
|
+
{ type: "text", body: "On it.", terminal: false },
|
|
6807
|
+
{
|
|
6808
|
+
type: "tool",
|
|
6809
|
+
id: "answer-1",
|
|
6810
|
+
name: "cabane_companion.answer",
|
|
6811
|
+
mcpServer: "cabane_companion",
|
|
6812
|
+
phase: "start",
|
|
6813
|
+
summary: "",
|
|
6814
|
+
input: {}
|
|
6815
|
+
},
|
|
6816
|
+
{
|
|
6817
|
+
type: "tool",
|
|
6818
|
+
id: "answer-1",
|
|
6819
|
+
name: "cabane_companion.answer",
|
|
6820
|
+
mcpServer: "cabane_companion",
|
|
6821
|
+
phase: "done",
|
|
6822
|
+
summary: "",
|
|
6823
|
+
input: {},
|
|
6824
|
+
result: [{ type: "text", text: '{"ok":true,"kind":"answer"}' }]
|
|
6825
|
+
},
|
|
6826
|
+
{ type: "text", body: "The answer.", terminal: true },
|
|
6827
|
+
closingSession(NEW_THREAD_ID),
|
|
6828
|
+
{ type: "result", ok: true }
|
|
6829
|
+
]
|
|
6830
|
+
},
|
|
6608
6831
|
{
|
|
6609
6832
|
// A transport/tool failure with no MCP result payload still uses the real adapter error message,
|
|
6610
6833
|
// never `undefined`.
|
|
@@ -6630,6 +6853,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6630
6853
|
type: "tool",
|
|
6631
6854
|
id: "mcp-failed",
|
|
6632
6855
|
name: "other.lookup",
|
|
6856
|
+
mcpServer: "other",
|
|
6633
6857
|
phase: "start",
|
|
6634
6858
|
summary: "receipt",
|
|
6635
6859
|
input: { q: "receipt" }
|
|
@@ -6638,6 +6862,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6638
6862
|
type: "tool",
|
|
6639
6863
|
id: "mcp-failed",
|
|
6640
6864
|
name: "other.lookup",
|
|
6865
|
+
mcpServer: "other",
|
|
6641
6866
|
phase: "error",
|
|
6642
6867
|
summary: "receipt",
|
|
6643
6868
|
input: { q: "receipt" },
|
|
@@ -6674,6 +6899,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6674
6899
|
type: "tool",
|
|
6675
6900
|
id: "mcp-image",
|
|
6676
6901
|
name: "other.render",
|
|
6902
|
+
mcpServer: "other",
|
|
6677
6903
|
phase: "start",
|
|
6678
6904
|
summary: "",
|
|
6679
6905
|
input: {}
|
|
@@ -6682,6 +6908,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6682
6908
|
type: "tool",
|
|
6683
6909
|
id: "mcp-image",
|
|
6684
6910
|
name: "other.render",
|
|
6911
|
+
mcpServer: "other",
|
|
6685
6912
|
phase: "done",
|
|
6686
6913
|
summary: "",
|
|
6687
6914
|
input: {},
|
|
@@ -6729,11 +6956,20 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6729
6956
|
],
|
|
6730
6957
|
expected: [
|
|
6731
6958
|
startedSession(NEW_THREAD_ID),
|
|
6732
|
-
{ type: "tool", id: "t1", name: "list", phase: "start", summary: "", input: {} },
|
|
6733
6959
|
{
|
|
6734
6960
|
type: "tool",
|
|
6735
6961
|
id: "t1",
|
|
6736
6962
|
name: "list",
|
|
6963
|
+
mcpServer: "cabane",
|
|
6964
|
+
phase: "start",
|
|
6965
|
+
summary: "",
|
|
6966
|
+
input: {}
|
|
6967
|
+
},
|
|
6968
|
+
{
|
|
6969
|
+
type: "tool",
|
|
6970
|
+
id: "t1",
|
|
6971
|
+
name: "list",
|
|
6972
|
+
mcpServer: "cabane",
|
|
6737
6973
|
phase: "done",
|
|
6738
6974
|
summary: "/product/tasks",
|
|
6739
6975
|
input: { path: "/product/tasks" },
|
|
@@ -8012,7 +8248,7 @@ import { join as join14 } from "path";
|
|
|
8012
8248
|
// src/turn-control-tools.ts
|
|
8013
8249
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
8014
8250
|
import { z as z16 } from "zod";
|
|
8015
|
-
var COMPANION_LOCAL_MCP_SERVER =
|
|
8251
|
+
var COMPANION_LOCAL_MCP_SERVER = TURN_CONTROL_MCP_SERVER;
|
|
8016
8252
|
var COMPANION_LOCAL_TOOL_GLOB = `mcp__${COMPANION_LOCAL_MCP_SERVER}__*`;
|
|
8017
8253
|
var ASK_TOOL = "ask";
|
|
8018
8254
|
var ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${ASK_TOOL}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.203",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The Cabane Companion (headless): connect an agent on your machine to your Cabane workspace as a responder — drive work against your own codebase, files, and MCP servers without putting any of it in Cabane.",
|
|
6
6
|
"license": "UNLICENSED",
|