@cabane/companion 0.6.217 → 0.6.219
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.js +80 -56
- package/dist/runtime.js +37 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ Pair this device
|
|
|
29
29
|
Enter this code in Cabane, or press Return to open the page:
|
|
30
30
|
|
|
31
31
|
WXYZ-2345
|
|
32
|
-
https://app.cabane.ai/pair/enter-code
|
|
32
|
+
https://app.cabane.ai/pair/enter-code
|
|
33
33
|
|
|
34
34
|
⠋ Waiting for you to confirm in Cabane… (the code is good for 15 minutes)
|
|
35
35
|
```
|
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,17 @@ 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_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_REQUEST_BODY_BYTES = 2e3;
|
|
2582
2586
|
MAX_ASK_OPTION_BYTES = 200;
|
|
2583
2587
|
ASK_OPTION_NOT_A_STRING = "An ask option is a plain string label.";
|
|
2584
2588
|
MIN_ASK_OPTIONS = 2;
|
|
@@ -2591,41 +2595,43 @@ var init_descriptions = __esm({
|
|
|
2591
2595
|
TURN_CONTROL_MCP_SERVER = "cabane_companion";
|
|
2592
2596
|
TURN_CONTROL_TOOLS = [
|
|
2593
2597
|
"answer",
|
|
2594
|
-
"ask",
|
|
2595
2598
|
"cancel_wake",
|
|
2599
|
+
"open_exchange",
|
|
2596
2600
|
"skip_turn",
|
|
2597
2601
|
"wake_me",
|
|
2598
|
-
"
|
|
2602
|
+
"withdraw_exchange"
|
|
2599
2603
|
];
|
|
2600
2604
|
TURN_CONTROL_TOOL_DESCRIPTIONS = {
|
|
2601
2605
|
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
|
-
-
|
|
2606
|
+
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.
|
|
2607
|
+
- \`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.
|
|
2608
|
+
- 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.
|
|
2609
|
+
- 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.
|
|
2610
|
+
- 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.
|
|
2611
|
+
- 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.
|
|
2612
|
+
- 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.
|
|
2613
|
+
- 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.
|
|
2614
|
+
- 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
2615
|
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
2616
|
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
|
-
|
|
2617
|
+
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.`,
|
|
2618
|
+
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
2619
|
};
|
|
2615
2620
|
TURN_CONTROL_FIELD_DESCRIPTIONS = {
|
|
2616
2621
|
answer: {},
|
|
2617
|
-
|
|
2618
|
-
targetUserId: "The person
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2622
|
+
open_exchange: {
|
|
2623
|
+
targetUserId: "The person: their `@handle` from your turn context's roster (preferred), or their user id.",
|
|
2624
|
+
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 ")}.`,
|
|
2625
|
+
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.`,
|
|
2626
|
+
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.`,
|
|
2627
|
+
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.`,
|
|
2628
|
+
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
2629
|
"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
2630
|
"questions.body": `Optional short framing beneath the headline, up to ${MAX_ASK_QUESTION_BYTES} characters, inline markdown only. NOT a report.`,
|
|
2625
2631
|
"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
|
|
2632
|
+
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.",
|
|
2633
|
+
"replaces.askId": "The id of the open exchange to withdraw.",
|
|
2634
|
+
"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
2635
|
},
|
|
2630
2636
|
cancel_wake: {},
|
|
2631
2637
|
skip_turn: {
|
|
@@ -2636,9 +2642,9 @@ var init_descriptions = __esm({
|
|
|
2636
2642
|
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
2643
|
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
2644
|
},
|
|
2639
|
-
|
|
2640
|
-
askId: "The open
|
|
2641
|
-
reason: `Why
|
|
2645
|
+
withdraw_exchange: {
|
|
2646
|
+
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.",
|
|
2647
|
+
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
2648
|
}
|
|
2643
2649
|
};
|
|
2644
2650
|
}
|
|
@@ -2820,11 +2826,11 @@ var init_prompt_guidance = __esm({
|
|
|
2820
2826
|
init_descriptions();
|
|
2821
2827
|
MEANINGS = {
|
|
2822
2828
|
answer: "declare the text you write next as your answer",
|
|
2823
|
-
|
|
2829
|
+
open_exchange: "put a question, go-ahead or request to a person",
|
|
2824
2830
|
cancel_wake: "cancel your own armed wake in this conversation",
|
|
2825
2831
|
skip_turn: "end without posting a reply",
|
|
2826
2832
|
wake_me: "schedule your own return; when it fires, say what you came back to check",
|
|
2827
|
-
|
|
2833
|
+
withdraw_exchange: "withdraw an open exchange in this conversation with a reason"
|
|
2828
2834
|
};
|
|
2829
2835
|
}
|
|
2830
2836
|
});
|
|
@@ -6224,37 +6230,55 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
6224
6230
|
)
|
|
6225
6231
|
] : [],
|
|
6226
6232
|
tool(
|
|
6227
|
-
|
|
6228
|
-
TURN_CONTROL_TOOL_DESCRIPTIONS.
|
|
6233
|
+
OPEN_EXCHANGE_TOOL,
|
|
6234
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS.open_exchange,
|
|
6229
6235
|
{
|
|
6230
|
-
targetUserId: z15.string().regex(ASK_TARGET_PATTERN, { error: ASK_TARGET_INVALID }).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6236
|
+
targetUserId: z15.string().regex(ASK_TARGET_PATTERN, { error: ASK_TARGET_INVALID }).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange.targetUserId),
|
|
6237
|
+
kind: z15.enum(EXCHANGE_KINDS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange.kind),
|
|
6238
|
+
// The request's cap; the server holds every other kind to its own.
|
|
6239
|
+
question: z15.string().min(1).max(MAX_REQUEST_BODY_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange.question),
|
|
6240
|
+
headline: z15.string().min(1).max(MAX_ASK_HEADLINE_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange.headline),
|
|
6241
|
+
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
6242
|
questions: z15.array(
|
|
6235
6243
|
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.
|
|
6244
|
+
headline: z15.string().min(1).max(MAX_ASK_HEADLINE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange["questions.headline"]),
|
|
6245
|
+
body: z15.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange["questions.body"]),
|
|
6238
6246
|
options: z15.array(
|
|
6239
6247
|
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.
|
|
6248
|
+
).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange["questions.options"])
|
|
6241
6249
|
})
|
|
6242
|
-
).min(1).max(MAX_ASK_ITEMS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.
|
|
6250
|
+
).min(1).max(MAX_ASK_ITEMS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange.questions),
|
|
6243
6251
|
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.
|
|
6252
|
+
askId: z15.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange["replaces.askId"]),
|
|
6253
|
+
reason: z15.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange["replaces.reason"])
|
|
6254
|
+
}).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.open_exchange.replaces)
|
|
6247
6255
|
},
|
|
6248
6256
|
async (args) => {
|
|
6257
|
+
const kind = args.kind ?? "question";
|
|
6249
6258
|
const hasSingle = args.question !== void 0;
|
|
6250
6259
|
const hasArray = args.questions !== void 0 && args.questions.length > 0;
|
|
6260
|
+
if (kind !== "question") {
|
|
6261
|
+
return performAct(acts, {
|
|
6262
|
+
kind: "ask",
|
|
6263
|
+
callId: randomUUID3(),
|
|
6264
|
+
...args.replaces ? { replaces: args.replaces } : {},
|
|
6265
|
+
ask: {
|
|
6266
|
+
targetUserId: args.targetUserId,
|
|
6267
|
+
kind,
|
|
6268
|
+
...args.headline ? { headline: args.headline } : {},
|
|
6269
|
+
...args.question ? { question: args.question } : {},
|
|
6270
|
+
...args.options ? { options: args.options } : {},
|
|
6271
|
+
...args.questions ? { questions: args.questions } : {}
|
|
6272
|
+
}
|
|
6273
|
+
});
|
|
6274
|
+
}
|
|
6251
6275
|
if (hasSingle && hasArray) {
|
|
6252
6276
|
return toolError(
|
|
6253
6277
|
"Provide either `question` (single) or `questions` (array), not both."
|
|
6254
6278
|
);
|
|
6255
6279
|
}
|
|
6256
|
-
if (!hasSingle && !hasArray) {
|
|
6257
|
-
return toolError("Provide `
|
|
6280
|
+
if (!hasSingle && !hasArray && args.headline === void 0) {
|
|
6281
|
+
return toolError("Provide `headline` (single) or `questions` (array).");
|
|
6258
6282
|
}
|
|
6259
6283
|
return performAct(acts, {
|
|
6260
6284
|
kind: "ask",
|
|
@@ -6271,7 +6295,7 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
6271
6295
|
}))
|
|
6272
6296
|
} : {
|
|
6273
6297
|
targetUserId: args.targetUserId,
|
|
6274
|
-
question: args.question,
|
|
6298
|
+
...args.question ? { question: args.question } : {},
|
|
6275
6299
|
...args.headline ? { headline: args.headline } : {},
|
|
6276
6300
|
...args.options && args.options.length > 0 ? { options: args.options } : {}
|
|
6277
6301
|
}
|
|
@@ -6316,11 +6340,11 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
6316
6340
|
// reasoning). Not idempotent either: a second call with the same id is
|
|
6317
6341
|
// refused with `ask_not_open`, which is information, not a no-op.
|
|
6318
6342
|
tool(
|
|
6319
|
-
|
|
6320
|
-
TURN_CONTROL_TOOL_DESCRIPTIONS.
|
|
6343
|
+
WITHDRAW_EXCHANGE_TOOL,
|
|
6344
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS.withdraw_exchange,
|
|
6321
6345
|
{
|
|
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.
|
|
6346
|
+
askId: z15.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_exchange.askId),
|
|
6347
|
+
reason: z15.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_exchange.reason)
|
|
6324
6348
|
},
|
|
6325
6349
|
async (args) => performAct(acts, {
|
|
6326
6350
|
kind: "withdraw_ask",
|
|
@@ -6348,7 +6372,7 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
6348
6372
|
]
|
|
6349
6373
|
});
|
|
6350
6374
|
}
|
|
6351
|
-
var COMPANION_LOCAL_MCP_SERVER, COMPANION_LOCAL_TOOL_GLOB, SKIP_TURN_TOOL,
|
|
6375
|
+
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
6376
|
var init_turn_control_tools = __esm({
|
|
6353
6377
|
"src/turn-control-tools.ts"() {
|
|
6354
6378
|
"use strict";
|
|
@@ -6359,10 +6383,10 @@ var init_turn_control_tools = __esm({
|
|
|
6359
6383
|
COMPANION_LOCAL_MCP_SERVER = TURN_CONTROL_MCP_SERVER;
|
|
6360
6384
|
COMPANION_LOCAL_TOOL_GLOB = `mcp__${COMPANION_LOCAL_MCP_SERVER}__*`;
|
|
6361
6385
|
SKIP_TURN_TOOL = "skip_turn";
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6386
|
+
OPEN_EXCHANGE_TOOL = "open_exchange";
|
|
6387
|
+
OPEN_EXCHANGE_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${OPEN_EXCHANGE_TOOL}`;
|
|
6388
|
+
WITHDRAW_EXCHANGE_TOOL = "withdraw_exchange";
|
|
6389
|
+
WITHDRAW_EXCHANGE_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WITHDRAW_EXCHANGE_TOOL}`;
|
|
6366
6390
|
ANSWER_TOOL2 = "answer";
|
|
6367
6391
|
WAKE_ME_TOOL = "wake_me";
|
|
6368
6392
|
WAKE_ME_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WAKE_ME_TOOL}`;
|
|
@@ -10083,7 +10107,7 @@ async function pairDevice(opts, deps = {}) {
|
|
|
10083
10107
|
const keys = interactive ? watchReturn({
|
|
10084
10108
|
onReturn: () => {
|
|
10085
10109
|
if (!current) return;
|
|
10086
|
-
const link = current.
|
|
10110
|
+
const link = current.verificationUri;
|
|
10087
10111
|
void open2(link).then((opened) => {
|
|
10088
10112
|
if (!opened)
|
|
10089
10113
|
spinner.message(
|
|
@@ -10112,7 +10136,7 @@ async function pairDevice(opts, deps = {}) {
|
|
|
10112
10136
|
}
|
|
10113
10137
|
current = code;
|
|
10114
10138
|
const minutes = Math.max(1, Math.round(code.expiresIn / 60));
|
|
10115
|
-
const codeLines = ["", ` ${code.userCode}`, ` ${code.
|
|
10139
|
+
const codeLines = ["", ` ${code.userCode}`, ` ${code.verificationUri}`, ""];
|
|
10116
10140
|
if (attempt === 1) {
|
|
10117
10141
|
ui.step([
|
|
10118
10142
|
"Pair this device",
|
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,9 @@ 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_REQUEST_BODY_BYTES = 2e3;
|
|
2940
2943
|
var MAX_ASK_OPTION_BYTES = 200;
|
|
2941
2944
|
var MIN_ASK_OPTIONS = 2;
|
|
2942
2945
|
var MAX_ASK_OPTIONS = 4;
|
|
@@ -2946,41 +2949,43 @@ var MAX_SKIP_REASON_BYTES = 500;
|
|
|
2946
2949
|
var TURN_CONTROL_MCP_SERVER = "cabane_companion";
|
|
2947
2950
|
var TURN_CONTROL_TOOLS = [
|
|
2948
2951
|
"answer",
|
|
2949
|
-
"ask",
|
|
2950
2952
|
"cancel_wake",
|
|
2953
|
+
"open_exchange",
|
|
2951
2954
|
"skip_turn",
|
|
2952
2955
|
"wake_me",
|
|
2953
|
-
"
|
|
2956
|
+
"withdraw_exchange"
|
|
2954
2957
|
];
|
|
2955
2958
|
var TURN_CONTROL_TOOL_DESCRIPTIONS = {
|
|
2956
2959
|
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
|
-
-
|
|
2960
|
+
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.
|
|
2961
|
+
- \`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.
|
|
2962
|
+
- 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.
|
|
2963
|
+
- 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.
|
|
2964
|
+
- 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.
|
|
2965
|
+
- 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.
|
|
2966
|
+
- 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.
|
|
2967
|
+
- 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.
|
|
2968
|
+
- 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
2969
|
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
2970
|
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
|
-
|
|
2971
|
+
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.`,
|
|
2972
|
+
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
2973
|
};
|
|
2970
2974
|
var TURN_CONTROL_FIELD_DESCRIPTIONS = {
|
|
2971
2975
|
answer: {},
|
|
2972
|
-
|
|
2973
|
-
targetUserId: "The person
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2976
|
+
open_exchange: {
|
|
2977
|
+
targetUserId: "The person: their `@handle` from your turn context's roster (preferred), or their user id.",
|
|
2978
|
+
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 ")}.`,
|
|
2979
|
+
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.`,
|
|
2980
|
+
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.`,
|
|
2981
|
+
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.`,
|
|
2982
|
+
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
2983
|
"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
2984
|
"questions.body": `Optional short framing beneath the headline, up to ${MAX_ASK_QUESTION_BYTES} characters, inline markdown only. NOT a report.`,
|
|
2980
2985
|
"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
|
|
2986
|
+
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.",
|
|
2987
|
+
"replaces.askId": "The id of the open exchange to withdraw.",
|
|
2988
|
+
"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
2989
|
},
|
|
2985
2990
|
cancel_wake: {},
|
|
2986
2991
|
skip_turn: {
|
|
@@ -2991,9 +2996,9 @@ var TURN_CONTROL_FIELD_DESCRIPTIONS = {
|
|
|
2991
2996
|
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
2997
|
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
2998
|
},
|
|
2994
|
-
|
|
2995
|
-
askId: "The open
|
|
2996
|
-
reason: `Why
|
|
2999
|
+
withdraw_exchange: {
|
|
3000
|
+
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.",
|
|
3001
|
+
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
3002
|
}
|
|
2998
3003
|
};
|
|
2999
3004
|
|
|
@@ -3005,11 +3010,11 @@ import {
|
|
|
3005
3010
|
// packages/agent-runtime/src/turn-control/prompt-guidance.ts
|
|
3006
3011
|
var MEANINGS = {
|
|
3007
3012
|
answer: "declare the text you write next as your answer",
|
|
3008
|
-
|
|
3013
|
+
open_exchange: "put a question, go-ahead or request to a person",
|
|
3009
3014
|
cancel_wake: "cancel your own armed wake in this conversation",
|
|
3010
3015
|
skip_turn: "end without posting a reply",
|
|
3011
3016
|
wake_me: "schedule your own return; when it fires, say what you came back to check",
|
|
3012
|
-
|
|
3017
|
+
withdraw_exchange: "withdraw an open exchange in this conversation with a reason"
|
|
3013
3018
|
};
|
|
3014
3019
|
function turnControlPromptGuidance(prefix) {
|
|
3015
3020
|
const controls = TURN_CONTROL_TOOLS.map((name) => `\`${prefix}${name}\` (${MEANINGS[name]})`);
|
|
@@ -6116,10 +6121,10 @@ import { randomUUID as randomUUID3 } from "crypto";
|
|
|
6116
6121
|
import { z as z15 } from "zod";
|
|
6117
6122
|
var COMPANION_LOCAL_MCP_SERVER = TURN_CONTROL_MCP_SERVER;
|
|
6118
6123
|
var COMPANION_LOCAL_TOOL_GLOB = `mcp__${COMPANION_LOCAL_MCP_SERVER}__*`;
|
|
6119
|
-
var
|
|
6120
|
-
var
|
|
6121
|
-
var
|
|
6122
|
-
var
|
|
6124
|
+
var OPEN_EXCHANGE_TOOL = "open_exchange";
|
|
6125
|
+
var OPEN_EXCHANGE_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${OPEN_EXCHANGE_TOOL}`;
|
|
6126
|
+
var WITHDRAW_EXCHANGE_TOOL = "withdraw_exchange";
|
|
6127
|
+
var WITHDRAW_EXCHANGE_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WITHDRAW_EXCHANGE_TOOL}`;
|
|
6123
6128
|
var WAKE_ME_TOOL = "wake_me";
|
|
6124
6129
|
var WAKE_ME_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WAKE_ME_TOOL}`;
|
|
6125
6130
|
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.219",
|
|
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",
|