@cabane/companion 0.6.83 → 0.6.85
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 +83 -55
- package/dist/runtime.js +83 -55
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -7568,6 +7568,62 @@ import { existsSync as existsSync10 } from "fs";
|
|
|
7568
7568
|
|
|
7569
7569
|
// src/turn-control-tools.ts
|
|
7570
7570
|
import { randomUUID } from "crypto";
|
|
7571
|
+
|
|
7572
|
+
// packages/agent-runtime/src/turn-control/descriptions.ts
|
|
7573
|
+
var MAX_ASK_QUESTION_BYTES = 400;
|
|
7574
|
+
var MAX_ASK_HEADLINE_BYTES = 120;
|
|
7575
|
+
var MAX_ASK_ITEMS = 20;
|
|
7576
|
+
var MAX_ASK_OPTION_BYTES = 200;
|
|
7577
|
+
var MAX_WAKE_NOTE_BYTES = 2e3;
|
|
7578
|
+
var MAX_WITHDRAW_REASON_BYTES = 400;
|
|
7579
|
+
var MAX_SEND_MESSAGE_BYTES = 65536;
|
|
7580
|
+
var MAX_SKIP_REASON_BYTES = 500;
|
|
7581
|
+
var TURN_CONTROL_TOOL_DESCRIPTIONS = {
|
|
7582
|
+
ask: `Ask a HUMAN a structured question (or a short LIST of them) you need answered to continue, then END your turn \u2014 don't wait for the reply. Use it when you genuinely can't proceed without a person's input (a decision only they can make, a missing fact). Pass \`targetUserId\` (a workspace member's user id \u2014 every person's id is on the roster in your turn context). Two forms: a SINGLE question \u2014 a \`headline\` (the actual question as one clear, capitalized sentence ending in \`?\`, "Do we go to prod?") plus a short \`question\` body for the framing the headline can't hold \u2014 OR, when a plan ends with SEVERAL bounded decisions at once, a \`questions\` array of 1\u2013${MAX_ASK_ITEMS} items, each \`{ headline, body?, options? }\`. Provide EITHER \`question\` (single) or \`questions\` (array), never both. **Several decisions landing together are ONE call with \`questions\`** \u2014 never cram the extras into prose, drop all but one, or fire an ask each: a second \`ask\` is a second inbox item competing with the first, never an edit to it. Each question keeps the same form: a one-sentence \`headline\`, a short \`body\` frame (NOT a report \u2014 your status, links and detail go in your REPLY; inline markdown only, no lists or headings), and 2\u20134 \`options\` when the answer is a bounded choice \u2014 for a yes/no go-ahead always pass them, so it's one click, not a typed reply. The ask is a first-class attention item aimed at that person; your final reply carries the CONTEXT, the ask carries the QUESTION(S). It is written and delivered THE MOMENT you call this \u2014 the person sees it in their inbox while your turn is still running \u2014 so call it once you know the question. The human sends ONE response: supplied answers are recorded, omissions become terminally Unanswered. Ask only when blocked \u2014 never ceremonially. \`replaces\` withdraws a stale ask of yours (or anyone's) in this conversation in the same write, with the reason shown to the person. Targets a human only; to hand work to another AGENT use send/dispatch instead.`,
|
|
7583
|
+
cancel_wake: `Stand down your own wake \u2014 the inverse of \`wake_me\`. No arguments: it cancels the wake you have armed in this conversation, NOW, when you call it. Reach for it when the thing you armed a wake to check has already happened, or the work it was watching is over \u2014 an armed wake you no longer need fires into a turn with nothing to do, and a supervision loop with no off switch is one you can only end by leaving it running. Whether you have one armed is printed in your turn context ("Wake armed: \u2026"); calling this with nothing armed is a clean no-op, no error. Each call acts as it arrives: \`cancel_wake\` then \`wake_me\` leaves the NEW wake armed; \`wake_me\` then \`cancel_wake\` leaves nothing armed. It reaches only your own wake in this conversation \u2014 never a peer's, and never a recurring schedule (those are \`schedules.*\`).`,
|
|
7584
|
+
reply_to: `Declare which addressed ask your final response answers. Pass the \`messageId\` shown as the owed reply id in your turn context, then finish your response normally. This records lineage only: it does not send another message. The server verifies that this agent owes that ask in this conversation; one reply per turn \u2014 a later call replaces the earlier declaration.`,
|
|
7585
|
+
send: `Send one addressed message to another agent in THIS conversation. Pass the peer's \`agentId\` and the complete \`message\` they should act on. The message is WRITTEN the moment you call this \u2014 durable, in the conversation's queue, in the order you called \u2014 but it is DELIVERED only after your own turn ends: the peer is not running yet and cannot answer you inside this turn. Each call is one message; two calls are two messages. Writing \`@handle\` in prose dispatches nobody. An unknown, deactivated, or self target is refused here, in the result. A handoff to a DIFFERENT conversation is \`conversations.create\` / \`conversations.post\` with their \`dispatch\` field instead. Never use send to hand an answer back to the requester \u2014 declare that with reply_to.`,
|
|
7586
|
+
skip_turn: `End your current turn WITHOUT posting a reply. Call this when you've been dispatched but the message genuinely doesn't need a response from you \u2014 a thanks/aside, a question already answered, chatter outside your lane, or a pile-on where someone else has it. 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.`,
|
|
7587
|
+
wake_me: `Wake yourself later \u2014 end this turn now and be re-dispatched at a time you pick, with a note you write to yourself. Use it for "wait until X": when the thing you need hasn't happened yet (a PR isn't merged, a human hasn't answered), arm a wake, end your turn, and you're woken later to CHECK \u2014 read the workspace, and either act or re-arm. Ground the delay before you arm it. Almost every wake is short \u2014 seconds to a couple of hours \u2014 waiting on a condition you can name: a session limit resetting, a PR merging. Reach past a few hours only when (a) a human asked for that timing, or (b) the wait is pinned to a real external event you can name \u2014 a report that only runs Mondays, a known reset time. A speculative far-future check-in you invented yourself is the one thing not to arm: if no one asked and you can't name both what clears the wait and why it takes that long, don't arm it \u2014 finish now, or raise an \`ask\`. Pass EXACTLY ONE of \`afterSeconds\` (a relative delay \u2014 \`300\` for five minutes) or \`at\` (an absolute ISO-8601 timestamp WITH a zone, e.g. \`2026-07-16T09:00:00-07:00\` \u2014 YOU compute it; the system never parses natural-language time). \`note\` is a message to your future self \u2014 it becomes the body of the wake message that re-dispatches you, so write the condition to re-check ("check whether CT441 merged yet"). The wake arms NOW, when you call this, and the delay counts from this call; a later \`wake_me\` in the same turn replaces it, and \`cancel_wake\` stands it down. A wake that comes due while your turn is still running waits and is delivered after your reply. This is the ONLY sanctioned way to schedule your own continuation; never reach for a host cron/scheduler. Guardrails: at least 60s out, at most 14 days \u2014 a wake that can't be armed returns an error here, in this call, so you can recompute; widen the interval as a loop ages (5m \u2192 15m \u2192 1h\u2026) rather than hammering; after many consecutive re-arms with no other activity you'll be steered to raise an \`ask\` to the human instead.`,
|
|
7588
|
+
withdraw_ask: `Withdraw an open ask \u2014 yours or anyone's in this conversation \u2014 because the question is moot, was wrong, or has been overtaken. \`reason\` is required and is shown to the person and, if it wasn't your ask, to the agent who asked it. If there is a current question to put in its place, don't call this \u2014 call \`ask\` with \`replaces\` so the two land as one and the person never sees neither or both. Never withdraw to hurry an answer; to act on a deadline, arm a \`wake_me\` and decide then.`
|
|
7589
|
+
};
|
|
7590
|
+
var TURN_CONTROL_FIELD_DESCRIPTIONS = {
|
|
7591
|
+
ask: {
|
|
7592
|
+
targetUserId: "The workspace member (human) to ask \u2014 a user id, from your turn context's roster.",
|
|
7593
|
+
question: `SINGLE-question form: a short body \u2014 one or two sentences of framing the headline can't hold. NOT a report (up to ${MAX_ASK_QUESTION_BYTES} characters, inline markdown only). Provide EITHER this or \`questions\`, not both. Put the crisp one-sentence question in \`headline\`.`,
|
|
7594
|
+
headline: `SINGLE-question form: the question itself as ONE clear, capitalized sentence ending in \`?\` ("Do we go to prod?"), up to ${MAX_ASK_HEADLINE_BYTES} characters. What the human reads first in the inbox and the chip \u2014 one scannable question, no elaboration (that goes in \`question\`). Strongly encouraged.`,
|
|
7595
|
+
options: `SINGLE-question form: optional 2\u20134 suggested one-click answers, up to ${MAX_ASK_OPTION_BYTES} characters each.`,
|
|
7596
|
+
questions: `MULTI-question form: 1\u2013${MAX_ASK_ITEMS} questions to ask at once, when a plan ends with several bounded decisions. Provide EITHER this or \`question\`/\`headline\`/\`options\`, not both.`,
|
|
7597
|
+
"questions.headline": `The one-sentence question ("Do we go to prod?") \u2014 required for each item, up to ${MAX_ASK_HEADLINE_BYTES} characters.`,
|
|
7598
|
+
"questions.body": `Optional short framing beneath the headline, up to ${MAX_ASK_QUESTION_BYTES} characters. NOT a report.`,
|
|
7599
|
+
"questions.options": `Optional 2\u20134 one-click answers for this question, up to ${MAX_ASK_OPTION_BYTES} characters each.`,
|
|
7600
|
+
replaces: "Withdraw a stale ask in this conversation as part of raising this one. Both land in a single write, so the person goes from the old question to the new one and never sees neither or both.",
|
|
7601
|
+
"replaces.askId": "The id of the open ask to withdraw.",
|
|
7602
|
+
"replaces.reason": `Why the old question is no longer the question \u2014 shown to the person on their Done row. Up to ${MAX_WITHDRAW_REASON_BYTES} characters.`
|
|
7603
|
+
},
|
|
7604
|
+
cancel_wake: {},
|
|
7605
|
+
reply_to: {
|
|
7606
|
+
messageId: "The owed ask message id from this turn context."
|
|
7607
|
+
},
|
|
7608
|
+
send: {
|
|
7609
|
+
agentId: "The peer agent to address \u2014 a workspace agent id, from your turn context's roster.",
|
|
7610
|
+
message: `The complete new request the peer should receive and act on. Up to ${MAX_SEND_MESSAGE_BYTES} characters.`
|
|
7611
|
+
},
|
|
7612
|
+
skip_turn: {
|
|
7613
|
+
reason: `Short reason you are declining \u2014 used for telemetry/debugging, never posted. Up to ${MAX_SKIP_REASON_BYTES} characters.`
|
|
7614
|
+
},
|
|
7615
|
+
wake_me: {
|
|
7616
|
+
afterSeconds: "Relative delay in seconds from NOW \u2014 this call (e.g. 300 = five minutes). Provide EITHER this or `at`, not both. Floor 60s, horizon 14 days \u2014 enforced server-side.",
|
|
7617
|
+
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.",
|
|
7618
|
+
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 ("check whether the PR merged").`
|
|
7619
|
+
},
|
|
7620
|
+
withdraw_ask: {
|
|
7621
|
+
askId: "The open ask to withdraw. Ask ids ride the conversation's message rows (`ask: { id, status, items }`), and your turn context lists the ones you have open.",
|
|
7622
|
+
reason: `Why this question is no longer the question ("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.`
|
|
7623
|
+
}
|
|
7624
|
+
};
|
|
7625
|
+
|
|
7626
|
+
// src/turn-control-tools.ts
|
|
7571
7627
|
import { z as z13 } from "zod";
|
|
7572
7628
|
var COMPANION_LOCAL_MCP_SERVER = "cabane_companion";
|
|
7573
7629
|
var SEND_TOOL = "send";
|
|
@@ -7578,10 +7634,8 @@ var ASK_TOOL = "ask";
|
|
|
7578
7634
|
var ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${ASK_TOOL}`;
|
|
7579
7635
|
var WITHDRAW_ASK_TOOL = "withdraw_ask";
|
|
7580
7636
|
var WITHDRAW_ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WITHDRAW_ASK_TOOL}`;
|
|
7581
|
-
var MAX_WITHDRAW_REASON = 400;
|
|
7582
7637
|
var REPLY_TO_TOOL = "reply_to";
|
|
7583
7638
|
var REPLY_TO_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${REPLY_TO_TOOL}`;
|
|
7584
|
-
var MAX_ASK_ITEMS = 20;
|
|
7585
7639
|
var WAKE_ME_TOOL = "wake_me";
|
|
7586
7640
|
var WAKE_ME_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WAKE_ME_TOOL}`;
|
|
7587
7641
|
var CANCEL_WAKE_TOOL = "cancel_wake";
|
|
@@ -7639,12 +7693,10 @@ function createTurnControlMcpServer(acts, skipState, replyState) {
|
|
|
7639
7693
|
tools: [
|
|
7640
7694
|
tool(
|
|
7641
7695
|
SEND_TOOL,
|
|
7642
|
-
|
|
7696
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS.send,
|
|
7643
7697
|
{
|
|
7644
|
-
agentId: z13.string().uuid().describe(
|
|
7645
|
-
|
|
7646
|
-
),
|
|
7647
|
-
message: z13.string().min(1).max(65536).describe("The complete new request the peer should receive and act on.")
|
|
7698
|
+
agentId: z13.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.send.agentId),
|
|
7699
|
+
message: z13.string().min(1).max(MAX_SEND_MESSAGE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.send.message)
|
|
7648
7700
|
},
|
|
7649
7701
|
async (args) => performAct(acts, {
|
|
7650
7702
|
kind: "send",
|
|
@@ -7657,9 +7709,9 @@ function createTurnControlMcpServer(acts, skipState, replyState) {
|
|
|
7657
7709
|
...replyState ? [
|
|
7658
7710
|
tool(
|
|
7659
7711
|
REPLY_TO_TOOL,
|
|
7660
|
-
|
|
7712
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS.reply_to,
|
|
7661
7713
|
{
|
|
7662
|
-
messageId: z13.string().uuid().describe(
|
|
7714
|
+
messageId: z13.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.reply_to.messageId)
|
|
7663
7715
|
},
|
|
7664
7716
|
async (args) => {
|
|
7665
7717
|
replyState.answersMessageId = args.messageId;
|
|
@@ -7678,9 +7730,9 @@ function createTurnControlMcpServer(acts, skipState, replyState) {
|
|
|
7678
7730
|
...skipState ? [
|
|
7679
7731
|
tool(
|
|
7680
7732
|
SKIP_TURN_TOOL,
|
|
7681
|
-
|
|
7733
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS.skip_turn,
|
|
7682
7734
|
{
|
|
7683
|
-
reason: z13.string().min(1).max(
|
|
7735
|
+
reason: z13.string().min(1).max(MAX_SKIP_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.skip_turn.reason)
|
|
7684
7736
|
},
|
|
7685
7737
|
async (args) => {
|
|
7686
7738
|
skipState.skipped = true;
|
|
@@ -7694,37 +7746,23 @@ function createTurnControlMcpServer(acts, skipState, replyState) {
|
|
|
7694
7746
|
] : [],
|
|
7695
7747
|
tool(
|
|
7696
7748
|
ASK_TOOL,
|
|
7697
|
-
|
|
7749
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS.ask,
|
|
7698
7750
|
{
|
|
7699
|
-
targetUserId: z13.string().uuid().describe(
|
|
7700
|
-
|
|
7701
|
-
),
|
|
7702
|
-
|
|
7703
|
-
"SINGLE-question form: a short body \u2014 one or two sentences of framing the headline can't hold. NOT a report (capped, inline markdown only). Provide EITHER this or `questions`, not both. Put the crisp one-sentence question in `headline`."
|
|
7704
|
-
),
|
|
7705
|
-
headline: z13.string().min(1).max(120).optional().describe(
|
|
7706
|
-
'SINGLE-question form: the question itself as ONE clear, capitalized sentence ending in `?` ("Do we go to prod?"). What the human reads first in the inbox and the chip \u2014 one scannable question, no elaboration (that goes in `question`). Strongly encouraged.'
|
|
7707
|
-
),
|
|
7708
|
-
options: z13.array(z13.string().min(1).max(200)).min(2).max(4).optional().describe("SINGLE-question form: optional 2\u20134 suggested one-click answers."),
|
|
7751
|
+
targetUserId: z13.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.targetUserId),
|
|
7752
|
+
question: z13.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.question),
|
|
7753
|
+
headline: z13.string().min(1).max(MAX_ASK_HEADLINE_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.headline),
|
|
7754
|
+
options: z13.array(z13.string().min(1).max(MAX_ASK_OPTION_BYTES)).min(2).max(4).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.options),
|
|
7709
7755
|
questions: z13.array(
|
|
7710
7756
|
z13.object({
|
|
7711
|
-
headline: z13.string().min(1).max(
|
|
7712
|
-
|
|
7713
|
-
)
|
|
7714
|
-
body: z13.string().min(1).max(400).optional().describe("Optional short framing beneath the headline. NOT a report."),
|
|
7715
|
-
options: z13.array(z13.string().min(1).max(200)).min(2).max(4).optional().describe("Optional 2\u20134 one-click answers for this question.")
|
|
7757
|
+
headline: z13.string().min(1).max(MAX_ASK_HEADLINE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.headline"]),
|
|
7758
|
+
body: z13.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.body"]),
|
|
7759
|
+
options: z13.array(z13.string().min(1).max(MAX_ASK_OPTION_BYTES)).min(2).max(4).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options"])
|
|
7716
7760
|
})
|
|
7717
|
-
).min(1).max(MAX_ASK_ITEMS).optional().describe(
|
|
7718
|
-
`MULTI-question form: 1\u2013${MAX_ASK_ITEMS} questions to ask at once, when a plan ends with several bounded decisions. Provide EITHER this or \`question\`/\`headline\`/\`options\`, not both.`
|
|
7719
|
-
),
|
|
7761
|
+
).min(1).max(MAX_ASK_ITEMS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.questions),
|
|
7720
7762
|
replaces: z13.object({
|
|
7721
|
-
askId: z13.string().uuid().describe("
|
|
7722
|
-
reason: z13.string().min(1).max(
|
|
7723
|
-
|
|
7724
|
-
)
|
|
7725
|
-
}).optional().describe(
|
|
7726
|
-
"Withdraw a stale ask in this conversation as part of raising this one. Both land in a single write, so the person goes from the old question to the new one and never sees neither or both."
|
|
7727
|
-
)
|
|
7763
|
+
askId: z13.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.askId"]),
|
|
7764
|
+
reason: z13.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.reason"])
|
|
7765
|
+
}).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.replaces)
|
|
7728
7766
|
},
|
|
7729
7767
|
async (args) => {
|
|
7730
7768
|
const hasSingle = args.question !== void 0;
|
|
@@ -7762,17 +7800,11 @@ function createTurnControlMcpServer(acts, skipState, replyState) {
|
|
|
7762
7800
|
),
|
|
7763
7801
|
tool(
|
|
7764
7802
|
WAKE_ME_TOOL,
|
|
7765
|
-
|
|
7803
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS.wake_me,
|
|
7766
7804
|
{
|
|
7767
|
-
afterSeconds: z13.number().int().positive().optional().describe(
|
|
7768
|
-
|
|
7769
|
-
)
|
|
7770
|
-
at: z13.string().datetime({ offset: true }).optional().describe(
|
|
7771
|
-
"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."
|
|
7772
|
-
),
|
|
7773
|
-
note: z13.string().min(1).max(2e3).describe(
|
|
7774
|
-
'A note to your future self \u2014 becomes the body of the wake message that re-dispatches you. Write the condition to re-check ("check whether the PR merged").'
|
|
7775
|
-
)
|
|
7805
|
+
afterSeconds: z13.number().int().positive().optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.afterSeconds),
|
|
7806
|
+
at: z13.string().datetime({ offset: true }).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.at),
|
|
7807
|
+
note: z13.string().min(1).max(MAX_WAKE_NOTE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.note)
|
|
7776
7808
|
},
|
|
7777
7809
|
async (args) => {
|
|
7778
7810
|
const hasAfter = args.afterSeconds !== void 0;
|
|
@@ -7804,14 +7836,10 @@ function createTurnControlMcpServer(acts, skipState, replyState) {
|
|
|
7804
7836
|
// refused with `ask_not_open`, which is information, not a no-op.
|
|
7805
7837
|
tool(
|
|
7806
7838
|
WITHDRAW_ASK_TOOL,
|
|
7807
|
-
|
|
7839
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS.withdraw_ask,
|
|
7808
7840
|
{
|
|
7809
|
-
askId: z13.string().uuid().describe(
|
|
7810
|
-
|
|
7811
|
-
),
|
|
7812
|
-
reason: z13.string().min(1).max(MAX_WITHDRAW_REASON).describe(
|
|
7813
|
-
'Why this question is no longer the question ("merged via GitHub #1551"). Required, and shown to the person on their Done row \u2014 a bare "withdrawn" tells them nothing.'
|
|
7814
|
-
)
|
|
7841
|
+
askId: z13.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.askId),
|
|
7842
|
+
reason: z13.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.reason)
|
|
7815
7843
|
},
|
|
7816
7844
|
async (args) => performAct(acts, {
|
|
7817
7845
|
kind: "withdraw_ask",
|
|
@@ -7824,7 +7852,7 @@ function createTurnControlMcpServer(acts, skipState, replyState) {
|
|
|
7824
7852
|
// caller's own wake in the conversation it's holding a turn in.
|
|
7825
7853
|
tool(
|
|
7826
7854
|
CANCEL_WAKE_TOOL,
|
|
7827
|
-
|
|
7855
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS.cancel_wake,
|
|
7828
7856
|
{},
|
|
7829
7857
|
async () => performAct(acts, { kind: "cancel_wake", callId: randomUUID() }),
|
|
7830
7858
|
{ annotations: CANCEL_TOOL, alwaysLoad: true }
|
package/dist/runtime.js
CHANGED
|
@@ -7067,6 +7067,62 @@ import { existsSync as existsSync10 } from "fs";
|
|
|
7067
7067
|
|
|
7068
7068
|
// src/turn-control-tools.ts
|
|
7069
7069
|
import { randomUUID } from "crypto";
|
|
7070
|
+
|
|
7071
|
+
// packages/agent-runtime/src/turn-control/descriptions.ts
|
|
7072
|
+
var MAX_ASK_QUESTION_BYTES = 400;
|
|
7073
|
+
var MAX_ASK_HEADLINE_BYTES = 120;
|
|
7074
|
+
var MAX_ASK_ITEMS = 20;
|
|
7075
|
+
var MAX_ASK_OPTION_BYTES = 200;
|
|
7076
|
+
var MAX_WAKE_NOTE_BYTES = 2e3;
|
|
7077
|
+
var MAX_WITHDRAW_REASON_BYTES = 400;
|
|
7078
|
+
var MAX_SEND_MESSAGE_BYTES = 65536;
|
|
7079
|
+
var MAX_SKIP_REASON_BYTES = 500;
|
|
7080
|
+
var TURN_CONTROL_TOOL_DESCRIPTIONS = {
|
|
7081
|
+
ask: `Ask a HUMAN a structured question (or a short LIST of them) you need answered to continue, then END your turn \u2014 don't wait for the reply. Use it when you genuinely can't proceed without a person's input (a decision only they can make, a missing fact). Pass \`targetUserId\` (a workspace member's user id \u2014 every person's id is on the roster in your turn context). Two forms: a SINGLE question \u2014 a \`headline\` (the actual question as one clear, capitalized sentence ending in \`?\`, "Do we go to prod?") plus a short \`question\` body for the framing the headline can't hold \u2014 OR, when a plan ends with SEVERAL bounded decisions at once, a \`questions\` array of 1\u2013${MAX_ASK_ITEMS} items, each \`{ headline, body?, options? }\`. Provide EITHER \`question\` (single) or \`questions\` (array), never both. **Several decisions landing together are ONE call with \`questions\`** \u2014 never cram the extras into prose, drop all but one, or fire an ask each: a second \`ask\` is a second inbox item competing with the first, never an edit to it. Each question keeps the same form: a one-sentence \`headline\`, a short \`body\` frame (NOT a report \u2014 your status, links and detail go in your REPLY; inline markdown only, no lists or headings), and 2\u20134 \`options\` when the answer is a bounded choice \u2014 for a yes/no go-ahead always pass them, so it's one click, not a typed reply. The ask is a first-class attention item aimed at that person; your final reply carries the CONTEXT, the ask carries the QUESTION(S). It is written and delivered THE MOMENT you call this \u2014 the person sees it in their inbox while your turn is still running \u2014 so call it once you know the question. The human sends ONE response: supplied answers are recorded, omissions become terminally Unanswered. Ask only when blocked \u2014 never ceremonially. \`replaces\` withdraws a stale ask of yours (or anyone's) in this conversation in the same write, with the reason shown to the person. Targets a human only; to hand work to another AGENT use send/dispatch instead.`,
|
|
7082
|
+
cancel_wake: `Stand down your own wake \u2014 the inverse of \`wake_me\`. No arguments: it cancels the wake you have armed in this conversation, NOW, when you call it. Reach for it when the thing you armed a wake to check has already happened, or the work it was watching is over \u2014 an armed wake you no longer need fires into a turn with nothing to do, and a supervision loop with no off switch is one you can only end by leaving it running. Whether you have one armed is printed in your turn context ("Wake armed: \u2026"); calling this with nothing armed is a clean no-op, no error. Each call acts as it arrives: \`cancel_wake\` then \`wake_me\` leaves the NEW wake armed; \`wake_me\` then \`cancel_wake\` leaves nothing armed. It reaches only your own wake in this conversation \u2014 never a peer's, and never a recurring schedule (those are \`schedules.*\`).`,
|
|
7083
|
+
reply_to: `Declare which addressed ask your final response answers. Pass the \`messageId\` shown as the owed reply id in your turn context, then finish your response normally. This records lineage only: it does not send another message. The server verifies that this agent owes that ask in this conversation; one reply per turn \u2014 a later call replaces the earlier declaration.`,
|
|
7084
|
+
send: `Send one addressed message to another agent in THIS conversation. Pass the peer's \`agentId\` and the complete \`message\` they should act on. The message is WRITTEN the moment you call this \u2014 durable, in the conversation's queue, in the order you called \u2014 but it is DELIVERED only after your own turn ends: the peer is not running yet and cannot answer you inside this turn. Each call is one message; two calls are two messages. Writing \`@handle\` in prose dispatches nobody. An unknown, deactivated, or self target is refused here, in the result. A handoff to a DIFFERENT conversation is \`conversations.create\` / \`conversations.post\` with their \`dispatch\` field instead. Never use send to hand an answer back to the requester \u2014 declare that with reply_to.`,
|
|
7085
|
+
skip_turn: `End your current turn WITHOUT posting a reply. Call this when you've been dispatched but the message genuinely doesn't need a response from you \u2014 a thanks/aside, a question already answered, chatter outside your lane, or a pile-on where someone else has it. 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.`,
|
|
7086
|
+
wake_me: `Wake yourself later \u2014 end this turn now and be re-dispatched at a time you pick, with a note you write to yourself. Use it for "wait until X": when the thing you need hasn't happened yet (a PR isn't merged, a human hasn't answered), arm a wake, end your turn, and you're woken later to CHECK \u2014 read the workspace, and either act or re-arm. Ground the delay before you arm it. Almost every wake is short \u2014 seconds to a couple of hours \u2014 waiting on a condition you can name: a session limit resetting, a PR merging. Reach past a few hours only when (a) a human asked for that timing, or (b) the wait is pinned to a real external event you can name \u2014 a report that only runs Mondays, a known reset time. A speculative far-future check-in you invented yourself is the one thing not to arm: if no one asked and you can't name both what clears the wait and why it takes that long, don't arm it \u2014 finish now, or raise an \`ask\`. Pass EXACTLY ONE of \`afterSeconds\` (a relative delay \u2014 \`300\` for five minutes) or \`at\` (an absolute ISO-8601 timestamp WITH a zone, e.g. \`2026-07-16T09:00:00-07:00\` \u2014 YOU compute it; the system never parses natural-language time). \`note\` is a message to your future self \u2014 it becomes the body of the wake message that re-dispatches you, so write the condition to re-check ("check whether CT441 merged yet"). The wake arms NOW, when you call this, and the delay counts from this call; a later \`wake_me\` in the same turn replaces it, and \`cancel_wake\` stands it down. A wake that comes due while your turn is still running waits and is delivered after your reply. This is the ONLY sanctioned way to schedule your own continuation; never reach for a host cron/scheduler. Guardrails: at least 60s out, at most 14 days \u2014 a wake that can't be armed returns an error here, in this call, so you can recompute; widen the interval as a loop ages (5m \u2192 15m \u2192 1h\u2026) rather than hammering; after many consecutive re-arms with no other activity you'll be steered to raise an \`ask\` to the human instead.`,
|
|
7087
|
+
withdraw_ask: `Withdraw an open ask \u2014 yours or anyone's in this conversation \u2014 because the question is moot, was wrong, or has been overtaken. \`reason\` is required and is shown to the person and, if it wasn't your ask, to the agent who asked it. If there is a current question to put in its place, don't call this \u2014 call \`ask\` with \`replaces\` so the two land as one and the person never sees neither or both. Never withdraw to hurry an answer; to act on a deadline, arm a \`wake_me\` and decide then.`
|
|
7088
|
+
};
|
|
7089
|
+
var TURN_CONTROL_FIELD_DESCRIPTIONS = {
|
|
7090
|
+
ask: {
|
|
7091
|
+
targetUserId: "The workspace member (human) to ask \u2014 a user id, from your turn context's roster.",
|
|
7092
|
+
question: `SINGLE-question form: a short body \u2014 one or two sentences of framing the headline can't hold. NOT a report (up to ${MAX_ASK_QUESTION_BYTES} characters, inline markdown only). Provide EITHER this or \`questions\`, not both. Put the crisp one-sentence question in \`headline\`.`,
|
|
7093
|
+
headline: `SINGLE-question form: the question itself as ONE clear, capitalized sentence ending in \`?\` ("Do we go to prod?"), up to ${MAX_ASK_HEADLINE_BYTES} characters. What the human reads first in the inbox and the chip \u2014 one scannable question, no elaboration (that goes in \`question\`). Strongly encouraged.`,
|
|
7094
|
+
options: `SINGLE-question form: optional 2\u20134 suggested one-click answers, up to ${MAX_ASK_OPTION_BYTES} characters each.`,
|
|
7095
|
+
questions: `MULTI-question form: 1\u2013${MAX_ASK_ITEMS} questions to ask at once, when a plan ends with several bounded decisions. Provide EITHER this or \`question\`/\`headline\`/\`options\`, not both.`,
|
|
7096
|
+
"questions.headline": `The one-sentence question ("Do we go to prod?") \u2014 required for each item, up to ${MAX_ASK_HEADLINE_BYTES} characters.`,
|
|
7097
|
+
"questions.body": `Optional short framing beneath the headline, up to ${MAX_ASK_QUESTION_BYTES} characters. NOT a report.`,
|
|
7098
|
+
"questions.options": `Optional 2\u20134 one-click answers for this question, up to ${MAX_ASK_OPTION_BYTES} characters each.`,
|
|
7099
|
+
replaces: "Withdraw a stale ask in this conversation as part of raising this one. Both land in a single write, so the person goes from the old question to the new one and never sees neither or both.",
|
|
7100
|
+
"replaces.askId": "The id of the open ask to withdraw.",
|
|
7101
|
+
"replaces.reason": `Why the old question is no longer the question \u2014 shown to the person on their Done row. Up to ${MAX_WITHDRAW_REASON_BYTES} characters.`
|
|
7102
|
+
},
|
|
7103
|
+
cancel_wake: {},
|
|
7104
|
+
reply_to: {
|
|
7105
|
+
messageId: "The owed ask message id from this turn context."
|
|
7106
|
+
},
|
|
7107
|
+
send: {
|
|
7108
|
+
agentId: "The peer agent to address \u2014 a workspace agent id, from your turn context's roster.",
|
|
7109
|
+
message: `The complete new request the peer should receive and act on. Up to ${MAX_SEND_MESSAGE_BYTES} characters.`
|
|
7110
|
+
},
|
|
7111
|
+
skip_turn: {
|
|
7112
|
+
reason: `Short reason you are declining \u2014 used for telemetry/debugging, never posted. Up to ${MAX_SKIP_REASON_BYTES} characters.`
|
|
7113
|
+
},
|
|
7114
|
+
wake_me: {
|
|
7115
|
+
afterSeconds: "Relative delay in seconds from NOW \u2014 this call (e.g. 300 = five minutes). Provide EITHER this or `at`, not both. Floor 60s, horizon 14 days \u2014 enforced server-side.",
|
|
7116
|
+
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.",
|
|
7117
|
+
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 ("check whether the PR merged").`
|
|
7118
|
+
},
|
|
7119
|
+
withdraw_ask: {
|
|
7120
|
+
askId: "The open ask to withdraw. Ask ids ride the conversation's message rows (`ask: { id, status, items }`), and your turn context lists the ones you have open.",
|
|
7121
|
+
reason: `Why this question is no longer the question ("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.`
|
|
7122
|
+
}
|
|
7123
|
+
};
|
|
7124
|
+
|
|
7125
|
+
// src/turn-control-tools.ts
|
|
7070
7126
|
import { z as z13 } from "zod";
|
|
7071
7127
|
var COMPANION_LOCAL_MCP_SERVER = "cabane_companion";
|
|
7072
7128
|
var SEND_TOOL = "send";
|
|
@@ -7077,10 +7133,8 @@ var ASK_TOOL = "ask";
|
|
|
7077
7133
|
var ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${ASK_TOOL}`;
|
|
7078
7134
|
var WITHDRAW_ASK_TOOL = "withdraw_ask";
|
|
7079
7135
|
var WITHDRAW_ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WITHDRAW_ASK_TOOL}`;
|
|
7080
|
-
var MAX_WITHDRAW_REASON = 400;
|
|
7081
7136
|
var REPLY_TO_TOOL = "reply_to";
|
|
7082
7137
|
var REPLY_TO_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${REPLY_TO_TOOL}`;
|
|
7083
|
-
var MAX_ASK_ITEMS = 20;
|
|
7084
7138
|
var WAKE_ME_TOOL = "wake_me";
|
|
7085
7139
|
var WAKE_ME_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WAKE_ME_TOOL}`;
|
|
7086
7140
|
var CANCEL_WAKE_TOOL = "cancel_wake";
|
|
@@ -7138,12 +7192,10 @@ function createTurnControlMcpServer(acts, skipState, replyState) {
|
|
|
7138
7192
|
tools: [
|
|
7139
7193
|
tool(
|
|
7140
7194
|
SEND_TOOL,
|
|
7141
|
-
|
|
7195
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS.send,
|
|
7142
7196
|
{
|
|
7143
|
-
agentId: z13.string().uuid().describe(
|
|
7144
|
-
|
|
7145
|
-
),
|
|
7146
|
-
message: z13.string().min(1).max(65536).describe("The complete new request the peer should receive and act on.")
|
|
7197
|
+
agentId: z13.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.send.agentId),
|
|
7198
|
+
message: z13.string().min(1).max(MAX_SEND_MESSAGE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.send.message)
|
|
7147
7199
|
},
|
|
7148
7200
|
async (args) => performAct(acts, {
|
|
7149
7201
|
kind: "send",
|
|
@@ -7156,9 +7208,9 @@ function createTurnControlMcpServer(acts, skipState, replyState) {
|
|
|
7156
7208
|
...replyState ? [
|
|
7157
7209
|
tool(
|
|
7158
7210
|
REPLY_TO_TOOL,
|
|
7159
|
-
|
|
7211
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS.reply_to,
|
|
7160
7212
|
{
|
|
7161
|
-
messageId: z13.string().uuid().describe(
|
|
7213
|
+
messageId: z13.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.reply_to.messageId)
|
|
7162
7214
|
},
|
|
7163
7215
|
async (args) => {
|
|
7164
7216
|
replyState.answersMessageId = args.messageId;
|
|
@@ -7177,9 +7229,9 @@ function createTurnControlMcpServer(acts, skipState, replyState) {
|
|
|
7177
7229
|
...skipState ? [
|
|
7178
7230
|
tool(
|
|
7179
7231
|
SKIP_TURN_TOOL,
|
|
7180
|
-
|
|
7232
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS.skip_turn,
|
|
7181
7233
|
{
|
|
7182
|
-
reason: z13.string().min(1).max(
|
|
7234
|
+
reason: z13.string().min(1).max(MAX_SKIP_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.skip_turn.reason)
|
|
7183
7235
|
},
|
|
7184
7236
|
async (args) => {
|
|
7185
7237
|
skipState.skipped = true;
|
|
@@ -7193,37 +7245,23 @@ function createTurnControlMcpServer(acts, skipState, replyState) {
|
|
|
7193
7245
|
] : [],
|
|
7194
7246
|
tool(
|
|
7195
7247
|
ASK_TOOL,
|
|
7196
|
-
|
|
7248
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS.ask,
|
|
7197
7249
|
{
|
|
7198
|
-
targetUserId: z13.string().uuid().describe(
|
|
7199
|
-
|
|
7200
|
-
),
|
|
7201
|
-
|
|
7202
|
-
"SINGLE-question form: a short body \u2014 one or two sentences of framing the headline can't hold. NOT a report (capped, inline markdown only). Provide EITHER this or `questions`, not both. Put the crisp one-sentence question in `headline`."
|
|
7203
|
-
),
|
|
7204
|
-
headline: z13.string().min(1).max(120).optional().describe(
|
|
7205
|
-
'SINGLE-question form: the question itself as ONE clear, capitalized sentence ending in `?` ("Do we go to prod?"). What the human reads first in the inbox and the chip \u2014 one scannable question, no elaboration (that goes in `question`). Strongly encouraged.'
|
|
7206
|
-
),
|
|
7207
|
-
options: z13.array(z13.string().min(1).max(200)).min(2).max(4).optional().describe("SINGLE-question form: optional 2\u20134 suggested one-click answers."),
|
|
7250
|
+
targetUserId: z13.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.targetUserId),
|
|
7251
|
+
question: z13.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.question),
|
|
7252
|
+
headline: z13.string().min(1).max(MAX_ASK_HEADLINE_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.headline),
|
|
7253
|
+
options: z13.array(z13.string().min(1).max(MAX_ASK_OPTION_BYTES)).min(2).max(4).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.options),
|
|
7208
7254
|
questions: z13.array(
|
|
7209
7255
|
z13.object({
|
|
7210
|
-
headline: z13.string().min(1).max(
|
|
7211
|
-
|
|
7212
|
-
)
|
|
7213
|
-
body: z13.string().min(1).max(400).optional().describe("Optional short framing beneath the headline. NOT a report."),
|
|
7214
|
-
options: z13.array(z13.string().min(1).max(200)).min(2).max(4).optional().describe("Optional 2\u20134 one-click answers for this question.")
|
|
7256
|
+
headline: z13.string().min(1).max(MAX_ASK_HEADLINE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.headline"]),
|
|
7257
|
+
body: z13.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.body"]),
|
|
7258
|
+
options: z13.array(z13.string().min(1).max(MAX_ASK_OPTION_BYTES)).min(2).max(4).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options"])
|
|
7215
7259
|
})
|
|
7216
|
-
).min(1).max(MAX_ASK_ITEMS).optional().describe(
|
|
7217
|
-
`MULTI-question form: 1\u2013${MAX_ASK_ITEMS} questions to ask at once, when a plan ends with several bounded decisions. Provide EITHER this or \`question\`/\`headline\`/\`options\`, not both.`
|
|
7218
|
-
),
|
|
7260
|
+
).min(1).max(MAX_ASK_ITEMS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.questions),
|
|
7219
7261
|
replaces: z13.object({
|
|
7220
|
-
askId: z13.string().uuid().describe("
|
|
7221
|
-
reason: z13.string().min(1).max(
|
|
7222
|
-
|
|
7223
|
-
)
|
|
7224
|
-
}).optional().describe(
|
|
7225
|
-
"Withdraw a stale ask in this conversation as part of raising this one. Both land in a single write, so the person goes from the old question to the new one and never sees neither or both."
|
|
7226
|
-
)
|
|
7262
|
+
askId: z13.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.askId"]),
|
|
7263
|
+
reason: z13.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.reason"])
|
|
7264
|
+
}).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.replaces)
|
|
7227
7265
|
},
|
|
7228
7266
|
async (args) => {
|
|
7229
7267
|
const hasSingle = args.question !== void 0;
|
|
@@ -7261,17 +7299,11 @@ function createTurnControlMcpServer(acts, skipState, replyState) {
|
|
|
7261
7299
|
),
|
|
7262
7300
|
tool(
|
|
7263
7301
|
WAKE_ME_TOOL,
|
|
7264
|
-
|
|
7302
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS.wake_me,
|
|
7265
7303
|
{
|
|
7266
|
-
afterSeconds: z13.number().int().positive().optional().describe(
|
|
7267
|
-
|
|
7268
|
-
)
|
|
7269
|
-
at: z13.string().datetime({ offset: true }).optional().describe(
|
|
7270
|
-
"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."
|
|
7271
|
-
),
|
|
7272
|
-
note: z13.string().min(1).max(2e3).describe(
|
|
7273
|
-
'A note to your future self \u2014 becomes the body of the wake message that re-dispatches you. Write the condition to re-check ("check whether the PR merged").'
|
|
7274
|
-
)
|
|
7304
|
+
afterSeconds: z13.number().int().positive().optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.afterSeconds),
|
|
7305
|
+
at: z13.string().datetime({ offset: true }).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.at),
|
|
7306
|
+
note: z13.string().min(1).max(MAX_WAKE_NOTE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.note)
|
|
7275
7307
|
},
|
|
7276
7308
|
async (args) => {
|
|
7277
7309
|
const hasAfter = args.afterSeconds !== void 0;
|
|
@@ -7303,14 +7335,10 @@ function createTurnControlMcpServer(acts, skipState, replyState) {
|
|
|
7303
7335
|
// refused with `ask_not_open`, which is information, not a no-op.
|
|
7304
7336
|
tool(
|
|
7305
7337
|
WITHDRAW_ASK_TOOL,
|
|
7306
|
-
|
|
7338
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS.withdraw_ask,
|
|
7307
7339
|
{
|
|
7308
|
-
askId: z13.string().uuid().describe(
|
|
7309
|
-
|
|
7310
|
-
),
|
|
7311
|
-
reason: z13.string().min(1).max(MAX_WITHDRAW_REASON).describe(
|
|
7312
|
-
'Why this question is no longer the question ("merged via GitHub #1551"). Required, and shown to the person on their Done row \u2014 a bare "withdrawn" tells them nothing.'
|
|
7313
|
-
)
|
|
7340
|
+
askId: z13.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.askId),
|
|
7341
|
+
reason: z13.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.reason)
|
|
7314
7342
|
},
|
|
7315
7343
|
async (args) => performAct(acts, {
|
|
7316
7344
|
kind: "withdraw_ask",
|
|
@@ -7323,7 +7351,7 @@ function createTurnControlMcpServer(acts, skipState, replyState) {
|
|
|
7323
7351
|
// caller's own wake in the conversation it's holding a turn in.
|
|
7324
7352
|
tool(
|
|
7325
7353
|
CANCEL_WAKE_TOOL,
|
|
7326
|
-
|
|
7354
|
+
TURN_CONTROL_TOOL_DESCRIPTIONS.cancel_wake,
|
|
7327
7355
|
{},
|
|
7328
7356
|
async () => performAct(acts, { kind: "cancel_wake", callId: randomUUID() }),
|
|
7329
7357
|
{ annotations: CANCEL_TOOL, alwaysLoad: true }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.85",
|
|
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",
|