@cabane/companion 0.6.99 → 0.6.101
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 +107 -80
- package/dist/runtime.js +107 -80
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4110,17 +4110,81 @@ import {
|
|
|
4110
4110
|
query as defaultQuery
|
|
4111
4111
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
4112
4112
|
|
|
4113
|
+
// packages/agent-runtime/src/turn-control/descriptions.ts
|
|
4114
|
+
var MAX_ASK_QUESTION_BYTES = 400;
|
|
4115
|
+
var MAX_ASK_HEADLINE_BYTES = 120;
|
|
4116
|
+
var MAX_ASK_ITEMS = 20;
|
|
4117
|
+
var MAX_ASK_OPTION_BYTES = 200;
|
|
4118
|
+
var MIN_ASK_OPTIONS = 2;
|
|
4119
|
+
var MAX_ASK_OPTIONS = 4;
|
|
4120
|
+
var MAX_WAKE_NOTE_BYTES = 2e3;
|
|
4121
|
+
var MAX_WITHDRAW_REASON_BYTES = 400;
|
|
4122
|
+
var MAX_SKIP_REASON_BYTES = 500;
|
|
4123
|
+
var TURN_CONTROL_TOOLS = [
|
|
4124
|
+
"ask",
|
|
4125
|
+
"cancel_wake",
|
|
4126
|
+
"skip_turn",
|
|
4127
|
+
"wake_me",
|
|
4128
|
+
"withdraw_ask"
|
|
4129
|
+
];
|
|
4130
|
+
var TURN_CONTROL_TOOL_DESCRIPTIONS = {
|
|
4131
|
+
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 can't proceed without a person's input (a decision only they can make, a missing fact). Pass \`targetUserId\`. Two forms: a SINGLE question \u2014 a \`headline\` 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 ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} \`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. Your final reply carries the CONTEXT, the ask carries the QUESTION(S). Your question is RECORDED when you call this and delivered when your turn ends \u2014 beside the reply that frames it, or earlier on a long turn, so they can answer while you work. Ask as soon as you know it, and write it to stand alone: in every headline, body and option, say "I" for yourself and name anyone else who acts \u2014 never "you", "we" or "they". The human sends ONE response: supplied answers are recorded, omissions become terminally Unanswered. A person can DISMISS a question instead of answering it \u2014 it ends, you are not told, nothing wakes you. Check an ask's status before re-raising a vanished one. \`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 bring a peer AGENT in, name them by \`@handle\` in your reply.`,
|
|
4132
|
+
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.*\`).`,
|
|
4133
|
+
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.`,
|
|
4134
|
+
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.`,
|
|
4135
|
+
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.`
|
|
4136
|
+
};
|
|
4137
|
+
var TURN_CONTROL_FIELD_DESCRIPTIONS = {
|
|
4138
|
+
ask: {
|
|
4139
|
+
targetUserId: "The workspace member (human) to ask \u2014 a user id, from your turn context's roster.",
|
|
4140
|
+
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\`. Say "I" for yourself; name anyone else described, never "you", "we" or "they".`,
|
|
4141
|
+
headline: `SINGLE-question form: the question itself as ONE clear, capitalized sentence ending in \`?\` ("Should Ledger deploy once Mia approves?"), 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. Say "I" for yourself; name anyone else described, never "you", "we" or "they".`,
|
|
4142
|
+
options: `SINGLE-question form: optional ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} suggested one-click answers, up to ${MAX_ASK_OPTION_BYTES} characters each. Each option must stand alone: "I" for yourself, anyone else responsible for its action by name \u2014 never "you", "we" or "they".`,
|
|
4143
|
+
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.`,
|
|
4144
|
+
"questions.headline": `The one-sentence question ("Should Ledger deploy once Mia approves?") \u2014 required for each item, up to ${MAX_ASK_HEADLINE_BYTES} characters. Say "I" for yourself; name anyone else described, never "you", "we" or "they".`,
|
|
4145
|
+
"questions.body": `Optional short framing beneath the headline, up to ${MAX_ASK_QUESTION_BYTES} characters. NOT a report. Say "I" for yourself; name anyone else described, never "you", "we" or "they".`,
|
|
4146
|
+
"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 stand alone: "I" for yourself, anyone else responsible for its action by name \u2014 never "you", "we" or "they".`,
|
|
4147
|
+
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.",
|
|
4148
|
+
"replaces.askId": "The id of the open ask to withdraw.",
|
|
4149
|
+
"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.`
|
|
4150
|
+
},
|
|
4151
|
+
cancel_wake: {},
|
|
4152
|
+
skip_turn: {
|
|
4153
|
+
reason: `Short reason you are declining \u2014 used for telemetry/debugging, never posted. Up to ${MAX_SKIP_REASON_BYTES} characters.`
|
|
4154
|
+
},
|
|
4155
|
+
wake_me: {
|
|
4156
|
+
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.",
|
|
4157
|
+
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.",
|
|
4158
|
+
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").`
|
|
4159
|
+
},
|
|
4160
|
+
withdraw_ask: {
|
|
4161
|
+
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.",
|
|
4162
|
+
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.`
|
|
4163
|
+
}
|
|
4164
|
+
};
|
|
4165
|
+
|
|
4166
|
+
// packages/agent-runtime/src/turn-control/prompt-guidance.ts
|
|
4167
|
+
var MEANINGS = {
|
|
4168
|
+
ask: "put a structured question to a person",
|
|
4169
|
+
cancel_wake: "cancel your own armed wake in this conversation",
|
|
4170
|
+
skip_turn: "end without posting a reply",
|
|
4171
|
+
wake_me: "schedule your own return",
|
|
4172
|
+
withdraw_ask: "withdraw an open ask in this conversation with a reason"
|
|
4173
|
+
};
|
|
4174
|
+
function turnControlPromptGuidance(prefix) {
|
|
4175
|
+
const controls = TURN_CONTROL_TOOLS.map((name) => `\`${prefix}${name}\` (${MEANINGS[name]})`);
|
|
4176
|
+
return `The turn tools are: ${controls.join("; ")}. Read a tool's description before calling it. A peer response is ordinary conversation speech; your reply is the answer.`;
|
|
4177
|
+
}
|
|
4178
|
+
|
|
4113
4179
|
// packages/agent-runtime/src/claude-code/addendum.ts
|
|
4114
4180
|
var CLAUDE_CODE_ADDENDUM = [
|
|
4115
4181
|
"On this harness the Cabane tools register under MCP prefixes. The workspace tools are",
|
|
4116
4182
|
"`mcp__cabane__query`, `mcp__cabane__mutate` and `mcp__cabane__describe`; the ancillary",
|
|
4117
4183
|
"tools are `mcp__cabane__list_workspaces`,",
|
|
4118
4184
|
"`mcp__cabane__read_binary`, `mcp__cabane__upload`, `mcp__cabane__begin_upload`,",
|
|
4119
|
-
"`mcp__cabane__finalize_upload`, and `mcp__cabane__mint_render_token
|
|
4120
|
-
"
|
|
4121
|
-
"
|
|
4122
|
-
"`mcp__cabane_companion__send`, and",
|
|
4123
|
-
"`mcp__cabane_companion__skip_turn`. Where this prompt names a tool by its bare verb, call",
|
|
4185
|
+
"`mcp__cabane__finalize_upload`, and `mcp__cabane__mint_render_token`.",
|
|
4186
|
+
turnControlPromptGuidance("mcp__cabane_companion__"),
|
|
4187
|
+
"Where this prompt names a tool by its bare verb, call",
|
|
4124
4188
|
"the prefixed form from your tool list. Your harness's own tools (shell, local file",
|
|
4125
4189
|
"read/edit) keep their ordinary names and act on the local machine only."
|
|
4126
4190
|
].join(" ");
|
|
@@ -5216,8 +5280,9 @@ function selectAdapter(registry, runtime) {
|
|
|
5216
5280
|
// packages/agent-runtime/src/opencode/addendum.ts
|
|
5217
5281
|
var OPENCODE_ADDENDUM = [
|
|
5218
5282
|
"Every tool here goes by its plain name: the workspace tools are `query`, `mutate` and",
|
|
5219
|
-
"`describe
|
|
5220
|
-
"
|
|
5283
|
+
"`describe`.",
|
|
5284
|
+
turnControlPromptGuidance(""),
|
|
5285
|
+
"The ancillary tools are `list_workspaces`, `read_binary`, `upload`, `begin_upload`,",
|
|
5221
5286
|
"`finalize_upload`, `mint_render_token`; and the host tools are plain verbs too (`bash`,",
|
|
5222
5287
|
"`read`, `edit`). Mind the collision: a bare `read` or `edit` is the HOST tool, acting on the",
|
|
5223
5288
|
"local machine \u2014 a workspace read is `query` with the operation name `files.read`. The",
|
|
@@ -6282,8 +6347,9 @@ var CODEX_ADDENDUM = [
|
|
|
6282
6347
|
"the deferred-tool inventory and invoke those exact qualified names; never conclude the",
|
|
6283
6348
|
"workspace is",
|
|
6284
6349
|
"absent without attempting discovery and invocation, and if a call fails, report the recorded",
|
|
6285
|
-
"error.
|
|
6286
|
-
"
|
|
6350
|
+
"error.",
|
|
6351
|
+
turnControlPromptGuidance("mcp__cabane_companion__"),
|
|
6352
|
+
"The ancillary tools are",
|
|
6287
6353
|
"`mcp__cabane__list_workspaces` /",
|
|
6288
6354
|
"`read_binary` / `upload` / `begin_upload` / `finalize_upload` / `mint_render_token` \u2014 all",
|
|
6289
6355
|
"possibly deferred too. There is no Cabane `read`/`write`/`search` tool \u2014 those are",
|
|
@@ -7878,54 +7944,6 @@ import { existsSync as existsSync10 } from "fs";
|
|
|
7878
7944
|
|
|
7879
7945
|
// src/turn-control-tools.ts
|
|
7880
7946
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
7881
|
-
|
|
7882
|
-
// packages/agent-runtime/src/turn-control/descriptions.ts
|
|
7883
|
-
var MAX_ASK_QUESTION_BYTES = 400;
|
|
7884
|
-
var MAX_ASK_HEADLINE_BYTES = 120;
|
|
7885
|
-
var MAX_ASK_ITEMS = 20;
|
|
7886
|
-
var MAX_ASK_OPTION_BYTES = 200;
|
|
7887
|
-
var MIN_ASK_OPTIONS = 2;
|
|
7888
|
-
var MAX_ASK_OPTIONS = 4;
|
|
7889
|
-
var MAX_WAKE_NOTE_BYTES = 2e3;
|
|
7890
|
-
var MAX_WITHDRAW_REASON_BYTES = 400;
|
|
7891
|
-
var MAX_SKIP_REASON_BYTES = 500;
|
|
7892
|
-
var TURN_CONTROL_TOOL_DESCRIPTIONS = {
|
|
7893
|
-
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\`. Two forms: a SINGLE question \u2014 a \`headline\` 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 ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} \`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. A person can DISMISS a question instead of answering it \u2014 it ends, you are not told, nothing wakes you. Check an ask's status before re-raising a vanished one. 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 bring a peer AGENT in, name them by \`@handle\` in your reply.`,
|
|
7894
|
-
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.*\`).`,
|
|
7895
|
-
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.`,
|
|
7896
|
-
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.`,
|
|
7897
|
-
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.`
|
|
7898
|
-
};
|
|
7899
|
-
var TURN_CONTROL_FIELD_DESCRIPTIONS = {
|
|
7900
|
-
ask: {
|
|
7901
|
-
targetUserId: "The workspace member (human) to ask \u2014 a user id, from your turn context's roster.",
|
|
7902
|
-
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\`.`,
|
|
7903
|
-
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.`,
|
|
7904
|
-
options: `SINGLE-question form: optional ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} suggested one-click answers, up to ${MAX_ASK_OPTION_BYTES} characters each.`,
|
|
7905
|
-
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.`,
|
|
7906
|
-
"questions.headline": `The one-sentence question ("Do we go to prod?") \u2014 required for each item, up to ${MAX_ASK_HEADLINE_BYTES} characters.`,
|
|
7907
|
-
"questions.body": `Optional short framing beneath the headline, up to ${MAX_ASK_QUESTION_BYTES} characters. NOT a report.`,
|
|
7908
|
-
"questions.options": `Optional ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} one-click answers for this question, up to ${MAX_ASK_OPTION_BYTES} characters each.`,
|
|
7909
|
-
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.",
|
|
7910
|
-
"replaces.askId": "The id of the open ask to withdraw.",
|
|
7911
|
-
"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.`
|
|
7912
|
-
},
|
|
7913
|
-
cancel_wake: {},
|
|
7914
|
-
skip_turn: {
|
|
7915
|
-
reason: `Short reason you are declining \u2014 used for telemetry/debugging, never posted. Up to ${MAX_SKIP_REASON_BYTES} characters.`
|
|
7916
|
-
},
|
|
7917
|
-
wake_me: {
|
|
7918
|
-
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.",
|
|
7919
|
-
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.",
|
|
7920
|
-
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").`
|
|
7921
|
-
},
|
|
7922
|
-
withdraw_ask: {
|
|
7923
|
-
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.",
|
|
7924
|
-
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.`
|
|
7925
|
-
}
|
|
7926
|
-
};
|
|
7927
|
-
|
|
7928
|
-
// src/turn-control-tools.ts
|
|
7929
7947
|
import { z as z13 } from "zod";
|
|
7930
7948
|
var COMPANION_LOCAL_MCP_SERVER = "cabane_companion";
|
|
7931
7949
|
var COMPANION_LOCAL_TOOL_GLOB = `mcp__${COMPANION_LOCAL_MCP_SERVER}__*`;
|
|
@@ -8221,6 +8239,17 @@ function describeErrorBody(body) {
|
|
|
8221
8239
|
if (text.length === 0) return void 0;
|
|
8222
8240
|
return text.length > ERROR_BODY_LOG_CAP ? `${text.slice(0, ERROR_BODY_LOG_CAP)}\u2026` : text;
|
|
8223
8241
|
}
|
|
8242
|
+
function apiErrorLogFields(err) {
|
|
8243
|
+
const fields = {
|
|
8244
|
+
err: err instanceof Error ? err.message : String(err)
|
|
8245
|
+
};
|
|
8246
|
+
if (err instanceof ApiError) {
|
|
8247
|
+
fields.status = err.status;
|
|
8248
|
+
const body = describeErrorBody(err.body);
|
|
8249
|
+
if (body !== void 0) fields.responseBody = body;
|
|
8250
|
+
}
|
|
8251
|
+
return fields;
|
|
8252
|
+
}
|
|
8224
8253
|
|
|
8225
8254
|
// src/turn-seq-floor.ts
|
|
8226
8255
|
var SeqFloorUnavailable = class extends Error {
|
|
@@ -8489,10 +8518,7 @@ var TurnCommitter = class {
|
|
|
8489
8518
|
constructor(deps) {
|
|
8490
8519
|
this.deps = deps;
|
|
8491
8520
|
this.onError = (err, hook) => {
|
|
8492
|
-
deps.log.warn(
|
|
8493
|
-
{ err: err instanceof Error ? err.message : String(err), hook },
|
|
8494
|
-
"dispatcher: transcript callback failed"
|
|
8495
|
-
);
|
|
8521
|
+
deps.log.warn({ ...apiErrorLogFields(err), hook }, "dispatcher: transcript callback failed");
|
|
8496
8522
|
deps.onCommitFailed?.(err);
|
|
8497
8523
|
};
|
|
8498
8524
|
const commit = {
|
|
@@ -8689,7 +8715,17 @@ async function absorbBundledLoss(ctx, aborted, outcome) {
|
|
|
8689
8715
|
|
|
8690
8716
|
// src/turn-execution.ts
|
|
8691
8717
|
var PREPARING_TOOL_NAME = "preparing";
|
|
8692
|
-
var
|
|
8718
|
+
var ERROR_REASON_MAX = 200;
|
|
8719
|
+
function missingSecretReason(missing) {
|
|
8720
|
+
const prefix = "missing_secret: ";
|
|
8721
|
+
for (let shown = missing.length; shown >= 0; shown--) {
|
|
8722
|
+
const omitted = missing.length - shown;
|
|
8723
|
+
const parts = [...missing.slice(0, shown), ...omitted > 0 ? [`+${omitted}`] : []];
|
|
8724
|
+
const reason = prefix + parts.join(",");
|
|
8725
|
+
if (reason.length <= ERROR_REASON_MAX) return reason;
|
|
8726
|
+
}
|
|
8727
|
+
return `${prefix}+${missing.length}`;
|
|
8728
|
+
}
|
|
8693
8729
|
var RUNTIME_UNAVAILABLE_PREFIX = "**This agent's runtime isn't available on this companion.** The model this agent uses needs a runtime this device isn't running, so I can't run this turn here. Details:";
|
|
8694
8730
|
var UNEXPECTED_ROLE_REASON = `refused the wake trigger (role "system" isn't dispatchable on this companion) \u2014 the companion is likely running outdated code; refresh it, then re-address the agent`;
|
|
8695
8731
|
var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
|
|
@@ -8880,7 +8916,7 @@ var TurnExecution = class {
|
|
|
8880
8916
|
);
|
|
8881
8917
|
} catch (err) {
|
|
8882
8918
|
turnLog.warn(
|
|
8883
|
-
{
|
|
8919
|
+
{ ...apiErrorLogFields(err), turnId },
|
|
8884
8920
|
"dispatcher: admitted-run settle failed terminally; server age-sweep is the backstop"
|
|
8885
8921
|
);
|
|
8886
8922
|
}
|
|
@@ -8909,6 +8945,12 @@ var TurnExecution = class {
|
|
|
8909
8945
|
);
|
|
8910
8946
|
throw this.concluded("turn_context_not_found");
|
|
8911
8947
|
}
|
|
8948
|
+
if (status2 === 409 && apiErrorCode(err) === "dispatch_not_admitted") {
|
|
8949
|
+
turnLog.warn(
|
|
8950
|
+
"dispatcher: turn-context 409 dispatch_not_admitted (wake already resolved); skipping"
|
|
8951
|
+
);
|
|
8952
|
+
throw this.concluded("turn_context_not_admitted");
|
|
8953
|
+
}
|
|
8912
8954
|
const reason = err instanceof Error ? err.message : String(err);
|
|
8913
8955
|
turnLog.error({ err: reason }, "dispatcher: failed to fetch turn context");
|
|
8914
8956
|
const fetchReason = `fetch_failed: ${reason}`;
|
|
@@ -8947,24 +8989,9 @@ var TurnExecution = class {
|
|
|
8947
8989
|
secretStore
|
|
8948
8990
|
);
|
|
8949
8991
|
if (missing.length > 0) {
|
|
8950
|
-
const list = missing.map((n) => `\`${n}\``).join(", ");
|
|
8951
8992
|
turnLog.error({ missing }, "dispatcher: turn needs secrets not declared on this companion");
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
body: `${MISSING_SECRET_PREFIX} ${list}`,
|
|
8955
|
-
kind: "runtime_notice",
|
|
8956
|
-
turnId: randomUUID3(),
|
|
8957
|
-
// CT113: even a turn that fails before it runs answers a message.
|
|
8958
|
-
parentMessageId: payload.messageId
|
|
8959
|
-
});
|
|
8960
|
-
} catch (postErr) {
|
|
8961
|
-
turnLog.warn(
|
|
8962
|
-
{ err: postErr instanceof Error ? postErr.message : String(postErr) },
|
|
8963
|
-
"dispatcher: missing-secret notice post failed"
|
|
8964
|
-
);
|
|
8965
|
-
}
|
|
8966
|
-
const reason = `missing_secret: ${missing.join(",")}`;
|
|
8967
|
-
throw this.concluded(reason);
|
|
8993
|
+
const reason = missingSecretReason(missing);
|
|
8994
|
+
throw this.concluded(reason, reason);
|
|
8968
8995
|
}
|
|
8969
8996
|
this.resolvedMcpServers = resolvedMcpServers;
|
|
8970
8997
|
}
|
package/dist/runtime.js
CHANGED
|
@@ -3607,17 +3607,81 @@ import {
|
|
|
3607
3607
|
query as defaultQuery
|
|
3608
3608
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
3609
3609
|
|
|
3610
|
+
// packages/agent-runtime/src/turn-control/descriptions.ts
|
|
3611
|
+
var MAX_ASK_QUESTION_BYTES = 400;
|
|
3612
|
+
var MAX_ASK_HEADLINE_BYTES = 120;
|
|
3613
|
+
var MAX_ASK_ITEMS = 20;
|
|
3614
|
+
var MAX_ASK_OPTION_BYTES = 200;
|
|
3615
|
+
var MIN_ASK_OPTIONS = 2;
|
|
3616
|
+
var MAX_ASK_OPTIONS = 4;
|
|
3617
|
+
var MAX_WAKE_NOTE_BYTES = 2e3;
|
|
3618
|
+
var MAX_WITHDRAW_REASON_BYTES = 400;
|
|
3619
|
+
var MAX_SKIP_REASON_BYTES = 500;
|
|
3620
|
+
var TURN_CONTROL_TOOLS = [
|
|
3621
|
+
"ask",
|
|
3622
|
+
"cancel_wake",
|
|
3623
|
+
"skip_turn",
|
|
3624
|
+
"wake_me",
|
|
3625
|
+
"withdraw_ask"
|
|
3626
|
+
];
|
|
3627
|
+
var TURN_CONTROL_TOOL_DESCRIPTIONS = {
|
|
3628
|
+
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 can't proceed without a person's input (a decision only they can make, a missing fact). Pass \`targetUserId\`. Two forms: a SINGLE question \u2014 a \`headline\` 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 ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} \`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. Your final reply carries the CONTEXT, the ask carries the QUESTION(S). Your question is RECORDED when you call this and delivered when your turn ends \u2014 beside the reply that frames it, or earlier on a long turn, so they can answer while you work. Ask as soon as you know it, and write it to stand alone: in every headline, body and option, say "I" for yourself and name anyone else who acts \u2014 never "you", "we" or "they". The human sends ONE response: supplied answers are recorded, omissions become terminally Unanswered. A person can DISMISS a question instead of answering it \u2014 it ends, you are not told, nothing wakes you. Check an ask's status before re-raising a vanished one. \`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 bring a peer AGENT in, name them by \`@handle\` in your reply.`,
|
|
3629
|
+
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.*\`).`,
|
|
3630
|
+
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.`,
|
|
3631
|
+
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.`,
|
|
3632
|
+
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.`
|
|
3633
|
+
};
|
|
3634
|
+
var TURN_CONTROL_FIELD_DESCRIPTIONS = {
|
|
3635
|
+
ask: {
|
|
3636
|
+
targetUserId: "The workspace member (human) to ask \u2014 a user id, from your turn context's roster.",
|
|
3637
|
+
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\`. Say "I" for yourself; name anyone else described, never "you", "we" or "they".`,
|
|
3638
|
+
headline: `SINGLE-question form: the question itself as ONE clear, capitalized sentence ending in \`?\` ("Should Ledger deploy once Mia approves?"), 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. Say "I" for yourself; name anyone else described, never "you", "we" or "they".`,
|
|
3639
|
+
options: `SINGLE-question form: optional ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} suggested one-click answers, up to ${MAX_ASK_OPTION_BYTES} characters each. Each option must stand alone: "I" for yourself, anyone else responsible for its action by name \u2014 never "you", "we" or "they".`,
|
|
3640
|
+
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.`,
|
|
3641
|
+
"questions.headline": `The one-sentence question ("Should Ledger deploy once Mia approves?") \u2014 required for each item, up to ${MAX_ASK_HEADLINE_BYTES} characters. Say "I" for yourself; name anyone else described, never "you", "we" or "they".`,
|
|
3642
|
+
"questions.body": `Optional short framing beneath the headline, up to ${MAX_ASK_QUESTION_BYTES} characters. NOT a report. Say "I" for yourself; name anyone else described, never "you", "we" or "they".`,
|
|
3643
|
+
"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 stand alone: "I" for yourself, anyone else responsible for its action by name \u2014 never "you", "we" or "they".`,
|
|
3644
|
+
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.",
|
|
3645
|
+
"replaces.askId": "The id of the open ask to withdraw.",
|
|
3646
|
+
"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.`
|
|
3647
|
+
},
|
|
3648
|
+
cancel_wake: {},
|
|
3649
|
+
skip_turn: {
|
|
3650
|
+
reason: `Short reason you are declining \u2014 used for telemetry/debugging, never posted. Up to ${MAX_SKIP_REASON_BYTES} characters.`
|
|
3651
|
+
},
|
|
3652
|
+
wake_me: {
|
|
3653
|
+
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.",
|
|
3654
|
+
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.",
|
|
3655
|
+
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").`
|
|
3656
|
+
},
|
|
3657
|
+
withdraw_ask: {
|
|
3658
|
+
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.",
|
|
3659
|
+
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.`
|
|
3660
|
+
}
|
|
3661
|
+
};
|
|
3662
|
+
|
|
3663
|
+
// packages/agent-runtime/src/turn-control/prompt-guidance.ts
|
|
3664
|
+
var MEANINGS = {
|
|
3665
|
+
ask: "put a structured question to a person",
|
|
3666
|
+
cancel_wake: "cancel your own armed wake in this conversation",
|
|
3667
|
+
skip_turn: "end without posting a reply",
|
|
3668
|
+
wake_me: "schedule your own return",
|
|
3669
|
+
withdraw_ask: "withdraw an open ask in this conversation with a reason"
|
|
3670
|
+
};
|
|
3671
|
+
function turnControlPromptGuidance(prefix) {
|
|
3672
|
+
const controls = TURN_CONTROL_TOOLS.map((name) => `\`${prefix}${name}\` (${MEANINGS[name]})`);
|
|
3673
|
+
return `The turn tools are: ${controls.join("; ")}. Read a tool's description before calling it. A peer response is ordinary conversation speech; your reply is the answer.`;
|
|
3674
|
+
}
|
|
3675
|
+
|
|
3610
3676
|
// packages/agent-runtime/src/claude-code/addendum.ts
|
|
3611
3677
|
var CLAUDE_CODE_ADDENDUM = [
|
|
3612
3678
|
"On this harness the Cabane tools register under MCP prefixes. The workspace tools are",
|
|
3613
3679
|
"`mcp__cabane__query`, `mcp__cabane__mutate` and `mcp__cabane__describe`; the ancillary",
|
|
3614
3680
|
"tools are `mcp__cabane__list_workspaces`,",
|
|
3615
3681
|
"`mcp__cabane__read_binary`, `mcp__cabane__upload`, `mcp__cabane__begin_upload`,",
|
|
3616
|
-
"`mcp__cabane__finalize_upload`, and `mcp__cabane__mint_render_token
|
|
3617
|
-
"
|
|
3618
|
-
"
|
|
3619
|
-
"`mcp__cabane_companion__send`, and",
|
|
3620
|
-
"`mcp__cabane_companion__skip_turn`. Where this prompt names a tool by its bare verb, call",
|
|
3682
|
+
"`mcp__cabane__finalize_upload`, and `mcp__cabane__mint_render_token`.",
|
|
3683
|
+
turnControlPromptGuidance("mcp__cabane_companion__"),
|
|
3684
|
+
"Where this prompt names a tool by its bare verb, call",
|
|
3621
3685
|
"the prefixed form from your tool list. Your harness's own tools (shell, local file",
|
|
3622
3686
|
"read/edit) keep their ordinary names and act on the local machine only."
|
|
3623
3687
|
].join(" ");
|
|
@@ -4713,8 +4777,9 @@ function selectAdapter(registry, runtime) {
|
|
|
4713
4777
|
// packages/agent-runtime/src/opencode/addendum.ts
|
|
4714
4778
|
var OPENCODE_ADDENDUM = [
|
|
4715
4779
|
"Every tool here goes by its plain name: the workspace tools are `query`, `mutate` and",
|
|
4716
|
-
"`describe
|
|
4717
|
-
"
|
|
4780
|
+
"`describe`.",
|
|
4781
|
+
turnControlPromptGuidance(""),
|
|
4782
|
+
"The ancillary tools are `list_workspaces`, `read_binary`, `upload`, `begin_upload`,",
|
|
4718
4783
|
"`finalize_upload`, `mint_render_token`; and the host tools are plain verbs too (`bash`,",
|
|
4719
4784
|
"`read`, `edit`). Mind the collision: a bare `read` or `edit` is the HOST tool, acting on the",
|
|
4720
4785
|
"local machine \u2014 a workspace read is `query` with the operation name `files.read`. The",
|
|
@@ -5779,8 +5844,9 @@ var CODEX_ADDENDUM = [
|
|
|
5779
5844
|
"the deferred-tool inventory and invoke those exact qualified names; never conclude the",
|
|
5780
5845
|
"workspace is",
|
|
5781
5846
|
"absent without attempting discovery and invocation, and if a call fails, report the recorded",
|
|
5782
|
-
"error.
|
|
5783
|
-
"
|
|
5847
|
+
"error.",
|
|
5848
|
+
turnControlPromptGuidance("mcp__cabane_companion__"),
|
|
5849
|
+
"The ancillary tools are",
|
|
5784
5850
|
"`mcp__cabane__list_workspaces` /",
|
|
5785
5851
|
"`read_binary` / `upload` / `begin_upload` / `finalize_upload` / `mint_render_token` \u2014 all",
|
|
5786
5852
|
"possibly deferred too. There is no Cabane `read`/`write`/`search` tool \u2014 those are",
|
|
@@ -7375,54 +7441,6 @@ import { existsSync as existsSync10 } from "fs";
|
|
|
7375
7441
|
|
|
7376
7442
|
// src/turn-control-tools.ts
|
|
7377
7443
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
7378
|
-
|
|
7379
|
-
// packages/agent-runtime/src/turn-control/descriptions.ts
|
|
7380
|
-
var MAX_ASK_QUESTION_BYTES = 400;
|
|
7381
|
-
var MAX_ASK_HEADLINE_BYTES = 120;
|
|
7382
|
-
var MAX_ASK_ITEMS = 20;
|
|
7383
|
-
var MAX_ASK_OPTION_BYTES = 200;
|
|
7384
|
-
var MIN_ASK_OPTIONS = 2;
|
|
7385
|
-
var MAX_ASK_OPTIONS = 4;
|
|
7386
|
-
var MAX_WAKE_NOTE_BYTES = 2e3;
|
|
7387
|
-
var MAX_WITHDRAW_REASON_BYTES = 400;
|
|
7388
|
-
var MAX_SKIP_REASON_BYTES = 500;
|
|
7389
|
-
var TURN_CONTROL_TOOL_DESCRIPTIONS = {
|
|
7390
|
-
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\`. Two forms: a SINGLE question \u2014 a \`headline\` 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 ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} \`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. A person can DISMISS a question instead of answering it \u2014 it ends, you are not told, nothing wakes you. Check an ask's status before re-raising a vanished one. 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 bring a peer AGENT in, name them by \`@handle\` in your reply.`,
|
|
7391
|
-
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.*\`).`,
|
|
7392
|
-
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.`,
|
|
7393
|
-
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.`,
|
|
7394
|
-
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.`
|
|
7395
|
-
};
|
|
7396
|
-
var TURN_CONTROL_FIELD_DESCRIPTIONS = {
|
|
7397
|
-
ask: {
|
|
7398
|
-
targetUserId: "The workspace member (human) to ask \u2014 a user id, from your turn context's roster.",
|
|
7399
|
-
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\`.`,
|
|
7400
|
-
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.`,
|
|
7401
|
-
options: `SINGLE-question form: optional ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} suggested one-click answers, up to ${MAX_ASK_OPTION_BYTES} characters each.`,
|
|
7402
|
-
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.`,
|
|
7403
|
-
"questions.headline": `The one-sentence question ("Do we go to prod?") \u2014 required for each item, up to ${MAX_ASK_HEADLINE_BYTES} characters.`,
|
|
7404
|
-
"questions.body": `Optional short framing beneath the headline, up to ${MAX_ASK_QUESTION_BYTES} characters. NOT a report.`,
|
|
7405
|
-
"questions.options": `Optional ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} one-click answers for this question, up to ${MAX_ASK_OPTION_BYTES} characters each.`,
|
|
7406
|
-
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.",
|
|
7407
|
-
"replaces.askId": "The id of the open ask to withdraw.",
|
|
7408
|
-
"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.`
|
|
7409
|
-
},
|
|
7410
|
-
cancel_wake: {},
|
|
7411
|
-
skip_turn: {
|
|
7412
|
-
reason: `Short reason you are declining \u2014 used for telemetry/debugging, never posted. Up to ${MAX_SKIP_REASON_BYTES} characters.`
|
|
7413
|
-
},
|
|
7414
|
-
wake_me: {
|
|
7415
|
-
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.",
|
|
7416
|
-
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.",
|
|
7417
|
-
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").`
|
|
7418
|
-
},
|
|
7419
|
-
withdraw_ask: {
|
|
7420
|
-
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.",
|
|
7421
|
-
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.`
|
|
7422
|
-
}
|
|
7423
|
-
};
|
|
7424
|
-
|
|
7425
|
-
// src/turn-control-tools.ts
|
|
7426
7444
|
import { z as z13 } from "zod";
|
|
7427
7445
|
var COMPANION_LOCAL_MCP_SERVER = "cabane_companion";
|
|
7428
7446
|
var COMPANION_LOCAL_TOOL_GLOB = `mcp__${COMPANION_LOCAL_MCP_SERVER}__*`;
|
|
@@ -7718,6 +7736,17 @@ function describeErrorBody(body) {
|
|
|
7718
7736
|
if (text.length === 0) return void 0;
|
|
7719
7737
|
return text.length > ERROR_BODY_LOG_CAP ? `${text.slice(0, ERROR_BODY_LOG_CAP)}\u2026` : text;
|
|
7720
7738
|
}
|
|
7739
|
+
function apiErrorLogFields(err) {
|
|
7740
|
+
const fields = {
|
|
7741
|
+
err: err instanceof Error ? err.message : String(err)
|
|
7742
|
+
};
|
|
7743
|
+
if (err instanceof ApiError) {
|
|
7744
|
+
fields.status = err.status;
|
|
7745
|
+
const body = describeErrorBody(err.body);
|
|
7746
|
+
if (body !== void 0) fields.responseBody = body;
|
|
7747
|
+
}
|
|
7748
|
+
return fields;
|
|
7749
|
+
}
|
|
7721
7750
|
|
|
7722
7751
|
// src/turn-seq-floor.ts
|
|
7723
7752
|
var SeqFloorUnavailable = class extends Error {
|
|
@@ -7986,10 +8015,7 @@ var TurnCommitter = class {
|
|
|
7986
8015
|
constructor(deps) {
|
|
7987
8016
|
this.deps = deps;
|
|
7988
8017
|
this.onError = (err, hook) => {
|
|
7989
|
-
deps.log.warn(
|
|
7990
|
-
{ err: err instanceof Error ? err.message : String(err), hook },
|
|
7991
|
-
"dispatcher: transcript callback failed"
|
|
7992
|
-
);
|
|
8018
|
+
deps.log.warn({ ...apiErrorLogFields(err), hook }, "dispatcher: transcript callback failed");
|
|
7993
8019
|
deps.onCommitFailed?.(err);
|
|
7994
8020
|
};
|
|
7995
8021
|
const commit = {
|
|
@@ -8186,7 +8212,17 @@ async function absorbBundledLoss(ctx, aborted, outcome) {
|
|
|
8186
8212
|
|
|
8187
8213
|
// src/turn-execution.ts
|
|
8188
8214
|
var PREPARING_TOOL_NAME = "preparing";
|
|
8189
|
-
var
|
|
8215
|
+
var ERROR_REASON_MAX = 200;
|
|
8216
|
+
function missingSecretReason(missing) {
|
|
8217
|
+
const prefix = "missing_secret: ";
|
|
8218
|
+
for (let shown = missing.length; shown >= 0; shown--) {
|
|
8219
|
+
const omitted = missing.length - shown;
|
|
8220
|
+
const parts = [...missing.slice(0, shown), ...omitted > 0 ? [`+${omitted}`] : []];
|
|
8221
|
+
const reason = prefix + parts.join(",");
|
|
8222
|
+
if (reason.length <= ERROR_REASON_MAX) return reason;
|
|
8223
|
+
}
|
|
8224
|
+
return `${prefix}+${missing.length}`;
|
|
8225
|
+
}
|
|
8190
8226
|
var RUNTIME_UNAVAILABLE_PREFIX = "**This agent's runtime isn't available on this companion.** The model this agent uses needs a runtime this device isn't running, so I can't run this turn here. Details:";
|
|
8191
8227
|
var UNEXPECTED_ROLE_REASON = `refused the wake trigger (role "system" isn't dispatchable on this companion) \u2014 the companion is likely running outdated code; refresh it, then re-address the agent`;
|
|
8192
8228
|
var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
|
|
@@ -8377,7 +8413,7 @@ var TurnExecution = class {
|
|
|
8377
8413
|
);
|
|
8378
8414
|
} catch (err) {
|
|
8379
8415
|
turnLog.warn(
|
|
8380
|
-
{
|
|
8416
|
+
{ ...apiErrorLogFields(err), turnId },
|
|
8381
8417
|
"dispatcher: admitted-run settle failed terminally; server age-sweep is the backstop"
|
|
8382
8418
|
);
|
|
8383
8419
|
}
|
|
@@ -8406,6 +8442,12 @@ var TurnExecution = class {
|
|
|
8406
8442
|
);
|
|
8407
8443
|
throw this.concluded("turn_context_not_found");
|
|
8408
8444
|
}
|
|
8445
|
+
if (status === 409 && apiErrorCode(err) === "dispatch_not_admitted") {
|
|
8446
|
+
turnLog.warn(
|
|
8447
|
+
"dispatcher: turn-context 409 dispatch_not_admitted (wake already resolved); skipping"
|
|
8448
|
+
);
|
|
8449
|
+
throw this.concluded("turn_context_not_admitted");
|
|
8450
|
+
}
|
|
8409
8451
|
const reason = err instanceof Error ? err.message : String(err);
|
|
8410
8452
|
turnLog.error({ err: reason }, "dispatcher: failed to fetch turn context");
|
|
8411
8453
|
const fetchReason = `fetch_failed: ${reason}`;
|
|
@@ -8444,24 +8486,9 @@ var TurnExecution = class {
|
|
|
8444
8486
|
secretStore
|
|
8445
8487
|
);
|
|
8446
8488
|
if (missing.length > 0) {
|
|
8447
|
-
const list = missing.map((n) => `\`${n}\``).join(", ");
|
|
8448
8489
|
turnLog.error({ missing }, "dispatcher: turn needs secrets not declared on this companion");
|
|
8449
|
-
|
|
8450
|
-
|
|
8451
|
-
body: `${MISSING_SECRET_PREFIX} ${list}`,
|
|
8452
|
-
kind: "runtime_notice",
|
|
8453
|
-
turnId: randomUUID3(),
|
|
8454
|
-
// CT113: even a turn that fails before it runs answers a message.
|
|
8455
|
-
parentMessageId: payload.messageId
|
|
8456
|
-
});
|
|
8457
|
-
} catch (postErr) {
|
|
8458
|
-
turnLog.warn(
|
|
8459
|
-
{ err: postErr instanceof Error ? postErr.message : String(postErr) },
|
|
8460
|
-
"dispatcher: missing-secret notice post failed"
|
|
8461
|
-
);
|
|
8462
|
-
}
|
|
8463
|
-
const reason = `missing_secret: ${missing.join(",")}`;
|
|
8464
|
-
throw this.concluded(reason);
|
|
8490
|
+
const reason = missingSecretReason(missing);
|
|
8491
|
+
throw this.concluded(reason, reason);
|
|
8465
8492
|
}
|
|
8466
8493
|
this.resolvedMcpServers = resolvedMcpServers;
|
|
8467
8494
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.101",
|
|
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",
|