@cabane/companion 0.6.218 → 0.6.220
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 +86 -56
- package/dist/runtime.js +39 -32
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1730,7 +1730,7 @@ var init_api = __esm({
|
|
|
1730
1730
|
`/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${agentId}/turn-intent?${q}`
|
|
1731
1731
|
);
|
|
1732
1732
|
}
|
|
1733
|
-
// Commit one outgoing act (
|
|
1733
|
+
// Commit one outgoing act (open_exchange, withdraw_exchange, wake_me, cancel_wake) at
|
|
1734
1734
|
// invocation. The server records the effect and receipt in one transaction;
|
|
1735
1735
|
// an ok:false response is a recorded refusal for the tool to report.
|
|
1736
1736
|
// Retries retain the caller's callId and recover the same receipt, even
|
|
@@ -2277,7 +2277,7 @@ var init_turn_request = __esm({
|
|
|
2277
2277
|
bearer: z8.string(),
|
|
2278
2278
|
activeConversationId: z8.string(),
|
|
2279
2279
|
// Scoped turn-control MCP URL. Codex and opencode mount it as `cabane_companion` with the bearer
|
|
2280
|
-
// and active-conversation header, for
|
|
2280
|
+
// and active-conversation header, for the turn-control tools (`TURN_CONTROL_TOOLS`).
|
|
2281
2281
|
// Claude Code mounts the companion's in-process instance instead. Optional; the companion
|
|
2282
2282
|
// populates it.
|
|
2283
2283
|
turnControlUrl: z8.string().optional(),
|
|
@@ -2572,13 +2572,18 @@ var init_transcript_sink = __esm({
|
|
|
2572
2572
|
});
|
|
2573
2573
|
|
|
2574
2574
|
// packages/agent-runtime/src/turn-control/descriptions.ts
|
|
2575
|
-
var MAX_ASK_QUESTION_BYTES, MAX_ASK_HEADLINE_BYTES, MAX_ASK_ITEMS, MAX_ASK_OPTION_BYTES, ASK_OPTION_NOT_A_STRING, MIN_ASK_OPTIONS, MAX_ASK_OPTIONS, ASK_TARGET_PATTERN, ASK_TARGET_INVALID, MAX_WAKE_NOTE_BYTES, MAX_WITHDRAW_REASON_BYTES, MAX_SKIP_REASON_BYTES, TURN_CONTROL_MCP_SERVER, TURN_CONTROL_TOOLS, TURN_CONTROL_TOOL_DESCRIPTIONS, TURN_CONTROL_FIELD_DESCRIPTIONS;
|
|
2575
|
+
var MAX_ASK_QUESTION_BYTES, MAX_ASK_HEADLINE_BYTES, MAX_ASK_ITEMS, EXCHANGE_KINDS, APPROVAL_OPTIONS, REQUEST_OPTIONS, MAX_EXCHANGE_FILES, MAX_REQUEST_BODY_BYTES, MAX_ASK_OPTION_BYTES, ASK_OPTION_NOT_A_STRING, MIN_ASK_OPTIONS, MAX_ASK_OPTIONS, ASK_TARGET_PATTERN, ASK_TARGET_INVALID, MAX_WAKE_NOTE_BYTES, MAX_WITHDRAW_REASON_BYTES, MAX_SKIP_REASON_BYTES, TURN_CONTROL_MCP_SERVER, TURN_CONTROL_TOOLS, TURN_CONTROL_TOOL_DESCRIPTIONS, TURN_CONTROL_FIELD_DESCRIPTIONS;
|
|
2576
2576
|
var init_descriptions = __esm({
|
|
2577
2577
|
"packages/agent-runtime/src/turn-control/descriptions.ts"() {
|
|
2578
2578
|
"use strict";
|
|
2579
2579
|
MAX_ASK_QUESTION_BYTES = 400;
|
|
2580
2580
|
MAX_ASK_HEADLINE_BYTES = 120;
|
|
2581
2581
|
MAX_ASK_ITEMS = 20;
|
|
2582
|
+
EXCHANGE_KINDS = ["question", "approval", "request"];
|
|
2583
|
+
APPROVAL_OPTIONS = ["Yes", "No"];
|
|
2584
|
+
REQUEST_OPTIONS = ["Done", "Couldn't"];
|
|
2585
|
+
MAX_EXCHANGE_FILES = 5;
|
|
2586
|
+
MAX_REQUEST_BODY_BYTES = 2e3;
|
|
2582
2587
|
MAX_ASK_OPTION_BYTES = 200;
|
|
2583
2588
|
ASK_OPTION_NOT_A_STRING = "An ask option is a plain string label.";
|
|
2584
2589
|
MIN_ASK_OPTIONS = 2;
|
|
@@ -2591,41 +2596,44 @@ var init_descriptions = __esm({
|
|
|
2591
2596
|
TURN_CONTROL_MCP_SERVER = "cabane_companion";
|
|
2592
2597
|
TURN_CONTROL_TOOLS = [
|
|
2593
2598
|
"answer",
|
|
2594
|
-
"ask",
|
|
2595
2599
|
"cancel_wake",
|
|
2600
|
+
"open_exchange",
|
|
2596
2601
|
"skip_turn",
|
|
2597
2602
|
"wake_me",
|
|
2598
|
-
"
|
|
2603
|
+
"withdraw_exchange"
|
|
2599
2604
|
];
|
|
2600
2605
|
TURN_CONTROL_TOOL_DESCRIPTIONS = {
|
|
2601
2606
|
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.`,
|
|
2602
|
-
|
|
2603
|
-
-
|
|
2604
|
-
-
|
|
2605
|
-
-
|
|
2606
|
-
-
|
|
2607
|
-
-
|
|
2608
|
-
-
|
|
2609
|
-
-
|
|
2607
|
+
open_exchange: `Put something in front of one person that comes back with a recorded outcome, then end your turn; don't wait for it. \`targetUserId\` is a person on your roster, by their \`@handle\`; to bring in an agent, name them with \`@handle\` in your reply.
|
|
2608
|
+
- \`kind\`: \`question\` for a choice among courses of action (two included) or free text; \`approval\` for a yes/no on one action you propose; \`request\` for something only the person can do, reported back.
|
|
2609
|
+
- A question is one item (\`headline\`, optional \`question\` body, \`options\`) or \`questions\`, 1\u2013${MAX_ASK_ITEMS} items; never both. An approval or request is \`headline\` and an optional \`question\` body only: its answers are fixed (${APPROVAL_OPTIONS.join(" / ")}; ${REQUEST_OPTIONS.join(" / ")}, output in the note or attachments). A request's body carries the steps and may hold a list and code blocks; every other body is a sentence or two of framing, inline Markdown only.
|
|
2610
|
+
- Decisions that arise together go in ONE call, in the order you want them answered. A second call is a separate exchange, never an edit.
|
|
2611
|
+
- Voice, in headline, body and options: "I" for yourself, never your own name; name everyone else who decides or acts, as on your roster. Never "you", "your", "we", "us", "our" or their contractions, nor "he", "she" or "they" for a named actor. A quotation stays accurate: say who said it.
|
|
2612
|
+
- It must stand alone, since it can arrive first: the reply carries the findings, the exchange the question; don't repeat the reply in it.
|
|
2613
|
+
- Recorded when you call this; delivered when your turn ends, or after 3 minutes if it is still running. Open it as soon as you know. The outcome reaches you on a later turn.
|
|
2614
|
+
- The person sends one response; unanswered items end as Unanswered. A person can dismiss an exchange; you aren't told and nothing wakes you. Check its status before raising it again.
|
|
2615
|
+
- To replace a stale exchange (yours or anyone's here), pass \`replaces: { askId, reason }\`: both land in one write and the person sees the reason. To withdraw without a replacement, use \`withdraw_exchange\`.`,
|
|
2610
2616
|
cancel_wake: `Stand down the wake you have armed in this conversation, now, when you call it. No arguments. Use it when what the wake was to check has already happened or the work it watched is over; an unneeded wake fires into a turn with nothing to do. Your turn context prints the armed wake ("Wake armed: \u2026"); one no longer printed has fired or been dismissed. Nothing armed is a clean no-op. Calls act in order: \`cancel_wake\` then \`wake_me\` leaves the new wake armed. It reaches only your own wake here, never a peer's and never a recurring schedule (\`schedules.*\`).`,
|
|
2611
2617
|
skip_turn: `End your current turn WITHOUT posting a reply. Call this when you've been woken to read a message that genuinely doesn't need a response from you \u2014 a thanks/aside, a question already answered, chatter outside your lane, or something another participant clearly owns. Your turn ends silently: no message bubble is posted. The \`reason\` is a short free-text note for telemetry (e.g. "already answered by cabane", "thanks, nothing to add"). Prefer this over posting a low-value "ok!"/"got it" reply. Don't also write a reply when you skip \u2014 skipping IS the whole turn.`,
|
|
2612
|
-
wake_me: `Wake yourself later: end this turn now and be re-dispatched at a time you pick, with a note to yourself. Use it to wait for a condition you can name (a PR merging, a session limit resetting, an answer landing): arm, end the turn, and you're woken to CHECK and either act or re-arm. Most wakes are short, seconds to a couple of hours. Reach past a few hours only when a person asked for that timing or the wait is pinned to a real external event (a report that runs Mondays); if no one asked and you can't name both what clears the wait and why it takes that long, don't arm it: finish now, or
|
|
2613
|
-
|
|
2618
|
+
wake_me: `Wake yourself later: end this turn now and be re-dispatched at a time you pick, with a note to yourself. Use it to wait for a condition you can name (a PR merging, a session limit resetting, an answer landing): arm, end the turn, and you're woken to CHECK and either act or re-arm. Most wakes are short, seconds to a couple of hours. Reach past a few hours only when a person asked for that timing or the wait is pinned to a real external event (a report that runs Mondays); if no one asked and you can't name both what clears the wait and why it takes that long, don't arm it: finish now, or open an exchange. Pass EXACTLY ONE of \`afterSeconds\` (\`300\` for five minutes) or \`at\` (an ISO-8601 timestamp WITH a zone, e.g. \`2026-07-16T09:00:00-07:00\`, which YOU compute; nothing parses natural-language time). \`note\` becomes the body of the message that re-dispatches you: write the condition to re-check. The wake arms NOW, when you call this; a later \`wake_me\` in the same turn replaces it, \`cancel_wake\` stands it down, and one that comes due mid-turn is delivered after your reply. This is the ONLY way to schedule your own continuation; never a host cron or scheduler. Floor 60s, horizon 14 days, enforced here so you can recompute. Widen the interval as a loop ages (5m \u2192 15m \u2192 1h\u2026); after many re-arms with no other activity you'll be steered to an exchange instead.`,
|
|
2619
|
+
withdraw_exchange: `Withdraw an open exchange \u2014 yours or anyone's in this conversation \u2014 because it is moot, was wrong, or has been overtaken. \`reason\` is required and is shown to the person and, if it wasn't yours, to the agent who opened it. If there is a current one to put in its place, don't call this \u2014 call \`open_exchange\` 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.`
|
|
2614
2620
|
};
|
|
2615
2621
|
TURN_CONTROL_FIELD_DESCRIPTIONS = {
|
|
2616
2622
|
answer: {},
|
|
2617
|
-
|
|
2618
|
-
targetUserId: "The person
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
+
open_exchange: {
|
|
2624
|
+
targetUserId: "The person: their `@handle` from your turn context's roster (preferred), or their user id.",
|
|
2625
|
+
kind: `What the exchange is for; default \`question\`. \`question\`: the person picks among courses of action or types an answer. \`approval\`: a yes/no on one proposed action \u2014 the person answers ${APPROVAL_OPTIONS.join(" or ")}. \`request\`: something only the person can do \u2014 they report ${REQUEST_OPTIONS.join(" or ")}.`,
|
|
2626
|
+
question: `The body beneath the headline \u2014 for a question or approval one or two sentences of framing, NOT a report (up to ${MAX_ASK_QUESTION_BYTES} characters, inline markdown only); for a request the steps, with lists and code blocks allowed (up to ${MAX_REQUEST_BODY_BYTES} characters). With \`questions\`, leave it out.`,
|
|
2627
|
+
headline: `ONE clear, capitalized sentence, up to ${MAX_ASK_HEADLINE_BYTES} characters: for a question or approval, the question ending in \`?\` ("Should Ledger deploy once Mia approves?"); for a request, what to do ("Run these and send me the output"). What the person reads first in For you. Required for an approval or request.`,
|
|
2628
|
+
files: `1\u2013${MAX_EXCHANGE_FILES} workspace paths, drawn as file chips above the body; the person must be able to read each.`,
|
|
2629
|
+
options: `A question's single form only: ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} suggested one-click answers, up to ${MAX_ASK_OPTION_BYTES} characters each. Each option must make sense on its own. Refused on an approval or request.`,
|
|
2630
|
+
questions: `A question only, MULTI form: 1\u2013${MAX_ASK_ITEMS} items at once, when a plan ends with several bounded decisions. Provide EITHER this or \`question\`/\`headline\`/\`options\`, not both.`,
|
|
2623
2631
|
"questions.headline": `The one-sentence question ("Should Ledger deploy once Mia approves?") \u2014 required for each item, up to ${MAX_ASK_HEADLINE_BYTES} characters.`,
|
|
2624
2632
|
"questions.body": `Optional short framing beneath the headline, up to ${MAX_ASK_QUESTION_BYTES} characters, inline markdown only. NOT a report.`,
|
|
2625
2633
|
"questions.options": `Optional ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} one-click answers for this question, up to ${MAX_ASK_OPTION_BYTES} characters each. Each option must make sense on its own.`,
|
|
2626
|
-
replaces: "Withdraw a stale
|
|
2627
|
-
"replaces.askId": "The id of the open
|
|
2628
|
-
"replaces.reason": `Why the old
|
|
2634
|
+
replaces: "Withdraw a stale exchange in this conversation as part of opening this one. Both land in a single write, so the person goes from the old one to the new one and never sees neither or both.",
|
|
2635
|
+
"replaces.askId": "The id of the open exchange to withdraw.",
|
|
2636
|
+
"replaces.reason": `Why the old one no longer stands \u2014 shown to the person on their Done row. Up to ${MAX_WITHDRAW_REASON_BYTES} characters.`
|
|
2629
2637
|
},
|
|
2630
2638
|
cancel_wake: {},
|
|
2631
2639
|
skip_turn: {
|
|
@@ -2636,9 +2644,9 @@ var init_descriptions = __esm({
|
|
|
2636
2644
|
at: "Absolute ISO-8601 timestamp WITH a zone (`Z` or `\xB1HH:MM`), e.g. `2026-07-16T09:00:00-07:00`. YOU compute it from a natural-language phrase using the current datetime in your turn context. Provide EITHER this or `afterSeconds`, not both.",
|
|
2637
2645
|
note: `A note to your future self \u2014 becomes the body of the wake message that re-dispatches you, up to ${MAX_WAKE_NOTE_BYTES} characters. Write the condition to re-check in a sentence or two ("check whether the PR merged"), not a plan. You never need to name yourself: the wake reaches you on its own.`
|
|
2638
2646
|
},
|
|
2639
|
-
|
|
2640
|
-
askId: "The open
|
|
2641
|
-
reason: `Why
|
|
2647
|
+
withdraw_exchange: {
|
|
2648
|
+
askId: "The open exchange to withdraw. Its id rides the conversation's message rows (`ask: { id, status, items }`), and your turn context lists the ones open here.",
|
|
2649
|
+
reason: `Why it no longer stands ("merged via GitHub #1551"). Required, and shown to the person on their Done row \u2014 a bare "withdrawn" tells them nothing. Up to ${MAX_WITHDRAW_REASON_BYTES} characters.`
|
|
2642
2650
|
}
|
|
2643
2651
|
};
|
|
2644
2652
|
}
|
|
@@ -2820,11 +2828,11 @@ var init_prompt_guidance = __esm({
|
|
|
2820
2828
|
init_descriptions();
|
|
2821
2829
|
MEANINGS = {
|
|
2822
2830
|
answer: "declare the text you write next as your answer",
|
|
2823
|
-
|
|
2831
|
+
open_exchange: "put a question, go-ahead or request to a person",
|
|
2824
2832
|
cancel_wake: "cancel your own armed wake in this conversation",
|
|
2825
2833
|
skip_turn: "end without posting a reply",
|
|
2826
2834
|
wake_me: "schedule your own return; when it fires, say what you came back to check",
|
|
2827
|
-
|
|
2835
|
+
withdraw_exchange: "withdraw an open exchange in this conversation with a reason"
|
|
2828
2836
|
};
|
|
2829
2837
|
}
|
|
2830
2838
|
});
|
|
@@ -6224,37 +6232,57 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
6224
6232
|
)
|
|
6225
6233
|
] : [],
|
|
6226
6234
|
tool(
|
|
6227
|
-
|
|
6228
|
-
TURN_CONTROL_TOOL_DESCRIPTIONS.
|
|
6235
|
+
OPEN_EXCHANGE_TOOL,
|
|
6236
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS.open_exchange,
|
|
6229
6237
|
{
|
|
6230
|
-
targetUserId: z15.string().regex(ASK_TARGET_PATTERN, { error: ASK_TARGET_INVALID }).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6238
|
+
targetUserId: z15.string().regex(ASK_TARGET_PATTERN, { error: ASK_TARGET_INVALID }).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange.targetUserId),
|
|
6239
|
+
kind: z15.enum(EXCHANGE_KINDS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange.kind),
|
|
6240
|
+
// The request's cap; the server holds every other kind to its own.
|
|
6241
|
+
question: z15.string().min(1).max(MAX_REQUEST_BODY_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange.question),
|
|
6242
|
+
headline: z15.string().min(1).max(MAX_ASK_HEADLINE_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange.headline),
|
|
6243
|
+
options: z15.array(z15.string({ error: ASK_OPTION_NOT_A_STRING }).min(1).max(MAX_ASK_OPTION_BYTES)).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange.options),
|
|
6234
6244
|
questions: z15.array(
|
|
6235
6245
|
z15.object({
|
|
6236
|
-
headline: z15.string().min(1).max(MAX_ASK_HEADLINE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.
|
|
6237
|
-
body: z15.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.
|
|
6246
|
+
headline: z15.string().min(1).max(MAX_ASK_HEADLINE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange["questions.headline"]),
|
|
6247
|
+
body: z15.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange["questions.body"]),
|
|
6238
6248
|
options: z15.array(
|
|
6239
6249
|
z15.string({ error: ASK_OPTION_NOT_A_STRING }).min(1).max(MAX_ASK_OPTION_BYTES)
|
|
6240
|
-
).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.
|
|
6250
|
+
).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange["questions.options"])
|
|
6241
6251
|
})
|
|
6242
|
-
).min(1).max(MAX_ASK_ITEMS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.
|
|
6252
|
+
).min(1).max(MAX_ASK_ITEMS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange.questions),
|
|
6253
|
+
files: z15.array(z15.string().min(1).max(1024)).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange.files),
|
|
6243
6254
|
replaces: z15.object({
|
|
6244
|
-
askId: z15.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.
|
|
6245
|
-
reason: z15.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.
|
|
6246
|
-
}).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.
|
|
6255
|
+
askId: z15.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange["replaces.askId"]),
|
|
6256
|
+
reason: z15.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange["replaces.reason"])
|
|
6257
|
+
}).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange.replaces)
|
|
6247
6258
|
},
|
|
6248
6259
|
async (args) => {
|
|
6260
|
+
const kind = args.kind ?? "question";
|
|
6249
6261
|
const hasSingle = args.question !== void 0;
|
|
6250
6262
|
const hasArray = args.questions !== void 0 && args.questions.length > 0;
|
|
6263
|
+
if (kind !== "question") {
|
|
6264
|
+
return performAct(acts, {
|
|
6265
|
+
kind: "ask",
|
|
6266
|
+
callId: randomUUID3(),
|
|
6267
|
+
...args.replaces ? { replaces: args.replaces } : {},
|
|
6268
|
+
ask: {
|
|
6269
|
+
targetUserId: args.targetUserId,
|
|
6270
|
+
kind,
|
|
6271
|
+
...args.headline ? { headline: args.headline } : {},
|
|
6272
|
+
...args.question ? { question: args.question } : {},
|
|
6273
|
+
...args.options ? { options: args.options } : {},
|
|
6274
|
+
...args.questions ? { questions: args.questions } : {},
|
|
6275
|
+
...args.files ? { files: args.files } : {}
|
|
6276
|
+
}
|
|
6277
|
+
});
|
|
6278
|
+
}
|
|
6251
6279
|
if (hasSingle && hasArray) {
|
|
6252
6280
|
return toolError(
|
|
6253
6281
|
"Provide either `question` (single) or `questions` (array), not both."
|
|
6254
6282
|
);
|
|
6255
6283
|
}
|
|
6256
|
-
if (!hasSingle && !hasArray) {
|
|
6257
|
-
return toolError("Provide `
|
|
6284
|
+
if (!hasSingle && !hasArray && args.headline === void 0) {
|
|
6285
|
+
return toolError("Provide `headline` (single) or `questions` (array).");
|
|
6258
6286
|
}
|
|
6259
6287
|
return performAct(acts, {
|
|
6260
6288
|
kind: "ask",
|
|
@@ -6268,12 +6296,14 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
6268
6296
|
headline: q.headline,
|
|
6269
6297
|
...q.body ? { body: q.body } : {},
|
|
6270
6298
|
...q.options && q.options.length > 0 ? { options: q.options } : {}
|
|
6271
|
-
}))
|
|
6299
|
+
})),
|
|
6300
|
+
...args.files ? { files: args.files } : {}
|
|
6272
6301
|
} : {
|
|
6273
6302
|
targetUserId: args.targetUserId,
|
|
6274
|
-
question: args.question,
|
|
6303
|
+
...args.question ? { question: args.question } : {},
|
|
6275
6304
|
...args.headline ? { headline: args.headline } : {},
|
|
6276
|
-
...args.options && args.options.length > 0 ? { options: args.options } : {}
|
|
6305
|
+
...args.options && args.options.length > 0 ? { options: args.options } : {},
|
|
6306
|
+
...args.files ? { files: args.files } : {}
|
|
6277
6307
|
}
|
|
6278
6308
|
});
|
|
6279
6309
|
},
|
|
@@ -6316,11 +6346,11 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
6316
6346
|
// reasoning). Not idempotent either: a second call with the same id is
|
|
6317
6347
|
// refused with `ask_not_open`, which is information, not a no-op.
|
|
6318
6348
|
tool(
|
|
6319
|
-
|
|
6320
|
-
TURN_CONTROL_TOOL_DESCRIPTIONS.
|
|
6349
|
+
WITHDRAW_EXCHANGE_TOOL,
|
|
6350
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS.withdraw_exchange,
|
|
6321
6351
|
{
|
|
6322
|
-
askId: z15.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.
|
|
6323
|
-
reason: z15.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.
|
|
6352
|
+
askId: z15.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_exchange.askId),
|
|
6353
|
+
reason: z15.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_exchange.reason)
|
|
6324
6354
|
},
|
|
6325
6355
|
async (args) => performAct(acts, {
|
|
6326
6356
|
kind: "withdraw_ask",
|
|
@@ -6348,7 +6378,7 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
6348
6378
|
]
|
|
6349
6379
|
});
|
|
6350
6380
|
}
|
|
6351
|
-
var COMPANION_LOCAL_MCP_SERVER, COMPANION_LOCAL_TOOL_GLOB, SKIP_TURN_TOOL,
|
|
6381
|
+
var COMPANION_LOCAL_MCP_SERVER, COMPANION_LOCAL_TOOL_GLOB, SKIP_TURN_TOOL, OPEN_EXCHANGE_TOOL, OPEN_EXCHANGE_TOOL_NAME, WITHDRAW_EXCHANGE_TOOL, WITHDRAW_EXCHANGE_TOOL_NAME, ANSWER_TOOL2, WAKE_ME_TOOL, WAKE_ME_TOOL_NAME, CANCEL_WAKE_TOOL, CANCEL_WAKE_TOOL_NAME, TURN_ACT_TIMEOUT_MS;
|
|
6352
6382
|
var init_turn_control_tools = __esm({
|
|
6353
6383
|
"src/turn-control-tools.ts"() {
|
|
6354
6384
|
"use strict";
|
|
@@ -6359,10 +6389,10 @@ var init_turn_control_tools = __esm({
|
|
|
6359
6389
|
COMPANION_LOCAL_MCP_SERVER = TURN_CONTROL_MCP_SERVER;
|
|
6360
6390
|
COMPANION_LOCAL_TOOL_GLOB = `mcp__${COMPANION_LOCAL_MCP_SERVER}__*`;
|
|
6361
6391
|
SKIP_TURN_TOOL = "skip_turn";
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6392
|
+
OPEN_EXCHANGE_TOOL = "open_exchange";
|
|
6393
|
+
OPEN_EXCHANGE_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${OPEN_EXCHANGE_TOOL}`;
|
|
6394
|
+
WITHDRAW_EXCHANGE_TOOL = "withdraw_exchange";
|
|
6395
|
+
WITHDRAW_EXCHANGE_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WITHDRAW_EXCHANGE_TOOL}`;
|
|
6366
6396
|
ANSWER_TOOL2 = "answer";
|
|
6367
6397
|
WAKE_ME_TOOL = "wake_me";
|
|
6368
6398
|
WAKE_ME_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WAKE_ME_TOOL}`;
|
package/dist/runtime.js
CHANGED
|
@@ -1932,7 +1932,7 @@ var CabaneApi = class {
|
|
|
1932
1932
|
`/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${agentId}/turn-intent?${q}`
|
|
1933
1933
|
);
|
|
1934
1934
|
}
|
|
1935
|
-
// Commit one outgoing act (
|
|
1935
|
+
// Commit one outgoing act (open_exchange, withdraw_exchange, wake_me, cancel_wake) at
|
|
1936
1936
|
// invocation. The server records the effect and receipt in one transaction;
|
|
1937
1937
|
// an ok:false response is a recorded refusal for the tool to report.
|
|
1938
1938
|
// Retries retain the caller's callId and recover the same receipt, even
|
|
@@ -2751,7 +2751,7 @@ var turnRequestSchema = z8.object({
|
|
|
2751
2751
|
bearer: z8.string(),
|
|
2752
2752
|
activeConversationId: z8.string(),
|
|
2753
2753
|
// Scoped turn-control MCP URL. Codex and opencode mount it as `cabane_companion` with the bearer
|
|
2754
|
-
// and active-conversation header, for
|
|
2754
|
+
// and active-conversation header, for the turn-control tools (`TURN_CONTROL_TOOLS`).
|
|
2755
2755
|
// Claude Code mounts the companion's in-process instance instead. Optional; the companion
|
|
2756
2756
|
// populates it.
|
|
2757
2757
|
turnControlUrl: z8.string().optional(),
|
|
@@ -2937,6 +2937,10 @@ function truncate(s, max) {
|
|
|
2937
2937
|
var MAX_ASK_QUESTION_BYTES = 400;
|
|
2938
2938
|
var MAX_ASK_HEADLINE_BYTES = 120;
|
|
2939
2939
|
var MAX_ASK_ITEMS = 20;
|
|
2940
|
+
var APPROVAL_OPTIONS = ["Yes", "No"];
|
|
2941
|
+
var REQUEST_OPTIONS = ["Done", "Couldn't"];
|
|
2942
|
+
var MAX_EXCHANGE_FILES = 5;
|
|
2943
|
+
var MAX_REQUEST_BODY_BYTES = 2e3;
|
|
2940
2944
|
var MAX_ASK_OPTION_BYTES = 200;
|
|
2941
2945
|
var MIN_ASK_OPTIONS = 2;
|
|
2942
2946
|
var MAX_ASK_OPTIONS = 4;
|
|
@@ -2946,41 +2950,44 @@ var MAX_SKIP_REASON_BYTES = 500;
|
|
|
2946
2950
|
var TURN_CONTROL_MCP_SERVER = "cabane_companion";
|
|
2947
2951
|
var TURN_CONTROL_TOOLS = [
|
|
2948
2952
|
"answer",
|
|
2949
|
-
"ask",
|
|
2950
2953
|
"cancel_wake",
|
|
2954
|
+
"open_exchange",
|
|
2951
2955
|
"skip_turn",
|
|
2952
2956
|
"wake_me",
|
|
2953
|
-
"
|
|
2957
|
+
"withdraw_exchange"
|
|
2954
2958
|
];
|
|
2955
2959
|
var TURN_CONTROL_TOOL_DESCRIPTIONS = {
|
|
2956
2960
|
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.`,
|
|
2957
|
-
|
|
2958
|
-
-
|
|
2959
|
-
-
|
|
2960
|
-
-
|
|
2961
|
-
-
|
|
2962
|
-
-
|
|
2963
|
-
-
|
|
2964
|
-
-
|
|
2961
|
+
open_exchange: `Put something in front of one person that comes back with a recorded outcome, then end your turn; don't wait for it. \`targetUserId\` is a person on your roster, by their \`@handle\`; to bring in an agent, name them with \`@handle\` in your reply.
|
|
2962
|
+
- \`kind\`: \`question\` for a choice among courses of action (two included) or free text; \`approval\` for a yes/no on one action you propose; \`request\` for something only the person can do, reported back.
|
|
2963
|
+
- A question is one item (\`headline\`, optional \`question\` body, \`options\`) or \`questions\`, 1\u2013${MAX_ASK_ITEMS} items; never both. An approval or request is \`headline\` and an optional \`question\` body only: its answers are fixed (${APPROVAL_OPTIONS.join(" / ")}; ${REQUEST_OPTIONS.join(" / ")}, output in the note or attachments). A request's body carries the steps and may hold a list and code blocks; every other body is a sentence or two of framing, inline Markdown only.
|
|
2964
|
+
- Decisions that arise together go in ONE call, in the order you want them answered. A second call is a separate exchange, never an edit.
|
|
2965
|
+
- Voice, in headline, body and options: "I" for yourself, never your own name; name everyone else who decides or acts, as on your roster. Never "you", "your", "we", "us", "our" or their contractions, nor "he", "she" or "they" for a named actor. A quotation stays accurate: say who said it.
|
|
2966
|
+
- It must stand alone, since it can arrive first: the reply carries the findings, the exchange the question; don't repeat the reply in it.
|
|
2967
|
+
- Recorded when you call this; delivered when your turn ends, or after 3 minutes if it is still running. Open it as soon as you know. The outcome reaches you on a later turn.
|
|
2968
|
+
- The person sends one response; unanswered items end as Unanswered. A person can dismiss an exchange; you aren't told and nothing wakes you. Check its status before raising it again.
|
|
2969
|
+
- To replace a stale exchange (yours or anyone's here), pass \`replaces: { askId, reason }\`: both land in one write and the person sees the reason. To withdraw without a replacement, use \`withdraw_exchange\`.`,
|
|
2965
2970
|
cancel_wake: `Stand down the wake you have armed in this conversation, now, when you call it. No arguments. Use it when what the wake was to check has already happened or the work it watched is over; an unneeded wake fires into a turn with nothing to do. Your turn context prints the armed wake ("Wake armed: \u2026"); one no longer printed has fired or been dismissed. Nothing armed is a clean no-op. Calls act in order: \`cancel_wake\` then \`wake_me\` leaves the new wake armed. It reaches only your own wake here, never a peer's and never a recurring schedule (\`schedules.*\`).`,
|
|
2966
2971
|
skip_turn: `End your current turn WITHOUT posting a reply. Call this when you've been woken to read a message that genuinely doesn't need a response from you \u2014 a thanks/aside, a question already answered, chatter outside your lane, or something another participant clearly owns. Your turn ends silently: no message bubble is posted. The \`reason\` is a short free-text note for telemetry (e.g. "already answered by cabane", "thanks, nothing to add"). Prefer this over posting a low-value "ok!"/"got it" reply. Don't also write a reply when you skip \u2014 skipping IS the whole turn.`,
|
|
2967
|
-
wake_me: `Wake yourself later: end this turn now and be re-dispatched at a time you pick, with a note to yourself. Use it to wait for a condition you can name (a PR merging, a session limit resetting, an answer landing): arm, end the turn, and you're woken to CHECK and either act or re-arm. Most wakes are short, seconds to a couple of hours. Reach past a few hours only when a person asked for that timing or the wait is pinned to a real external event (a report that runs Mondays); if no one asked and you can't name both what clears the wait and why it takes that long, don't arm it: finish now, or
|
|
2968
|
-
|
|
2972
|
+
wake_me: `Wake yourself later: end this turn now and be re-dispatched at a time you pick, with a note to yourself. Use it to wait for a condition you can name (a PR merging, a session limit resetting, an answer landing): arm, end the turn, and you're woken to CHECK and either act or re-arm. Most wakes are short, seconds to a couple of hours. Reach past a few hours only when a person asked for that timing or the wait is pinned to a real external event (a report that runs Mondays); if no one asked and you can't name both what clears the wait and why it takes that long, don't arm it: finish now, or open an exchange. Pass EXACTLY ONE of \`afterSeconds\` (\`300\` for five minutes) or \`at\` (an ISO-8601 timestamp WITH a zone, e.g. \`2026-07-16T09:00:00-07:00\`, which YOU compute; nothing parses natural-language time). \`note\` becomes the body of the message that re-dispatches you: write the condition to re-check. The wake arms NOW, when you call this; a later \`wake_me\` in the same turn replaces it, \`cancel_wake\` stands it down, and one that comes due mid-turn is delivered after your reply. This is the ONLY way to schedule your own continuation; never a host cron or scheduler. Floor 60s, horizon 14 days, enforced here so you can recompute. Widen the interval as a loop ages (5m \u2192 15m \u2192 1h\u2026); after many re-arms with no other activity you'll be steered to an exchange instead.`,
|
|
2973
|
+
withdraw_exchange: `Withdraw an open exchange \u2014 yours or anyone's in this conversation \u2014 because it is moot, was wrong, or has been overtaken. \`reason\` is required and is shown to the person and, if it wasn't yours, to the agent who opened it. If there is a current one to put in its place, don't call this \u2014 call \`open_exchange\` 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.`
|
|
2969
2974
|
};
|
|
2970
2975
|
var TURN_CONTROL_FIELD_DESCRIPTIONS = {
|
|
2971
2976
|
answer: {},
|
|
2972
|
-
|
|
2973
|
-
targetUserId: "The person
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2977
|
+
open_exchange: {
|
|
2978
|
+
targetUserId: "The person: their `@handle` from your turn context's roster (preferred), or their user id.",
|
|
2979
|
+
kind: `What the exchange is for; default \`question\`. \`question\`: the person picks among courses of action or types an answer. \`approval\`: a yes/no on one proposed action \u2014 the person answers ${APPROVAL_OPTIONS.join(" or ")}. \`request\`: something only the person can do \u2014 they report ${REQUEST_OPTIONS.join(" or ")}.`,
|
|
2980
|
+
question: `The body beneath the headline \u2014 for a question or approval one or two sentences of framing, NOT a report (up to ${MAX_ASK_QUESTION_BYTES} characters, inline markdown only); for a request the steps, with lists and code blocks allowed (up to ${MAX_REQUEST_BODY_BYTES} characters). With \`questions\`, leave it out.`,
|
|
2981
|
+
headline: `ONE clear, capitalized sentence, up to ${MAX_ASK_HEADLINE_BYTES} characters: for a question or approval, the question ending in \`?\` ("Should Ledger deploy once Mia approves?"); for a request, what to do ("Run these and send me the output"). What the person reads first in For you. Required for an approval or request.`,
|
|
2982
|
+
files: `1\u2013${MAX_EXCHANGE_FILES} workspace paths, drawn as file chips above the body; the person must be able to read each.`,
|
|
2983
|
+
options: `A question's single form only: ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} suggested one-click answers, up to ${MAX_ASK_OPTION_BYTES} characters each. Each option must make sense on its own. Refused on an approval or request.`,
|
|
2984
|
+
questions: `A question only, MULTI form: 1\u2013${MAX_ASK_ITEMS} items at once, when a plan ends with several bounded decisions. Provide EITHER this or \`question\`/\`headline\`/\`options\`, not both.`,
|
|
2978
2985
|
"questions.headline": `The one-sentence question ("Should Ledger deploy once Mia approves?") \u2014 required for each item, up to ${MAX_ASK_HEADLINE_BYTES} characters.`,
|
|
2979
2986
|
"questions.body": `Optional short framing beneath the headline, up to ${MAX_ASK_QUESTION_BYTES} characters, inline markdown only. NOT a report.`,
|
|
2980
2987
|
"questions.options": `Optional ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} one-click answers for this question, up to ${MAX_ASK_OPTION_BYTES} characters each. Each option must make sense on its own.`,
|
|
2981
|
-
replaces: "Withdraw a stale
|
|
2982
|
-
"replaces.askId": "The id of the open
|
|
2983
|
-
"replaces.reason": `Why the old
|
|
2988
|
+
replaces: "Withdraw a stale exchange in this conversation as part of opening this one. Both land in a single write, so the person goes from the old one to the new one and never sees neither or both.",
|
|
2989
|
+
"replaces.askId": "The id of the open exchange to withdraw.",
|
|
2990
|
+
"replaces.reason": `Why the old one no longer stands \u2014 shown to the person on their Done row. Up to ${MAX_WITHDRAW_REASON_BYTES} characters.`
|
|
2984
2991
|
},
|
|
2985
2992
|
cancel_wake: {},
|
|
2986
2993
|
skip_turn: {
|
|
@@ -2991,9 +2998,9 @@ var TURN_CONTROL_FIELD_DESCRIPTIONS = {
|
|
|
2991
2998
|
at: "Absolute ISO-8601 timestamp WITH a zone (`Z` or `\xB1HH:MM`), e.g. `2026-07-16T09:00:00-07:00`. YOU compute it from a natural-language phrase using the current datetime in your turn context. Provide EITHER this or `afterSeconds`, not both.",
|
|
2992
2999
|
note: `A note to your future self \u2014 becomes the body of the wake message that re-dispatches you, up to ${MAX_WAKE_NOTE_BYTES} characters. Write the condition to re-check in a sentence or two ("check whether the PR merged"), not a plan. You never need to name yourself: the wake reaches you on its own.`
|
|
2993
3000
|
},
|
|
2994
|
-
|
|
2995
|
-
askId: "The open
|
|
2996
|
-
reason: `Why
|
|
3001
|
+
withdraw_exchange: {
|
|
3002
|
+
askId: "The open exchange to withdraw. Its id rides the conversation's message rows (`ask: { id, status, items }`), and your turn context lists the ones open here.",
|
|
3003
|
+
reason: `Why it no longer stands ("merged via GitHub #1551"). Required, and shown to the person on their Done row \u2014 a bare "withdrawn" tells them nothing. Up to ${MAX_WITHDRAW_REASON_BYTES} characters.`
|
|
2997
3004
|
}
|
|
2998
3005
|
};
|
|
2999
3006
|
|
|
@@ -3005,11 +3012,11 @@ import {
|
|
|
3005
3012
|
// packages/agent-runtime/src/turn-control/prompt-guidance.ts
|
|
3006
3013
|
var MEANINGS = {
|
|
3007
3014
|
answer: "declare the text you write next as your answer",
|
|
3008
|
-
|
|
3015
|
+
open_exchange: "put a question, go-ahead or request to a person",
|
|
3009
3016
|
cancel_wake: "cancel your own armed wake in this conversation",
|
|
3010
3017
|
skip_turn: "end without posting a reply",
|
|
3011
3018
|
wake_me: "schedule your own return; when it fires, say what you came back to check",
|
|
3012
|
-
|
|
3019
|
+
withdraw_exchange: "withdraw an open exchange in this conversation with a reason"
|
|
3013
3020
|
};
|
|
3014
3021
|
function turnControlPromptGuidance(prefix) {
|
|
3015
3022
|
const controls = TURN_CONTROL_TOOLS.map((name) => `\`${prefix}${name}\` (${MEANINGS[name]})`);
|
|
@@ -6116,10 +6123,10 @@ import { randomUUID as randomUUID3 } from "crypto";
|
|
|
6116
6123
|
import { z as z15 } from "zod";
|
|
6117
6124
|
var COMPANION_LOCAL_MCP_SERVER = TURN_CONTROL_MCP_SERVER;
|
|
6118
6125
|
var COMPANION_LOCAL_TOOL_GLOB = `mcp__${COMPANION_LOCAL_MCP_SERVER}__*`;
|
|
6119
|
-
var
|
|
6120
|
-
var
|
|
6121
|
-
var
|
|
6122
|
-
var
|
|
6126
|
+
var OPEN_EXCHANGE_TOOL = "open_exchange";
|
|
6127
|
+
var OPEN_EXCHANGE_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${OPEN_EXCHANGE_TOOL}`;
|
|
6128
|
+
var WITHDRAW_EXCHANGE_TOOL = "withdraw_exchange";
|
|
6129
|
+
var WITHDRAW_EXCHANGE_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WITHDRAW_EXCHANGE_TOOL}`;
|
|
6123
6130
|
var WAKE_ME_TOOL = "wake_me";
|
|
6124
6131
|
var WAKE_ME_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WAKE_ME_TOOL}`;
|
|
6125
6132
|
var CANCEL_WAKE_TOOL = "cancel_wake";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.220",
|
|
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",
|