@cabane/companion 0.6.65 → 0.6.67
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 +31 -152
- package/dist/runtime.js +31 -152
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2287,7 +2287,6 @@ var ACTIVE_RUN_OUTBOX_SEQ = 0;
|
|
|
2287
2287
|
function activeRunOutboxKey(conversationId, agentId) {
|
|
2288
2288
|
return `activerun:${conversationId}:${agentId}`;
|
|
2289
2289
|
}
|
|
2290
|
-
var ACTIVE_CONVERSATION_HEADER = "x-cabane-active-conversation";
|
|
2291
2290
|
var CabaneApi = class {
|
|
2292
2291
|
constructor(opts) {
|
|
2293
2292
|
this.opts = opts;
|
|
@@ -2425,45 +2424,6 @@ var CabaneApi = class {
|
|
|
2425
2424
|
getConversation(workspaceId, conversationId) {
|
|
2426
2425
|
return this.request("GET", `/api/workspaces/${workspaceId}/conversations/${conversationId}`);
|
|
2427
2426
|
}
|
|
2428
|
-
// CT339: spawn a sub-agent conversation — the public create-conversation
|
|
2429
|
-
// endpoint (`POST /conversations/messages`) with an addressed agent ask,
|
|
2430
|
-
// the machinery the `sub_agent` turn-control tool is sugar over. Two things make
|
|
2431
|
-
// it distinct from an ordinary `request` call, so it does its own `fetch`:
|
|
2432
|
-
// - a PER-CALL bearer — the turn's OBO token when the API minted one, else the
|
|
2433
|
-
// companion PAT — so the spawn carries the same authority as the agent's other
|
|
2434
|
-
// cabane calls this turn (mirrors build-options' `turnToken ?? agentPat`);
|
|
2435
|
-
// - the `x-cabane-active-conversation` header naming the caller's turn, which
|
|
2436
|
-
// the server verifies against the live run to resolve the caller pair for
|
|
2437
|
-
// the explicit child's birth subscription (the same origin that stamps
|
|
2438
|
-
// provenance).
|
|
2439
|
-
// Single-shot (no outbox/retry): a `sub_agent` spawn is a live, user-visible
|
|
2440
|
-
// action, and its result is returned to the agent immediately. Returns
|
|
2441
|
-
// `{ status, body }` un-thrown so the caller maps validation errors
|
|
2442
|
-
// to a legible tool result rather than an exception.
|
|
2443
|
-
async createSubAgentConversation(workspaceId, activeConversationId, bearer, body) {
|
|
2444
|
-
const res = await fetch(`${this.base}/api/workspaces/${workspaceId}/conversations/messages`, {
|
|
2445
|
-
method: "POST",
|
|
2446
|
-
headers: {
|
|
2447
|
-
Authorization: `Bearer ${bearer}`,
|
|
2448
|
-
"content-type": "application/json",
|
|
2449
|
-
accept: "application/json",
|
|
2450
|
-
// CT238/CT339: the per-turn active-conversation header — server-verified
|
|
2451
|
-
// against the caller's live run before it's honored (never client-trusted).
|
|
2452
|
-
[ACTIVE_CONVERSATION_HEADER]: activeConversationId
|
|
2453
|
-
},
|
|
2454
|
-
body: JSON.stringify(body)
|
|
2455
|
-
});
|
|
2456
|
-
const raw = await res.text();
|
|
2457
|
-
let parsed = void 0;
|
|
2458
|
-
if (raw.length > 0) {
|
|
2459
|
-
try {
|
|
2460
|
-
parsed = JSON.parse(raw);
|
|
2461
|
-
} catch {
|
|
2462
|
-
parsed = raw;
|
|
2463
|
-
}
|
|
2464
|
-
}
|
|
2465
|
-
return { status: res.status, body: parsed };
|
|
2466
|
-
}
|
|
2467
2427
|
// SJ383: companion-only participant ops. All three authenticate with the
|
|
2468
2428
|
// workspace's agent-bound PAT (passed as `token` on this client) — never
|
|
2469
2429
|
// the user PAT.
|
|
@@ -3407,7 +3367,7 @@ var turnRequestSchema = z8.object({
|
|
|
3407
3367
|
// EXTERNAL adapters (Codex / opencode) mount it by URL under the key
|
|
3408
3368
|
// `cabane_companion` — using the same `bearer` (the turn token) and the same
|
|
3409
3369
|
// active-conversation header they send to the `cabane` server — so their
|
|
3410
|
-
// agents get `ask`/`wake_me`/`send`/`
|
|
3370
|
+
// agents get `ask`/`reply_to`/`wake_me`/`send`/`skip_turn`, the
|
|
3411
3371
|
// verbs they can't get from the companion's in-process SDK server. Optional:
|
|
3412
3372
|
// claude-code ignores it (it mounts the in-process instance instead), and
|
|
3413
3373
|
// every existing `cabane`-block fixture keeps parsing unchanged; the
|
|
@@ -3770,7 +3730,7 @@ var CLAUDE_CODE_ADDENDUM = [
|
|
|
3770
3730
|
"`mcp__cabane__finalize_upload`, and `mcp__cabane__mint_render_token`; the turn tools are",
|
|
3771
3731
|
"`mcp__cabane_companion__ask`, `mcp__cabane_companion__reply_to`,",
|
|
3772
3732
|
"`mcp__cabane_companion__wake_me`,",
|
|
3773
|
-
"`mcp__cabane_companion__send`,
|
|
3733
|
+
"`mcp__cabane_companion__send`, and",
|
|
3774
3734
|
"`mcp__cabane_companion__skip_turn`. Where this prompt names a tool by its bare verb, call",
|
|
3775
3735
|
"the prefixed form from your tool list. Your harness's own tools (shell, local file",
|
|
3776
3736
|
"read/edit) keep their ordinary names and act on the local machine only."
|
|
@@ -3882,7 +3842,7 @@ function decideResume(stored, currentCwd) {
|
|
|
3882
3842
|
|
|
3883
3843
|
// packages/agent-runtime/src/claude-code/options.ts
|
|
3884
3844
|
var CABANE_MCP_SERVER = "cabane";
|
|
3885
|
-
var
|
|
3845
|
+
var ACTIVE_CONVERSATION_HEADER = "x-cabane-active-conversation";
|
|
3886
3846
|
async function preToolUseHook(input) {
|
|
3887
3847
|
const toolInput = "tool_input" in input && input.tool_input && typeof input.tool_input === "object" ? input.tool_input : {};
|
|
3888
3848
|
const toolName = "tool_name" in input ? input.tool_name : void 0;
|
|
@@ -3919,7 +3879,7 @@ function buildClaudeCodeOptions(req, augment) {
|
|
|
3919
3879
|
url: req.cabane.mcpUrl,
|
|
3920
3880
|
headers: {
|
|
3921
3881
|
Authorization: `Bearer ${req.cabane.bearer}`,
|
|
3922
|
-
[
|
|
3882
|
+
[ACTIVE_CONVERSATION_HEADER]: req.cabane.activeConversationId
|
|
3923
3883
|
},
|
|
3924
3884
|
// Claude Code 2.1.x defers every MCP tool behind tool-search by default;
|
|
3925
3885
|
// for cabane — the always-needed core toolset — force it into the turn-1
|
|
@@ -4741,7 +4701,7 @@ function selectAdapter(registry, runtime) {
|
|
|
4741
4701
|
// packages/agent-runtime/src/opencode/addendum.ts
|
|
4742
4702
|
var OPENCODE_ADDENDUM = [
|
|
4743
4703
|
"Every tool here goes by its plain name: the workspace tool is `sdk`; the turn tools are",
|
|
4744
|
-
"`ask`, `reply_to`, `wake_me`, `send`, `
|
|
4704
|
+
"`ask`, `reply_to`, `wake_me`, `send`, `skip_turn`; the ancillary tools are",
|
|
4745
4705
|
"`list_workspaces`, `read_binary`, `upload`, `begin_upload`, `finalize_upload`,",
|
|
4746
4706
|
"`mint_render_token`; and the host tools are plain verbs too (`bash`, `read`, `edit`). Mind",
|
|
4747
4707
|
"the collision: a bare `read` or `edit` is the HOST tool, acting on the local machine \u2014 a",
|
|
@@ -5008,7 +4968,7 @@ function parseOpencodeModel(model) {
|
|
|
5008
4968
|
// packages/agent-runtime/src/opencode/run-spec.ts
|
|
5009
4969
|
var CABANE_MCP_SERVER2 = "cabane";
|
|
5010
4970
|
var TURN_CONTROL_MCP_SERVER = "cabane_companion";
|
|
5011
|
-
var
|
|
4971
|
+
var ACTIVE_CONVERSATION_HEADER2 = "x-cabane-active-conversation";
|
|
5012
4972
|
function buildRunSpec(req, resumeSessionId) {
|
|
5013
4973
|
const { policy, config } = req;
|
|
5014
4974
|
const directory = req.local.cwd ?? "";
|
|
@@ -5065,7 +5025,7 @@ function buildMcp(req) {
|
|
|
5065
5025
|
url: req.cabane.mcpUrl,
|
|
5066
5026
|
headers: {
|
|
5067
5027
|
Authorization: `Bearer ${req.cabane.bearer}`,
|
|
5068
|
-
[
|
|
5028
|
+
[ACTIVE_CONVERSATION_HEADER2]: req.cabane.activeConversationId
|
|
5069
5029
|
},
|
|
5070
5030
|
enabled: true
|
|
5071
5031
|
};
|
|
@@ -5075,7 +5035,7 @@ function buildMcp(req) {
|
|
|
5075
5035
|
url: req.cabane.turnControlUrl,
|
|
5076
5036
|
headers: {
|
|
5077
5037
|
Authorization: `Bearer ${req.cabane.bearer}`,
|
|
5078
|
-
[
|
|
5038
|
+
[ACTIVE_CONVERSATION_HEADER2]: req.cabane.activeConversationId
|
|
5079
5039
|
},
|
|
5080
5040
|
enabled: true
|
|
5081
5041
|
};
|
|
@@ -5718,7 +5678,7 @@ var CODEX_ADDENDUM = [
|
|
|
5718
5678
|
"the deferred-tool inventory and invoke that exact qualified name; never conclude the SDK is",
|
|
5719
5679
|
"absent without attempting discovery and invocation, and if a call fails, report the recorded",
|
|
5720
5680
|
"error. The turn tools are `mcp__cabane_companion__ask` / `reply_to` / `wake_me` /",
|
|
5721
|
-
"`send` / `
|
|
5681
|
+
"`send` / `skip_turn`, and the ancillary tools",
|
|
5722
5682
|
"`mcp__cabane__list_workspaces` /",
|
|
5723
5683
|
"`read_binary` / `upload` / `begin_upload` / `finalize_upload` / `mint_render_token` \u2014 all",
|
|
5724
5684
|
"possibly deferred too. There is no Cabane `read`/`write`/`search` tool \u2014 those are",
|
|
@@ -6082,7 +6042,7 @@ function parseCodexModel(model) {
|
|
|
6082
6042
|
// packages/agent-runtime/src/codex/run-spec.ts
|
|
6083
6043
|
var CABANE_MCP_SERVER3 = "cabane";
|
|
6084
6044
|
var TURN_CONTROL_MCP_SERVER2 = "cabane_companion";
|
|
6085
|
-
var
|
|
6045
|
+
var ACTIVE_CONVERSATION_HEADER3 = "x-cabane-active-conversation";
|
|
6086
6046
|
var ENV_ENVELOPE_KEYS = [
|
|
6087
6047
|
"CABANE_PLAYGROUND",
|
|
6088
6048
|
"CABANE_PLAYGROUND_BIN",
|
|
@@ -6166,7 +6126,7 @@ function buildConfig(req, baseInstructionsFile) {
|
|
|
6166
6126
|
url: req.cabane.mcpUrl,
|
|
6167
6127
|
http_headers: {
|
|
6168
6128
|
Authorization: `Bearer ${req.cabane.bearer}`,
|
|
6169
|
-
[
|
|
6129
|
+
[ACTIVE_CONVERSATION_HEADER3]: req.cabane.activeConversationId
|
|
6170
6130
|
},
|
|
6171
6131
|
default_tools_approval_mode: "approve"
|
|
6172
6132
|
};
|
|
@@ -6175,7 +6135,7 @@ function buildConfig(req, baseInstructionsFile) {
|
|
|
6175
6135
|
url: req.cabane.turnControlUrl,
|
|
6176
6136
|
http_headers: {
|
|
6177
6137
|
Authorization: `Bearer ${req.cabane.bearer}`,
|
|
6178
|
-
[
|
|
6138
|
+
[ACTIVE_CONVERSATION_HEADER3]: req.cabane.activeConversationId
|
|
6179
6139
|
},
|
|
6180
6140
|
default_tools_approval_mode: "approve"
|
|
6181
6141
|
};
|
|
@@ -7229,8 +7189,6 @@ var ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${ASK_TOOL}`;
|
|
|
7229
7189
|
var REPLY_TO_TOOL = "reply_to";
|
|
7230
7190
|
var REPLY_TO_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${REPLY_TO_TOOL}`;
|
|
7231
7191
|
var MAX_ASK_ITEMS = 20;
|
|
7232
|
-
var SUB_AGENT_TOOL = "sub_agent";
|
|
7233
|
-
var SUB_AGENT_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${SUB_AGENT_TOOL}`;
|
|
7234
7192
|
var WAKE_ME_TOOL = "wake_me";
|
|
7235
7193
|
var WAKE_ME_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WAKE_ME_TOOL}`;
|
|
7236
7194
|
var CANCEL_WAKE_TOOL = "cancel_wake";
|
|
@@ -7254,14 +7212,7 @@ function createSkipState() {
|
|
|
7254
7212
|
return { skipped: false, reason: null };
|
|
7255
7213
|
}
|
|
7256
7214
|
function createAskState() {
|
|
7257
|
-
return {
|
|
7258
|
-
targetUserId: null,
|
|
7259
|
-
question: null,
|
|
7260
|
-
headline: null,
|
|
7261
|
-
options: null,
|
|
7262
|
-
questions: null,
|
|
7263
|
-
order: null
|
|
7264
|
-
};
|
|
7215
|
+
return { targetUserId: null, question: null, headline: null, options: null, questions: null };
|
|
7265
7216
|
}
|
|
7266
7217
|
function createWakeState() {
|
|
7267
7218
|
return { afterSeconds: null, at: null, note: null, cancelled: false };
|
|
@@ -7276,8 +7227,8 @@ function resolveDeclaredReplyField(input) {
|
|
|
7276
7227
|
);
|
|
7277
7228
|
const askRaised = Boolean(input.askState.targetUserId);
|
|
7278
7229
|
const wakeArmed = input.wakeState.afterSeconds !== null || input.wakeState.at !== null;
|
|
7279
|
-
if (outwardSend || askRaised || wakeArmed
|
|
7280
|
-
return { answersMessageId: input.owedReplyMessageId };
|
|
7230
|
+
if (outwardSend || askRaised || wakeArmed) return {};
|
|
7231
|
+
return { answersMessageId: input.owedReplyMessageId, answersAutoDeclared: true };
|
|
7281
7232
|
}
|
|
7282
7233
|
function wakeCommitField(state) {
|
|
7283
7234
|
if (state.cancelled) return { wake: { cancel: true } };
|
|
@@ -7291,7 +7242,7 @@ function wakeCommitField(state) {
|
|
|
7291
7242
|
}
|
|
7292
7243
|
};
|
|
7293
7244
|
}
|
|
7294
|
-
function createTurnControlMcpServer(sendState, skipState, askState,
|
|
7245
|
+
function createTurnControlMcpServer(sendState, skipState, askState, wakeState, replyState, controlOrder) {
|
|
7295
7246
|
return createSdkMcpServer({
|
|
7296
7247
|
name: COMPANION_LOCAL_MCP_SERVER,
|
|
7297
7248
|
version: "0.0.0",
|
|
@@ -7407,7 +7358,6 @@ function createTurnControlMcpServer(sendState, skipState, askState, subAgentCrea
|
|
|
7407
7358
|
};
|
|
7408
7359
|
}
|
|
7409
7360
|
askState.targetUserId = args.targetUserId;
|
|
7410
|
-
askState.order = controlOrder?.next() ?? null;
|
|
7411
7361
|
if (hasArray) {
|
|
7412
7362
|
askState.questions = args.questions;
|
|
7413
7363
|
askState.question = null;
|
|
@@ -7428,42 +7378,6 @@ function createTurnControlMcpServer(sendState, skipState, askState, subAgentCrea
|
|
|
7428
7378
|
{ annotations: { readOnlyHint: true, openWorldHint: false }, alwaysLoad: true }
|
|
7429
7379
|
)
|
|
7430
7380
|
] : [],
|
|
7431
|
-
...subAgentCreate ? [
|
|
7432
|
-
tool(
|
|
7433
|
-
SUB_AGENT_TOOL,
|
|
7434
|
-
"Ask into a child conversation \u2014 a bounded question or piece of work whose ANSWER returns to you automatically. This creates the child and dispatches its agent now, returning the child's `conversationId` immediately; the answer lands LATER as a message in THIS conversation, and you're woken once every child you asked this turn has answered. So DON'T wait: after asking, 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 and it carries the answer). Parallel fan-out = call this N times in one turn (they run concurrently; ONE wake carrying all the answers); series = one call per turn. `prompt` is the ask itself \u2014 write it as a request that expects an answer back, not a role you are assigning or a job description: self-contained (the child starts fresh, with only this prompt), and say what shape the answer should take. `agentId` (optional) asks a PEER instead of yourself \u2014 same mechanics, a different mind (use for capability/context you lack); default (self) is a fresh context window of you. `title` (optional) names the child thread (answers link it, so a legible title helps). A single child buys no wall-clock time (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 ask a child for a lookup you can do in-turn with your own tools. The answer 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.",
|
|
7435
|
-
{
|
|
7436
|
-
prompt: z13.string().min(1).max(65536).describe(
|
|
7437
|
-
"The ask \u2014 a self-contained request that expects an answer back (the child starts fresh: only this prompt + the thread it lands in). Say what shape the answer should take."
|
|
7438
|
-
),
|
|
7439
|
-
agentId: z13.string().uuid().optional().describe(
|
|
7440
|
-
"Optional peer to run the sub-agent as (a workspace agent id, from your turn context's roster); omit to spawn yourself with a fresh context window."
|
|
7441
|
-
),
|
|
7442
|
-
title: z13.string().max(200).optional().describe("Optional title for the child thread (result chips link it).")
|
|
7443
|
-
},
|
|
7444
|
-
async (args) => {
|
|
7445
|
-
const result = await subAgentCreate(args);
|
|
7446
|
-
if (!result.ok) {
|
|
7447
|
-
return {
|
|
7448
|
-
content: [{ type: "text", text: result.error }],
|
|
7449
|
-
isError: true
|
|
7450
|
-
};
|
|
7451
|
-
}
|
|
7452
|
-
return {
|
|
7453
|
-
content: [
|
|
7454
|
-
{
|
|
7455
|
-
type: "text",
|
|
7456
|
-
text: JSON.stringify({
|
|
7457
|
-
conversationId: result.conversationId,
|
|
7458
|
-
note: "Asked. Don't wait for it \u2014 finish what else this turn can do, then end your turn; the answer posts back here and you're woken once every child you asked this turn has answered."
|
|
7459
|
-
})
|
|
7460
|
-
}
|
|
7461
|
-
]
|
|
7462
|
-
};
|
|
7463
|
-
},
|
|
7464
|
-
{ annotations: { readOnlyHint: false, openWorldHint: true }, alwaysLoad: true }
|
|
7465
|
-
)
|
|
7466
|
-
] : [],
|
|
7467
7381
|
...wakeState ? [
|
|
7468
7382
|
tool(
|
|
7469
7383
|
WAKE_ME_TOOL,
|
|
@@ -7983,10 +7897,15 @@ var TurnCommitter = class {
|
|
|
7983
7897
|
}
|
|
7984
7898
|
// CT1281: the order the control tools were CALLED, for the addressed rows the
|
|
7985
7899
|
// server writes from this one commit. It reports a position only for an intent
|
|
7986
|
-
// that actually made it into the commit —
|
|
7987
|
-
//
|
|
7988
|
-
//
|
|
7989
|
-
//
|
|
7900
|
+
// that actually made it into the commit — a self-send the committer stripped
|
|
7901
|
+
// contributes no row and so has no place in the line. An intent with no
|
|
7902
|
+
// recorded call (the runtime's auto-declared reply) is deliberately absent, and
|
|
7903
|
+
// the server sorts it after every recorded one — it binds the turn's closing
|
|
7904
|
+
// words.
|
|
7905
|
+
//
|
|
7906
|
+
// The ask is absent by design, not by omission: its carrier queues last among
|
|
7907
|
+
// the turn's rows however early the tool fired, so there is no position to
|
|
7908
|
+
// report.
|
|
7990
7909
|
orderField(fields) {
|
|
7991
7910
|
const order = {};
|
|
7992
7911
|
const replyOrder = this.deps.replyState.order;
|
|
@@ -7994,14 +7913,14 @@ var TurnCommitter = class {
|
|
|
7994
7913
|
if (fields.dispatch !== void 0 && this.deps.sendState.order !== null) {
|
|
7995
7914
|
order.send = this.deps.sendState.order;
|
|
7996
7915
|
}
|
|
7997
|
-
if (fields.ask !== void 0 && this.deps.askState.order !== null) {
|
|
7998
|
-
order.ask = this.deps.askState.order;
|
|
7999
|
-
}
|
|
8000
7916
|
return Object.keys(order).length > 0 ? { turnControlOrder: order } : {};
|
|
8001
7917
|
}
|
|
8002
7918
|
// The declared reply: explicit `reply_to` first, else the runtime's own
|
|
8003
7919
|
// declaration for the turn that just answers — see
|
|
8004
|
-
// `resolveDeclaredReplyField` for the whole rule and its reasons.
|
|
7920
|
+
// `resolveDeclaredReplyField` for the whole rule and its reasons. CT1224: an
|
|
7921
|
+
// auto-declaration is flagged as such on the wire, because the server drops it
|
|
7922
|
+
// when the turn also opened a conversation (a fan-out is mid-arc). An explicit
|
|
7923
|
+
// `reply_to` carries no flag and always stands.
|
|
8005
7924
|
answersField(kind) {
|
|
8006
7925
|
return resolveDeclaredReplyField({
|
|
8007
7926
|
kind,
|
|
@@ -8010,8 +7929,7 @@ var TurnCommitter = class {
|
|
|
8010
7929
|
replyState: this.deps.replyState,
|
|
8011
7930
|
sendState: this.deps.sendState,
|
|
8012
7931
|
askState: this.deps.askState,
|
|
8013
|
-
wakeState: this.deps.wakeState
|
|
8014
|
-
spawnedSubAgent: this.deps.spawnedSubAgent()
|
|
7932
|
+
wakeState: this.deps.wakeState
|
|
8015
7933
|
});
|
|
8016
7934
|
}
|
|
8017
7935
|
// Carry both halves of the addressed send. The server writes `dispatchBody`
|
|
@@ -8132,17 +8050,6 @@ function describeErrorBody(body) {
|
|
|
8132
8050
|
if (text.length === 0) return void 0;
|
|
8133
8051
|
return text.length > ERROR_BODY_LOG_CAP ? `${text.slice(0, ERROR_BODY_LOG_CAP)}\u2026` : text;
|
|
8134
8052
|
}
|
|
8135
|
-
function describeSubAgentError(status2, body) {
|
|
8136
|
-
const code = body && typeof body === "object" && "error" in body ? String(body.error) : void 0;
|
|
8137
|
-
switch (code) {
|
|
8138
|
-
case "dispatch_agent_not_found":
|
|
8139
|
-
return "sub_agent: no live agent in this workspace matches that `agentId`. Check the roster in your turn context, or omit `agentId` to spawn yourself.";
|
|
8140
|
-
case "dispatch_ask_requires_dispatch":
|
|
8141
|
-
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.`;
|
|
8142
|
-
default:
|
|
8143
|
-
return `sub_agent: the spawn failed (${code ?? `HTTP ${status2}`}).`;
|
|
8144
|
-
}
|
|
8145
|
-
}
|
|
8146
8053
|
var Dispatcher = class {
|
|
8147
8054
|
constructor(opts) {
|
|
8148
8055
|
this.opts = opts;
|
|
@@ -8505,36 +8412,10 @@ ${reason}`,
|
|
|
8505
8412
|
const wakeState = createWakeState();
|
|
8506
8413
|
const replyState = createReplyState();
|
|
8507
8414
|
const controlOrder = createTurnControlOrder();
|
|
8508
|
-
let spawnedSubAgent = false;
|
|
8509
|
-
const subAgentCreate = async (args) => {
|
|
8510
|
-
const dispatchTarget = args.agentId ?? payload.agentId;
|
|
8511
|
-
const projectId = turnContext.conversation.projectId;
|
|
8512
|
-
const { status: status2, body } = await this.opts.api.createSubAgentConversation(
|
|
8513
|
-
workspaceId,
|
|
8514
|
-
payload.conversationId,
|
|
8515
|
-
turnContext.turnToken ?? this.opts.credential,
|
|
8516
|
-
{
|
|
8517
|
-
...projectId ? { projectId } : {},
|
|
8518
|
-
...args.title ? { title: args.title } : {},
|
|
8519
|
-
body: args.prompt,
|
|
8520
|
-
dispatch: dispatchTarget,
|
|
8521
|
-
dispatchAsk: true,
|
|
8522
|
-
parentConversationId: payload.conversationId
|
|
8523
|
-
}
|
|
8524
|
-
);
|
|
8525
|
-
if (status2 >= 400) return { ok: false, error: describeSubAgentError(status2, body) };
|
|
8526
|
-
const conversationId = body?.conversation?.id;
|
|
8527
|
-
if (!conversationId) {
|
|
8528
|
-
return { ok: false, error: "sub_agent: the spawn returned no conversation id." };
|
|
8529
|
-
}
|
|
8530
|
-
spawnedSubAgent = true;
|
|
8531
|
-
return { ok: true, conversationId };
|
|
8532
|
-
};
|
|
8533
8415
|
const turnControlServer = createTurnControlMcpServer(
|
|
8534
8416
|
sendState,
|
|
8535
8417
|
skipState,
|
|
8536
8418
|
askState,
|
|
8537
|
-
subAgentCreate,
|
|
8538
8419
|
wakeState,
|
|
8539
8420
|
replyState,
|
|
8540
8421
|
controlOrder
|
|
@@ -8670,8 +8551,7 @@ ${reason}`,
|
|
|
8670
8551
|
replyState,
|
|
8671
8552
|
// The ledger-derived owed reply, for the runtime's own declaration when
|
|
8672
8553
|
// the agent doesn't call `reply_to` (resolveDeclaredReplyField).
|
|
8673
|
-
owedReplyMessageId: turnContext.owedReplyMessageId ?? null
|
|
8674
|
-
spawnedSubAgent: () => spawnedSubAgent
|
|
8554
|
+
owedReplyMessageId: turnContext.owedReplyMessageId ?? null
|
|
8675
8555
|
});
|
|
8676
8556
|
const usesHttpTurnControl = turnRuntime === "codex" || turnRuntime === "opencode";
|
|
8677
8557
|
let turnControlIntentFetched = false;
|
|
@@ -8687,7 +8567,6 @@ ${reason}`,
|
|
|
8687
8567
|
);
|
|
8688
8568
|
if (intent.ask) {
|
|
8689
8569
|
askState.targetUserId = intent.ask.targetUserId;
|
|
8690
|
-
askState.order = intent.askOrder ?? null;
|
|
8691
8570
|
if (intent.ask.questions && intent.ask.questions.length > 0) {
|
|
8692
8571
|
askState.questions = intent.ask.questions;
|
|
8693
8572
|
askState.question = null;
|
package/dist/runtime.js
CHANGED
|
@@ -1707,7 +1707,6 @@ var ACTIVE_RUN_OUTBOX_SEQ = 0;
|
|
|
1707
1707
|
function activeRunOutboxKey(conversationId, agentId) {
|
|
1708
1708
|
return `activerun:${conversationId}:${agentId}`;
|
|
1709
1709
|
}
|
|
1710
|
-
var ACTIVE_CONVERSATION_HEADER = "x-cabane-active-conversation";
|
|
1711
1710
|
var CabaneApi = class {
|
|
1712
1711
|
constructor(opts) {
|
|
1713
1712
|
this.opts = opts;
|
|
@@ -1845,45 +1844,6 @@ var CabaneApi = class {
|
|
|
1845
1844
|
getConversation(workspaceId, conversationId) {
|
|
1846
1845
|
return this.request("GET", `/api/workspaces/${workspaceId}/conversations/${conversationId}`);
|
|
1847
1846
|
}
|
|
1848
|
-
// CT339: spawn a sub-agent conversation — the public create-conversation
|
|
1849
|
-
// endpoint (`POST /conversations/messages`) with an addressed agent ask,
|
|
1850
|
-
// the machinery the `sub_agent` turn-control tool is sugar over. Two things make
|
|
1851
|
-
// it distinct from an ordinary `request` call, so it does its own `fetch`:
|
|
1852
|
-
// - a PER-CALL bearer — the turn's OBO token when the API minted one, else the
|
|
1853
|
-
// companion PAT — so the spawn carries the same authority as the agent's other
|
|
1854
|
-
// cabane calls this turn (mirrors build-options' `turnToken ?? agentPat`);
|
|
1855
|
-
// - the `x-cabane-active-conversation` header naming the caller's turn, which
|
|
1856
|
-
// the server verifies against the live run to resolve the caller pair for
|
|
1857
|
-
// the explicit child's birth subscription (the same origin that stamps
|
|
1858
|
-
// provenance).
|
|
1859
|
-
// Single-shot (no outbox/retry): a `sub_agent` spawn is a live, user-visible
|
|
1860
|
-
// action, and its result is returned to the agent immediately. Returns
|
|
1861
|
-
// `{ status, body }` un-thrown so the caller maps validation errors
|
|
1862
|
-
// to a legible tool result rather than an exception.
|
|
1863
|
-
async createSubAgentConversation(workspaceId, activeConversationId, bearer, body) {
|
|
1864
|
-
const res = await fetch(`${this.base}/api/workspaces/${workspaceId}/conversations/messages`, {
|
|
1865
|
-
method: "POST",
|
|
1866
|
-
headers: {
|
|
1867
|
-
Authorization: `Bearer ${bearer}`,
|
|
1868
|
-
"content-type": "application/json",
|
|
1869
|
-
accept: "application/json",
|
|
1870
|
-
// CT238/CT339: the per-turn active-conversation header — server-verified
|
|
1871
|
-
// against the caller's live run before it's honored (never client-trusted).
|
|
1872
|
-
[ACTIVE_CONVERSATION_HEADER]: activeConversationId
|
|
1873
|
-
},
|
|
1874
|
-
body: JSON.stringify(body)
|
|
1875
|
-
});
|
|
1876
|
-
const raw = await res.text();
|
|
1877
|
-
let parsed = void 0;
|
|
1878
|
-
if (raw.length > 0) {
|
|
1879
|
-
try {
|
|
1880
|
-
parsed = JSON.parse(raw);
|
|
1881
|
-
} catch {
|
|
1882
|
-
parsed = raw;
|
|
1883
|
-
}
|
|
1884
|
-
}
|
|
1885
|
-
return { status: res.status, body: parsed };
|
|
1886
|
-
}
|
|
1887
1847
|
// SJ383: companion-only participant ops. All three authenticate with the
|
|
1888
1848
|
// workspace's agent-bound PAT (passed as `token` on this client) — never
|
|
1889
1849
|
// the user PAT.
|
|
@@ -2906,7 +2866,7 @@ var turnRequestSchema = z8.object({
|
|
|
2906
2866
|
// EXTERNAL adapters (Codex / opencode) mount it by URL under the key
|
|
2907
2867
|
// `cabane_companion` — using the same `bearer` (the turn token) and the same
|
|
2908
2868
|
// active-conversation header they send to the `cabane` server — so their
|
|
2909
|
-
// agents get `ask`/`wake_me`/`send`/`
|
|
2869
|
+
// agents get `ask`/`reply_to`/`wake_me`/`send`/`skip_turn`, the
|
|
2910
2870
|
// verbs they can't get from the companion's in-process SDK server. Optional:
|
|
2911
2871
|
// claude-code ignores it (it mounts the in-process instance instead), and
|
|
2912
2872
|
// every existing `cabane`-block fixture keeps parsing unchanged; the
|
|
@@ -3269,7 +3229,7 @@ var CLAUDE_CODE_ADDENDUM = [
|
|
|
3269
3229
|
"`mcp__cabane__finalize_upload`, and `mcp__cabane__mint_render_token`; the turn tools are",
|
|
3270
3230
|
"`mcp__cabane_companion__ask`, `mcp__cabane_companion__reply_to`,",
|
|
3271
3231
|
"`mcp__cabane_companion__wake_me`,",
|
|
3272
|
-
"`mcp__cabane_companion__send`,
|
|
3232
|
+
"`mcp__cabane_companion__send`, and",
|
|
3273
3233
|
"`mcp__cabane_companion__skip_turn`. Where this prompt names a tool by its bare verb, call",
|
|
3274
3234
|
"the prefixed form from your tool list. Your harness's own tools (shell, local file",
|
|
3275
3235
|
"read/edit) keep their ordinary names and act on the local machine only."
|
|
@@ -3381,7 +3341,7 @@ function decideResume(stored, currentCwd) {
|
|
|
3381
3341
|
|
|
3382
3342
|
// packages/agent-runtime/src/claude-code/options.ts
|
|
3383
3343
|
var CABANE_MCP_SERVER = "cabane";
|
|
3384
|
-
var
|
|
3344
|
+
var ACTIVE_CONVERSATION_HEADER = "x-cabane-active-conversation";
|
|
3385
3345
|
async function preToolUseHook(input) {
|
|
3386
3346
|
const toolInput = "tool_input" in input && input.tool_input && typeof input.tool_input === "object" ? input.tool_input : {};
|
|
3387
3347
|
const toolName = "tool_name" in input ? input.tool_name : void 0;
|
|
@@ -3418,7 +3378,7 @@ function buildClaudeCodeOptions(req, augment) {
|
|
|
3418
3378
|
url: req.cabane.mcpUrl,
|
|
3419
3379
|
headers: {
|
|
3420
3380
|
Authorization: `Bearer ${req.cabane.bearer}`,
|
|
3421
|
-
[
|
|
3381
|
+
[ACTIVE_CONVERSATION_HEADER]: req.cabane.activeConversationId
|
|
3422
3382
|
},
|
|
3423
3383
|
// Claude Code 2.1.x defers every MCP tool behind tool-search by default;
|
|
3424
3384
|
// for cabane — the always-needed core toolset — force it into the turn-1
|
|
@@ -4240,7 +4200,7 @@ function selectAdapter(registry, runtime) {
|
|
|
4240
4200
|
// packages/agent-runtime/src/opencode/addendum.ts
|
|
4241
4201
|
var OPENCODE_ADDENDUM = [
|
|
4242
4202
|
"Every tool here goes by its plain name: the workspace tool is `sdk`; the turn tools are",
|
|
4243
|
-
"`ask`, `reply_to`, `wake_me`, `send`, `
|
|
4203
|
+
"`ask`, `reply_to`, `wake_me`, `send`, `skip_turn`; the ancillary tools are",
|
|
4244
4204
|
"`list_workspaces`, `read_binary`, `upload`, `begin_upload`, `finalize_upload`,",
|
|
4245
4205
|
"`mint_render_token`; and the host tools are plain verbs too (`bash`, `read`, `edit`). Mind",
|
|
4246
4206
|
"the collision: a bare `read` or `edit` is the HOST tool, acting on the local machine \u2014 a",
|
|
@@ -4507,7 +4467,7 @@ function parseOpencodeModel(model) {
|
|
|
4507
4467
|
// packages/agent-runtime/src/opencode/run-spec.ts
|
|
4508
4468
|
var CABANE_MCP_SERVER2 = "cabane";
|
|
4509
4469
|
var TURN_CONTROL_MCP_SERVER = "cabane_companion";
|
|
4510
|
-
var
|
|
4470
|
+
var ACTIVE_CONVERSATION_HEADER2 = "x-cabane-active-conversation";
|
|
4511
4471
|
function buildRunSpec(req, resumeSessionId) {
|
|
4512
4472
|
const { policy, config } = req;
|
|
4513
4473
|
const directory = req.local.cwd ?? "";
|
|
@@ -4564,7 +4524,7 @@ function buildMcp(req) {
|
|
|
4564
4524
|
url: req.cabane.mcpUrl,
|
|
4565
4525
|
headers: {
|
|
4566
4526
|
Authorization: `Bearer ${req.cabane.bearer}`,
|
|
4567
|
-
[
|
|
4527
|
+
[ACTIVE_CONVERSATION_HEADER2]: req.cabane.activeConversationId
|
|
4568
4528
|
},
|
|
4569
4529
|
enabled: true
|
|
4570
4530
|
};
|
|
@@ -4574,7 +4534,7 @@ function buildMcp(req) {
|
|
|
4574
4534
|
url: req.cabane.turnControlUrl,
|
|
4575
4535
|
headers: {
|
|
4576
4536
|
Authorization: `Bearer ${req.cabane.bearer}`,
|
|
4577
|
-
[
|
|
4537
|
+
[ACTIVE_CONVERSATION_HEADER2]: req.cabane.activeConversationId
|
|
4578
4538
|
},
|
|
4579
4539
|
enabled: true
|
|
4580
4540
|
};
|
|
@@ -5217,7 +5177,7 @@ var CODEX_ADDENDUM = [
|
|
|
5217
5177
|
"the deferred-tool inventory and invoke that exact qualified name; never conclude the SDK is",
|
|
5218
5178
|
"absent without attempting discovery and invocation, and if a call fails, report the recorded",
|
|
5219
5179
|
"error. The turn tools are `mcp__cabane_companion__ask` / `reply_to` / `wake_me` /",
|
|
5220
|
-
"`send` / `
|
|
5180
|
+
"`send` / `skip_turn`, and the ancillary tools",
|
|
5221
5181
|
"`mcp__cabane__list_workspaces` /",
|
|
5222
5182
|
"`read_binary` / `upload` / `begin_upload` / `finalize_upload` / `mint_render_token` \u2014 all",
|
|
5223
5183
|
"possibly deferred too. There is no Cabane `read`/`write`/`search` tool \u2014 those are",
|
|
@@ -5581,7 +5541,7 @@ function parseCodexModel(model) {
|
|
|
5581
5541
|
// packages/agent-runtime/src/codex/run-spec.ts
|
|
5582
5542
|
var CABANE_MCP_SERVER3 = "cabane";
|
|
5583
5543
|
var TURN_CONTROL_MCP_SERVER2 = "cabane_companion";
|
|
5584
|
-
var
|
|
5544
|
+
var ACTIVE_CONVERSATION_HEADER3 = "x-cabane-active-conversation";
|
|
5585
5545
|
var ENV_ENVELOPE_KEYS = [
|
|
5586
5546
|
"CABANE_PLAYGROUND",
|
|
5587
5547
|
"CABANE_PLAYGROUND_BIN",
|
|
@@ -5665,7 +5625,7 @@ function buildConfig(req, baseInstructionsFile) {
|
|
|
5665
5625
|
url: req.cabane.mcpUrl,
|
|
5666
5626
|
http_headers: {
|
|
5667
5627
|
Authorization: `Bearer ${req.cabane.bearer}`,
|
|
5668
|
-
[
|
|
5628
|
+
[ACTIVE_CONVERSATION_HEADER3]: req.cabane.activeConversationId
|
|
5669
5629
|
},
|
|
5670
5630
|
default_tools_approval_mode: "approve"
|
|
5671
5631
|
};
|
|
@@ -5674,7 +5634,7 @@ function buildConfig(req, baseInstructionsFile) {
|
|
|
5674
5634
|
url: req.cabane.turnControlUrl,
|
|
5675
5635
|
http_headers: {
|
|
5676
5636
|
Authorization: `Bearer ${req.cabane.bearer}`,
|
|
5677
|
-
[
|
|
5637
|
+
[ACTIVE_CONVERSATION_HEADER3]: req.cabane.activeConversationId
|
|
5678
5638
|
},
|
|
5679
5639
|
default_tools_approval_mode: "approve"
|
|
5680
5640
|
};
|
|
@@ -6728,8 +6688,6 @@ var ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${ASK_TOOL}`;
|
|
|
6728
6688
|
var REPLY_TO_TOOL = "reply_to";
|
|
6729
6689
|
var REPLY_TO_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${REPLY_TO_TOOL}`;
|
|
6730
6690
|
var MAX_ASK_ITEMS = 20;
|
|
6731
|
-
var SUB_AGENT_TOOL = "sub_agent";
|
|
6732
|
-
var SUB_AGENT_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${SUB_AGENT_TOOL}`;
|
|
6733
6691
|
var WAKE_ME_TOOL = "wake_me";
|
|
6734
6692
|
var WAKE_ME_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WAKE_ME_TOOL}`;
|
|
6735
6693
|
var CANCEL_WAKE_TOOL = "cancel_wake";
|
|
@@ -6753,14 +6711,7 @@ function createSkipState() {
|
|
|
6753
6711
|
return { skipped: false, reason: null };
|
|
6754
6712
|
}
|
|
6755
6713
|
function createAskState() {
|
|
6756
|
-
return {
|
|
6757
|
-
targetUserId: null,
|
|
6758
|
-
question: null,
|
|
6759
|
-
headline: null,
|
|
6760
|
-
options: null,
|
|
6761
|
-
questions: null,
|
|
6762
|
-
order: null
|
|
6763
|
-
};
|
|
6714
|
+
return { targetUserId: null, question: null, headline: null, options: null, questions: null };
|
|
6764
6715
|
}
|
|
6765
6716
|
function createWakeState() {
|
|
6766
6717
|
return { afterSeconds: null, at: null, note: null, cancelled: false };
|
|
@@ -6775,8 +6726,8 @@ function resolveDeclaredReplyField(input) {
|
|
|
6775
6726
|
);
|
|
6776
6727
|
const askRaised = Boolean(input.askState.targetUserId);
|
|
6777
6728
|
const wakeArmed = input.wakeState.afterSeconds !== null || input.wakeState.at !== null;
|
|
6778
|
-
if (outwardSend || askRaised || wakeArmed
|
|
6779
|
-
return { answersMessageId: input.owedReplyMessageId };
|
|
6729
|
+
if (outwardSend || askRaised || wakeArmed) return {};
|
|
6730
|
+
return { answersMessageId: input.owedReplyMessageId, answersAutoDeclared: true };
|
|
6780
6731
|
}
|
|
6781
6732
|
function wakeCommitField(state) {
|
|
6782
6733
|
if (state.cancelled) return { wake: { cancel: true } };
|
|
@@ -6790,7 +6741,7 @@ function wakeCommitField(state) {
|
|
|
6790
6741
|
}
|
|
6791
6742
|
};
|
|
6792
6743
|
}
|
|
6793
|
-
function createTurnControlMcpServer(sendState, skipState, askState,
|
|
6744
|
+
function createTurnControlMcpServer(sendState, skipState, askState, wakeState, replyState, controlOrder) {
|
|
6794
6745
|
return createSdkMcpServer({
|
|
6795
6746
|
name: COMPANION_LOCAL_MCP_SERVER,
|
|
6796
6747
|
version: "0.0.0",
|
|
@@ -6906,7 +6857,6 @@ function createTurnControlMcpServer(sendState, skipState, askState, subAgentCrea
|
|
|
6906
6857
|
};
|
|
6907
6858
|
}
|
|
6908
6859
|
askState.targetUserId = args.targetUserId;
|
|
6909
|
-
askState.order = controlOrder?.next() ?? null;
|
|
6910
6860
|
if (hasArray) {
|
|
6911
6861
|
askState.questions = args.questions;
|
|
6912
6862
|
askState.question = null;
|
|
@@ -6927,42 +6877,6 @@ function createTurnControlMcpServer(sendState, skipState, askState, subAgentCrea
|
|
|
6927
6877
|
{ annotations: { readOnlyHint: true, openWorldHint: false }, alwaysLoad: true }
|
|
6928
6878
|
)
|
|
6929
6879
|
] : [],
|
|
6930
|
-
...subAgentCreate ? [
|
|
6931
|
-
tool(
|
|
6932
|
-
SUB_AGENT_TOOL,
|
|
6933
|
-
"Ask into a child conversation \u2014 a bounded question or piece of work whose ANSWER returns to you automatically. This creates the child and dispatches its agent now, returning the child's `conversationId` immediately; the answer lands LATER as a message in THIS conversation, and you're woken once every child you asked this turn has answered. So DON'T wait: after asking, 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 and it carries the answer). Parallel fan-out = call this N times in one turn (they run concurrently; ONE wake carrying all the answers); series = one call per turn. `prompt` is the ask itself \u2014 write it as a request that expects an answer back, not a role you are assigning or a job description: self-contained (the child starts fresh, with only this prompt), and say what shape the answer should take. `agentId` (optional) asks a PEER instead of yourself \u2014 same mechanics, a different mind (use for capability/context you lack); default (self) is a fresh context window of you. `title` (optional) names the child thread (answers link it, so a legible title helps). A single child buys no wall-clock time (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 ask a child for a lookup you can do in-turn with your own tools. The answer 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.",
|
|
6934
|
-
{
|
|
6935
|
-
prompt: z13.string().min(1).max(65536).describe(
|
|
6936
|
-
"The ask \u2014 a self-contained request that expects an answer back (the child starts fresh: only this prompt + the thread it lands in). Say what shape the answer should take."
|
|
6937
|
-
),
|
|
6938
|
-
agentId: z13.string().uuid().optional().describe(
|
|
6939
|
-
"Optional peer to run the sub-agent as (a workspace agent id, from your turn context's roster); omit to spawn yourself with a fresh context window."
|
|
6940
|
-
),
|
|
6941
|
-
title: z13.string().max(200).optional().describe("Optional title for the child thread (result chips link it).")
|
|
6942
|
-
},
|
|
6943
|
-
async (args) => {
|
|
6944
|
-
const result = await subAgentCreate(args);
|
|
6945
|
-
if (!result.ok) {
|
|
6946
|
-
return {
|
|
6947
|
-
content: [{ type: "text", text: result.error }],
|
|
6948
|
-
isError: true
|
|
6949
|
-
};
|
|
6950
|
-
}
|
|
6951
|
-
return {
|
|
6952
|
-
content: [
|
|
6953
|
-
{
|
|
6954
|
-
type: "text",
|
|
6955
|
-
text: JSON.stringify({
|
|
6956
|
-
conversationId: result.conversationId,
|
|
6957
|
-
note: "Asked. Don't wait for it \u2014 finish what else this turn can do, then end your turn; the answer posts back here and you're woken once every child you asked this turn has answered."
|
|
6958
|
-
})
|
|
6959
|
-
}
|
|
6960
|
-
]
|
|
6961
|
-
};
|
|
6962
|
-
},
|
|
6963
|
-
{ annotations: { readOnlyHint: false, openWorldHint: true }, alwaysLoad: true }
|
|
6964
|
-
)
|
|
6965
|
-
] : [],
|
|
6966
6880
|
...wakeState ? [
|
|
6967
6881
|
tool(
|
|
6968
6882
|
WAKE_ME_TOOL,
|
|
@@ -7482,10 +7396,15 @@ var TurnCommitter = class {
|
|
|
7482
7396
|
}
|
|
7483
7397
|
// CT1281: the order the control tools were CALLED, for the addressed rows the
|
|
7484
7398
|
// server writes from this one commit. It reports a position only for an intent
|
|
7485
|
-
// that actually made it into the commit —
|
|
7486
|
-
//
|
|
7487
|
-
//
|
|
7488
|
-
//
|
|
7399
|
+
// that actually made it into the commit — a self-send the committer stripped
|
|
7400
|
+
// contributes no row and so has no place in the line. An intent with no
|
|
7401
|
+
// recorded call (the runtime's auto-declared reply) is deliberately absent, and
|
|
7402
|
+
// the server sorts it after every recorded one — it binds the turn's closing
|
|
7403
|
+
// words.
|
|
7404
|
+
//
|
|
7405
|
+
// The ask is absent by design, not by omission: its carrier queues last among
|
|
7406
|
+
// the turn's rows however early the tool fired, so there is no position to
|
|
7407
|
+
// report.
|
|
7489
7408
|
orderField(fields) {
|
|
7490
7409
|
const order = {};
|
|
7491
7410
|
const replyOrder = this.deps.replyState.order;
|
|
@@ -7493,14 +7412,14 @@ var TurnCommitter = class {
|
|
|
7493
7412
|
if (fields.dispatch !== void 0 && this.deps.sendState.order !== null) {
|
|
7494
7413
|
order.send = this.deps.sendState.order;
|
|
7495
7414
|
}
|
|
7496
|
-
if (fields.ask !== void 0 && this.deps.askState.order !== null) {
|
|
7497
|
-
order.ask = this.deps.askState.order;
|
|
7498
|
-
}
|
|
7499
7415
|
return Object.keys(order).length > 0 ? { turnControlOrder: order } : {};
|
|
7500
7416
|
}
|
|
7501
7417
|
// The declared reply: explicit `reply_to` first, else the runtime's own
|
|
7502
7418
|
// declaration for the turn that just answers — see
|
|
7503
|
-
// `resolveDeclaredReplyField` for the whole rule and its reasons.
|
|
7419
|
+
// `resolveDeclaredReplyField` for the whole rule and its reasons. CT1224: an
|
|
7420
|
+
// auto-declaration is flagged as such on the wire, because the server drops it
|
|
7421
|
+
// when the turn also opened a conversation (a fan-out is mid-arc). An explicit
|
|
7422
|
+
// `reply_to` carries no flag and always stands.
|
|
7504
7423
|
answersField(kind) {
|
|
7505
7424
|
return resolveDeclaredReplyField({
|
|
7506
7425
|
kind,
|
|
@@ -7509,8 +7428,7 @@ var TurnCommitter = class {
|
|
|
7509
7428
|
replyState: this.deps.replyState,
|
|
7510
7429
|
sendState: this.deps.sendState,
|
|
7511
7430
|
askState: this.deps.askState,
|
|
7512
|
-
wakeState: this.deps.wakeState
|
|
7513
|
-
spawnedSubAgent: this.deps.spawnedSubAgent()
|
|
7431
|
+
wakeState: this.deps.wakeState
|
|
7514
7432
|
});
|
|
7515
7433
|
}
|
|
7516
7434
|
// Carry both halves of the addressed send. The server writes `dispatchBody`
|
|
@@ -7631,17 +7549,6 @@ function describeErrorBody(body) {
|
|
|
7631
7549
|
if (text.length === 0) return void 0;
|
|
7632
7550
|
return text.length > ERROR_BODY_LOG_CAP ? `${text.slice(0, ERROR_BODY_LOG_CAP)}\u2026` : text;
|
|
7633
7551
|
}
|
|
7634
|
-
function describeSubAgentError(status, body) {
|
|
7635
|
-
const code = body && typeof body === "object" && "error" in body ? String(body.error) : void 0;
|
|
7636
|
-
switch (code) {
|
|
7637
|
-
case "dispatch_agent_not_found":
|
|
7638
|
-
return "sub_agent: no live agent in this workspace matches that `agentId`. Check the roster in your turn context, or omit `agentId` to spawn yourself.";
|
|
7639
|
-
case "dispatch_ask_requires_dispatch":
|
|
7640
|
-
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.`;
|
|
7641
|
-
default:
|
|
7642
|
-
return `sub_agent: the spawn failed (${code ?? `HTTP ${status}`}).`;
|
|
7643
|
-
}
|
|
7644
|
-
}
|
|
7645
7552
|
var Dispatcher = class {
|
|
7646
7553
|
constructor(opts) {
|
|
7647
7554
|
this.opts = opts;
|
|
@@ -8004,36 +7911,10 @@ ${reason}`,
|
|
|
8004
7911
|
const wakeState = createWakeState();
|
|
8005
7912
|
const replyState = createReplyState();
|
|
8006
7913
|
const controlOrder = createTurnControlOrder();
|
|
8007
|
-
let spawnedSubAgent = false;
|
|
8008
|
-
const subAgentCreate = async (args) => {
|
|
8009
|
-
const dispatchTarget = args.agentId ?? payload.agentId;
|
|
8010
|
-
const projectId = turnContext.conversation.projectId;
|
|
8011
|
-
const { status, body } = await this.opts.api.createSubAgentConversation(
|
|
8012
|
-
workspaceId,
|
|
8013
|
-
payload.conversationId,
|
|
8014
|
-
turnContext.turnToken ?? this.opts.credential,
|
|
8015
|
-
{
|
|
8016
|
-
...projectId ? { projectId } : {},
|
|
8017
|
-
...args.title ? { title: args.title } : {},
|
|
8018
|
-
body: args.prompt,
|
|
8019
|
-
dispatch: dispatchTarget,
|
|
8020
|
-
dispatchAsk: true,
|
|
8021
|
-
parentConversationId: payload.conversationId
|
|
8022
|
-
}
|
|
8023
|
-
);
|
|
8024
|
-
if (status >= 400) return { ok: false, error: describeSubAgentError(status, body) };
|
|
8025
|
-
const conversationId = body?.conversation?.id;
|
|
8026
|
-
if (!conversationId) {
|
|
8027
|
-
return { ok: false, error: "sub_agent: the spawn returned no conversation id." };
|
|
8028
|
-
}
|
|
8029
|
-
spawnedSubAgent = true;
|
|
8030
|
-
return { ok: true, conversationId };
|
|
8031
|
-
};
|
|
8032
7914
|
const turnControlServer = createTurnControlMcpServer(
|
|
8033
7915
|
sendState,
|
|
8034
7916
|
skipState,
|
|
8035
7917
|
askState,
|
|
8036
|
-
subAgentCreate,
|
|
8037
7918
|
wakeState,
|
|
8038
7919
|
replyState,
|
|
8039
7920
|
controlOrder
|
|
@@ -8169,8 +8050,7 @@ ${reason}`,
|
|
|
8169
8050
|
replyState,
|
|
8170
8051
|
// The ledger-derived owed reply, for the runtime's own declaration when
|
|
8171
8052
|
// the agent doesn't call `reply_to` (resolveDeclaredReplyField).
|
|
8172
|
-
owedReplyMessageId: turnContext.owedReplyMessageId ?? null
|
|
8173
|
-
spawnedSubAgent: () => spawnedSubAgent
|
|
8053
|
+
owedReplyMessageId: turnContext.owedReplyMessageId ?? null
|
|
8174
8054
|
});
|
|
8175
8055
|
const usesHttpTurnControl = turnRuntime === "codex" || turnRuntime === "opencode";
|
|
8176
8056
|
let turnControlIntentFetched = false;
|
|
@@ -8186,7 +8066,6 @@ ${reason}`,
|
|
|
8186
8066
|
);
|
|
8187
8067
|
if (intent.ask) {
|
|
8188
8068
|
askState.targetUserId = intent.ask.targetUserId;
|
|
8189
|
-
askState.order = intent.askOrder ?? null;
|
|
8190
8069
|
if (intent.ask.questions && intent.ask.questions.length > 0) {
|
|
8191
8070
|
askState.questions = intent.ask.questions;
|
|
8192
8071
|
askState.question = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.67",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The Cabane Companion (headless): connect a coding 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",
|