@cabane/companion 0.6.201 → 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 +442 -118
- package/dist/runtime.js +273 -30
- package/package.json +1 -1
package/dist/runtime.js
CHANGED
|
@@ -1802,6 +1802,11 @@ function isTurnEndedRefusal(err) {
|
|
|
1802
1802
|
const body = err.body;
|
|
1803
1803
|
return typeof body === "object" && body !== null && body.reason === "turn_ended";
|
|
1804
1804
|
}
|
|
1805
|
+
function turnEndedOutcome(err) {
|
|
1806
|
+
if (!isTurnEndedRefusal(err)) return void 0;
|
|
1807
|
+
const outcome = err.body.outcome;
|
|
1808
|
+
return typeof outcome === "string" ? outcome : void 0;
|
|
1809
|
+
}
|
|
1805
1810
|
var ERROR_BODY_LOG_CAP = 2e3;
|
|
1806
1811
|
function describeErrorBody(body) {
|
|
1807
1812
|
if (body === void 0 || body === null) return void 0;
|
|
@@ -1945,7 +1950,7 @@ var CabaneApi = class {
|
|
|
1945
1950
|
// text use (turnId, seq) within each kind for nonzero seq; tool activity
|
|
1946
1951
|
// uses (turnId, toolUseId). Legacy zero/missing seq bypasses row deduplication.
|
|
1947
1952
|
// Server fences and the outbox's discard/eviction bounds still apply.
|
|
1948
|
-
async durableCommit(kind, path, body, turnId, seq, signal) {
|
|
1953
|
+
async durableCommit(kind, path, body, turnId, seq, signal, trailPath) {
|
|
1949
1954
|
try {
|
|
1950
1955
|
await this.request("POST", path, body, {
|
|
1951
1956
|
retry: true,
|
|
@@ -1956,7 +1961,16 @@ var CabaneApi = class {
|
|
|
1956
1961
|
if (!outbox) throw err;
|
|
1957
1962
|
if (signal?.aborted || isAbortError(err)) throw err;
|
|
1958
1963
|
if (!isRetryable(err)) throw err;
|
|
1959
|
-
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
|
+
});
|
|
1960
1974
|
this.opts.log?.debug(
|
|
1961
1975
|
{ kind, turnId, seq, err: err instanceof Error ? err.message : String(err) },
|
|
1962
1976
|
"companion: commit queued to outbox after transient failure (will drain when the API returns)"
|
|
@@ -1981,6 +1995,29 @@ var CabaneApi = class {
|
|
|
1981
1995
|
progressed = true;
|
|
1982
1996
|
} catch (err) {
|
|
1983
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
|
+
}
|
|
1984
2021
|
if (entry.kind === "active-run" && isTurnEndedRefusal(err)) {
|
|
1985
2022
|
this.opts.log?.debug(
|
|
1986
2023
|
{ kind: entry.kind, turnId: entry.turnId, ...apiErrorLogFields(err) },
|
|
@@ -2056,6 +2093,19 @@ var CabaneApi = class {
|
|
|
2056
2093
|
(e) => e.turnId === turnId && e.kind === "message" && e.body?.kind !== "runtime_notice"
|
|
2057
2094
|
);
|
|
2058
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
|
+
}
|
|
2059
2109
|
// ADOPT a turn context's issuance as the execution source of its work, once,
|
|
2060
2110
|
// before preparation emits or queues anything, so every row can name a source
|
|
2061
2111
|
// the server durably recorded and will never reap. Adoption is idempotent, so
|
|
@@ -2133,9 +2183,11 @@ var CabaneApi = class {
|
|
|
2133
2183
|
signal: AbortSignal.timeout(this.opts.leaseCheckTimeoutMs ?? LEASE_CHECK_TIMEOUT_MS)
|
|
2134
2184
|
}
|
|
2135
2185
|
);
|
|
2136
|
-
|
|
2186
|
+
if (res?.state === "ended") return res.outcome === "stopped" ? "stopped" : "ended";
|
|
2187
|
+
return res?.state === "running" ? "running" : "unknown";
|
|
2137
2188
|
} catch (err) {
|
|
2138
|
-
|
|
2189
|
+
if (!isTurnEndedRefusal(err)) return "unknown";
|
|
2190
|
+
return turnEndedOutcome(err) === "stopped" ? "stopped" : "ended";
|
|
2139
2191
|
}
|
|
2140
2192
|
}
|
|
2141
2193
|
// Flips `active_run_started_at` and optionally captures the SDK session
|
|
@@ -2227,15 +2279,36 @@ var CabaneApi = class {
|
|
|
2227
2279
|
// seq also orders speech against the turn's tool/thinking/text trail.
|
|
2228
2280
|
// Transport/5xx failures get bounded retries and optional outbox delivery;
|
|
2229
2281
|
// terminal 4xx and cancellation stop delivery rather than proving no commit.
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
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
|
+
}
|
|
2239
2312
|
}
|
|
2240
2313
|
// Report a tool transition (start/done/error). The server checks span
|
|
2241
2314
|
// ownership, upserts by (turnId, toolUseId), and publishes agent_activity SSE.
|
|
@@ -2281,6 +2354,18 @@ var CabaneApi = class {
|
|
|
2281
2354
|
);
|
|
2282
2355
|
}
|
|
2283
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
|
+
}
|
|
2284
2369
|
function isRetryable(err) {
|
|
2285
2370
|
if (err instanceof ApiError) return err.status >= 500;
|
|
2286
2371
|
if (isAbortError(err)) return false;
|
|
@@ -2771,6 +2856,8 @@ var turnOutcomes = ["success", "failure", "cancelled", "skipped"];
|
|
|
2771
2856
|
var turnSessionModes = ["fresh", "resumed", "degraded"];
|
|
2772
2857
|
var turnRuntimeResultKinds = ["success", "error", "no_terminal"];
|
|
2773
2858
|
var turnFinalSources = [
|
|
2859
|
+
// The agent declared its answer with the `answer` turn act (CT1973).
|
|
2860
|
+
"declared",
|
|
2774
2861
|
"runtime_text",
|
|
2775
2862
|
"progress_promotion",
|
|
2776
2863
|
"host_fallback",
|
|
@@ -3056,11 +3143,6 @@ function truncate(s, max) {
|
|
|
3056
3143
|
return `${s.slice(0, max)}\u2026[+${s.length - max} chars]`;
|
|
3057
3144
|
}
|
|
3058
3145
|
|
|
3059
|
-
// packages/agent-runtime/src/claude-code/index.ts
|
|
3060
|
-
import {
|
|
3061
|
-
query as defaultQuery
|
|
3062
|
-
} from "@anthropic-ai/claude-agent-sdk";
|
|
3063
|
-
|
|
3064
3146
|
// packages/agent-runtime/src/turn-control/descriptions.ts
|
|
3065
3147
|
var MAX_ASK_QUESTION_BYTES = 400;
|
|
3066
3148
|
var MAX_ASK_HEADLINE_BYTES = 120;
|
|
@@ -3071,7 +3153,9 @@ var MAX_ASK_OPTIONS = 4;
|
|
|
3071
3153
|
var MAX_WAKE_NOTE_BYTES = 2e3;
|
|
3072
3154
|
var MAX_WITHDRAW_REASON_BYTES = 400;
|
|
3073
3155
|
var MAX_SKIP_REASON_BYTES = 500;
|
|
3156
|
+
var TURN_CONTROL_MCP_SERVER = "cabane_companion";
|
|
3074
3157
|
var TURN_CONTROL_TOOLS = [
|
|
3158
|
+
"answer",
|
|
3075
3159
|
"ask",
|
|
3076
3160
|
"cancel_wake",
|
|
3077
3161
|
"skip_turn",
|
|
@@ -3079,6 +3163,7 @@ var TURN_CONTROL_TOOLS = [
|
|
|
3079
3163
|
"withdraw_ask"
|
|
3080
3164
|
];
|
|
3081
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.`,
|
|
3082
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.
|
|
3083
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.
|
|
3084
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.
|
|
@@ -3093,6 +3178,7 @@ var TURN_CONTROL_TOOL_DESCRIPTIONS = {
|
|
|
3093
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.`
|
|
3094
3179
|
};
|
|
3095
3180
|
var TURN_CONTROL_FIELD_DESCRIPTIONS = {
|
|
3181
|
+
answer: {},
|
|
3096
3182
|
ask: {
|
|
3097
3183
|
targetUserId: "The person to ask: their `@handle` from your turn context's roster (preferred), or their user id.",
|
|
3098
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\`.`,
|
|
@@ -3121,8 +3207,14 @@ var TURN_CONTROL_FIELD_DESCRIPTIONS = {
|
|
|
3121
3207
|
}
|
|
3122
3208
|
};
|
|
3123
3209
|
|
|
3210
|
+
// packages/agent-runtime/src/claude-code/index.ts
|
|
3211
|
+
import {
|
|
3212
|
+
query as defaultQuery
|
|
3213
|
+
} from "@anthropic-ai/claude-agent-sdk";
|
|
3214
|
+
|
|
3124
3215
|
// packages/agent-runtime/src/turn-control/prompt-guidance.ts
|
|
3125
3216
|
var MEANINGS = {
|
|
3217
|
+
answer: "declare the text you write next as your answer",
|
|
3126
3218
|
ask: "put a structured question to a person",
|
|
3127
3219
|
cancel_wake: "cancel your own armed wake in this conversation",
|
|
3128
3220
|
skip_turn: "end without posting a reply",
|
|
@@ -4520,6 +4612,15 @@ function deepestError(node) {
|
|
|
4520
4612
|
}
|
|
4521
4613
|
|
|
4522
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
|
+
}
|
|
4523
4624
|
async function* decodeOpencodeStream(events, ctx) {
|
|
4524
4625
|
let held = null;
|
|
4525
4626
|
const pending = /* @__PURE__ */ new Map();
|
|
@@ -4546,12 +4647,14 @@ async function* decodeOpencodeStream(events, ctx) {
|
|
|
4546
4647
|
if (sealed) yield sealed;
|
|
4547
4648
|
const key = part.callID ?? part.id;
|
|
4548
4649
|
const input = part.state.input;
|
|
4650
|
+
const server = opencodeMcpServerOf(part.tool, ctx.mcpServers);
|
|
4651
|
+
const origin = server ? { mcpServer: server } : {};
|
|
4549
4652
|
if (!startedTools.has(key)) {
|
|
4550
4653
|
const name = prettyToolName(part.tool);
|
|
4551
4654
|
const summary = summarizeToolInput(input);
|
|
4552
4655
|
pending.set(key, { name, summary, input });
|
|
4553
4656
|
startedTools.add(key);
|
|
4554
|
-
yield { type: "tool", id: key, name, phase: "start", summary, input };
|
|
4657
|
+
yield { type: "tool", id: key, name, phase: "start", summary, input, ...origin };
|
|
4555
4658
|
}
|
|
4556
4659
|
const entry = pending.get(key);
|
|
4557
4660
|
if (entry && hasToolInput(input)) {
|
|
@@ -4571,7 +4674,8 @@ async function* decodeOpencodeStream(events, ctx) {
|
|
|
4571
4674
|
phase,
|
|
4572
4675
|
summary: started?.summary ?? "",
|
|
4573
4676
|
input: started?.input ?? input,
|
|
4574
|
-
result
|
|
4677
|
+
result,
|
|
4678
|
+
...origin
|
|
4575
4679
|
};
|
|
4576
4680
|
}
|
|
4577
4681
|
} else {
|
|
@@ -4655,7 +4759,6 @@ function parseOpencodeModel(model) {
|
|
|
4655
4759
|
|
|
4656
4760
|
// packages/agent-runtime/src/opencode/run-spec.ts
|
|
4657
4761
|
var CABANE_MCP_SERVER2 = "cabane";
|
|
4658
|
-
var TURN_CONTROL_MCP_SERVER = "cabane_companion";
|
|
4659
4762
|
var ACTIVE_CONVERSATION_HEADER2 = "x-cabane-active-conversation";
|
|
4660
4763
|
function buildRunSpec(req, resumeSessionId) {
|
|
4661
4764
|
const { config } = req;
|
|
@@ -5049,7 +5152,10 @@ function createOpencodeAdapter(deps = {}) {
|
|
|
5049
5152
|
}
|
|
5050
5153
|
yield* decodeOpencodeStream(result.events, {
|
|
5051
5154
|
signal,
|
|
5052
|
-
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)
|
|
5053
5159
|
});
|
|
5054
5160
|
}
|
|
5055
5161
|
};
|
|
@@ -5185,7 +5291,15 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
5185
5291
|
expected: [
|
|
5186
5292
|
sessionEvent2(NEW_SESSION_ID),
|
|
5187
5293
|
// start: fires off the first (empty) snapshot — empty input + summary, by design.
|
|
5188
|
-
{
|
|
5294
|
+
{
|
|
5295
|
+
type: "tool",
|
|
5296
|
+
id: "call_1",
|
|
5297
|
+
name: "cabane_list",
|
|
5298
|
+
phase: "start",
|
|
5299
|
+
summary: "",
|
|
5300
|
+
input: {},
|
|
5301
|
+
mcpServer: "cabane"
|
|
5302
|
+
},
|
|
5189
5303
|
// done: carries the full args captured from the later snapshot, and the
|
|
5190
5304
|
// summary recomputed from them — NOT the first-seen `{}`.
|
|
5191
5305
|
{
|
|
@@ -5195,8 +5309,60 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
5195
5309
|
phase: "done",
|
|
5196
5310
|
summary: "/product/tasks",
|
|
5197
5311
|
input: { path: "/product/tasks" },
|
|
5198
|
-
result: "CT301.md"
|
|
5312
|
+
result: "CT301.md",
|
|
5313
|
+
mcpServer: "cabane"
|
|
5314
|
+
},
|
|
5315
|
+
{ type: "result", ok: true }
|
|
5316
|
+
]
|
|
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"
|
|
5199
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 },
|
|
5200
5366
|
{ type: "result", ok: true }
|
|
5201
5367
|
]
|
|
5202
5368
|
},
|
|
@@ -5554,6 +5720,7 @@ function readToolItem(item, eventType) {
|
|
|
5554
5720
|
name: server && server !== "cabane" ? `${server}.${tool2}` : tool2,
|
|
5555
5721
|
input: item.arguments,
|
|
5556
5722
|
status: toStatus(item.status),
|
|
5723
|
+
...server ? { server } : {},
|
|
5557
5724
|
...item.status === "completed" ? { result } : item.status === "failed" ? { result: hasMcpReceipt(result) ? result : readMcpError(item.error) } : {}
|
|
5558
5725
|
};
|
|
5559
5726
|
}
|
|
@@ -5782,11 +5949,20 @@ async function* decodeCodexStream(events, ctx) {
|
|
|
5782
5949
|
if (!tool2) continue;
|
|
5783
5950
|
yield* flushInterim();
|
|
5784
5951
|
const name = prettyToolName(tool2.name);
|
|
5952
|
+
const origin = tool2.server ? { mcpServer: tool2.server } : {};
|
|
5785
5953
|
if (!startedTools.has(tool2.id)) {
|
|
5786
5954
|
const summary = summarizeToolInput(tool2.input);
|
|
5787
5955
|
pending.set(tool2.id, { name, summary, input: tool2.input });
|
|
5788
5956
|
startedTools.add(tool2.id);
|
|
5789
|
-
yield {
|
|
5957
|
+
yield {
|
|
5958
|
+
type: "tool",
|
|
5959
|
+
id: tool2.id,
|
|
5960
|
+
name,
|
|
5961
|
+
phase: "start",
|
|
5962
|
+
summary,
|
|
5963
|
+
input: tool2.input,
|
|
5964
|
+
...origin
|
|
5965
|
+
};
|
|
5790
5966
|
}
|
|
5791
5967
|
const entry = pending.get(tool2.id);
|
|
5792
5968
|
if (entry && hasToolInput2(tool2.input)) {
|
|
@@ -5804,7 +5980,8 @@ async function* decodeCodexStream(events, ctx) {
|
|
|
5804
5980
|
phase: tool2.status === "completed" ? "done" : "error",
|
|
5805
5981
|
summary: started?.summary ?? "",
|
|
5806
5982
|
input: started?.input ?? tool2.input,
|
|
5807
|
-
result: tool2.result
|
|
5983
|
+
result: tool2.result,
|
|
5984
|
+
...origin
|
|
5808
5985
|
};
|
|
5809
5986
|
}
|
|
5810
5987
|
continue;
|
|
@@ -5895,7 +6072,6 @@ function parseCodexModel(model) {
|
|
|
5895
6072
|
|
|
5896
6073
|
// packages/agent-runtime/src/codex/run-spec.ts
|
|
5897
6074
|
var CABANE_MCP_SERVER3 = "cabane";
|
|
5898
|
-
var TURN_CONTROL_MCP_SERVER2 = "cabane_companion";
|
|
5899
6075
|
var ACTIVE_CONVERSATION_HEADER3 = "x-cabane-active-conversation";
|
|
5900
6076
|
var ENV_ENVELOPE_KEYS = [
|
|
5901
6077
|
"CABANE_CONVERSATION_ID",
|
|
@@ -6024,7 +6200,7 @@ function buildConfig(req) {
|
|
|
6024
6200
|
default_tools_approval_mode: "approve"
|
|
6025
6201
|
};
|
|
6026
6202
|
if (req.cabane.turnControlUrl) {
|
|
6027
|
-
mcp_servers[
|
|
6203
|
+
mcp_servers[TURN_CONTROL_MCP_SERVER] = {
|
|
6028
6204
|
url: req.cabane.turnControlUrl,
|
|
6029
6205
|
http_headers: {
|
|
6030
6206
|
Authorization: `Bearer ${req.cabane.bearer}`,
|
|
@@ -6474,6 +6650,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6474
6650
|
type: "tool",
|
|
6475
6651
|
id: "t1",
|
|
6476
6652
|
name: "read",
|
|
6653
|
+
mcpServer: "cabane",
|
|
6477
6654
|
phase: "start",
|
|
6478
6655
|
summary: "sleep.md",
|
|
6479
6656
|
input: { path: "sleep.md" }
|
|
@@ -6482,6 +6659,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6482
6659
|
type: "tool",
|
|
6483
6660
|
id: "t1",
|
|
6484
6661
|
name: "read",
|
|
6662
|
+
mcpServer: "cabane",
|
|
6485
6663
|
phase: "done",
|
|
6486
6664
|
summary: "sleep.md",
|
|
6487
6665
|
input: { path: "sleep.md" },
|
|
@@ -6527,6 +6705,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6527
6705
|
type: "tool",
|
|
6528
6706
|
id: "query-success",
|
|
6529
6707
|
name: "query",
|
|
6708
|
+
mcpServer: "cabane",
|
|
6530
6709
|
phase: "start",
|
|
6531
6710
|
summary: "",
|
|
6532
6711
|
input: { op: "files.read", args: { path: "/ABOUT.md" } }
|
|
@@ -6535,6 +6714,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6535
6714
|
type: "tool",
|
|
6536
6715
|
id: "query-success",
|
|
6537
6716
|
name: "query",
|
|
6717
|
+
mcpServer: "cabane",
|
|
6538
6718
|
phase: "done",
|
|
6539
6719
|
summary: "",
|
|
6540
6720
|
input: { op: "files.read", args: { path: "/ABOUT.md" } },
|
|
@@ -6581,6 +6761,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6581
6761
|
type: "tool",
|
|
6582
6762
|
id: "query-failed",
|
|
6583
6763
|
name: "query",
|
|
6764
|
+
mcpServer: "cabane",
|
|
6584
6765
|
phase: "start",
|
|
6585
6766
|
summary: "",
|
|
6586
6767
|
input: { op: "files.read", args: { path: "/ABOUT.md" } }
|
|
@@ -6589,6 +6770,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6589
6770
|
type: "tool",
|
|
6590
6771
|
id: "query-failed",
|
|
6591
6772
|
name: "query",
|
|
6773
|
+
mcpServer: "cabane",
|
|
6592
6774
|
phase: "error",
|
|
6593
6775
|
summary: "",
|
|
6594
6776
|
input: { op: "files.read", args: { path: "/ABOUT.md" } },
|
|
@@ -6598,6 +6780,54 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6598
6780
|
{ type: "result", ok: true }
|
|
6599
6781
|
]
|
|
6600
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
|
+
},
|
|
6601
6831
|
{
|
|
6602
6832
|
// A transport/tool failure with no MCP result payload still uses the real adapter error message,
|
|
6603
6833
|
// never `undefined`.
|
|
@@ -6623,6 +6853,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6623
6853
|
type: "tool",
|
|
6624
6854
|
id: "mcp-failed",
|
|
6625
6855
|
name: "other.lookup",
|
|
6856
|
+
mcpServer: "other",
|
|
6626
6857
|
phase: "start",
|
|
6627
6858
|
summary: "receipt",
|
|
6628
6859
|
input: { q: "receipt" }
|
|
@@ -6631,6 +6862,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6631
6862
|
type: "tool",
|
|
6632
6863
|
id: "mcp-failed",
|
|
6633
6864
|
name: "other.lookup",
|
|
6865
|
+
mcpServer: "other",
|
|
6634
6866
|
phase: "error",
|
|
6635
6867
|
summary: "receipt",
|
|
6636
6868
|
input: { q: "receipt" },
|
|
@@ -6667,6 +6899,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6667
6899
|
type: "tool",
|
|
6668
6900
|
id: "mcp-image",
|
|
6669
6901
|
name: "other.render",
|
|
6902
|
+
mcpServer: "other",
|
|
6670
6903
|
phase: "start",
|
|
6671
6904
|
summary: "",
|
|
6672
6905
|
input: {}
|
|
@@ -6675,6 +6908,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6675
6908
|
type: "tool",
|
|
6676
6909
|
id: "mcp-image",
|
|
6677
6910
|
name: "other.render",
|
|
6911
|
+
mcpServer: "other",
|
|
6678
6912
|
phase: "done",
|
|
6679
6913
|
summary: "",
|
|
6680
6914
|
input: {},
|
|
@@ -6722,11 +6956,20 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6722
6956
|
],
|
|
6723
6957
|
expected: [
|
|
6724
6958
|
startedSession(NEW_THREAD_ID),
|
|
6725
|
-
{ type: "tool", id: "t1", name: "list", phase: "start", summary: "", input: {} },
|
|
6726
6959
|
{
|
|
6727
6960
|
type: "tool",
|
|
6728
6961
|
id: "t1",
|
|
6729
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",
|
|
6730
6973
|
phase: "done",
|
|
6731
6974
|
summary: "/product/tasks",
|
|
6732
6975
|
input: { path: "/product/tasks" },
|
|
@@ -8005,7 +8248,7 @@ import { join as join14 } from "path";
|
|
|
8005
8248
|
// src/turn-control-tools.ts
|
|
8006
8249
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
8007
8250
|
import { z as z16 } from "zod";
|
|
8008
|
-
var COMPANION_LOCAL_MCP_SERVER =
|
|
8251
|
+
var COMPANION_LOCAL_MCP_SERVER = TURN_CONTROL_MCP_SERVER;
|
|
8009
8252
|
var COMPANION_LOCAL_TOOL_GLOB = `mcp__${COMPANION_LOCAL_MCP_SERVER}__*`;
|
|
8010
8253
|
var ASK_TOOL = "ask";
|
|
8011
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",
|