@cabane/companion 0.6.52 → 0.6.54
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 +248 -6
- package/dist/runtime.js +248 -6
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -232,6 +232,9 @@ function realAccountHome() {
|
|
|
232
232
|
}
|
|
233
233
|
return realHomeCache;
|
|
234
234
|
}
|
|
235
|
+
function agentCompanionHome() {
|
|
236
|
+
return join(cabaneDir(), "agent-home");
|
|
237
|
+
}
|
|
235
238
|
function cabaneDir() {
|
|
236
239
|
const dir2 = join(process.env.CABANE_COMPANION_HOME || homedir(), ".cabane");
|
|
237
240
|
if (process.env.VITEST) {
|
|
@@ -5530,6 +5533,7 @@ function readToolItem(item, eventType) {
|
|
|
5530
5533
|
if (type === "mcp_tool_call") {
|
|
5531
5534
|
const tool2 = str(item.tool) ?? "tool";
|
|
5532
5535
|
const server = str(item.server);
|
|
5536
|
+
const result = readMcpResult(item.result);
|
|
5533
5537
|
return {
|
|
5534
5538
|
id,
|
|
5535
5539
|
// A cabane tool reads best as its bare verb (`read`); a non-cabane server's
|
|
@@ -5537,7 +5541,7 @@ function readToolItem(item, eventType) {
|
|
|
5537
5541
|
name: server && server !== "cabane" ? `${server}.${tool2}` : tool2,
|
|
5538
5542
|
input: item.arguments,
|
|
5539
5543
|
status: toStatus(item.status),
|
|
5540
|
-
...item.status === "completed" ? { result
|
|
5544
|
+
...item.status === "completed" ? { result } : item.status === "failed" ? { result: hasMcpReceipt(result) ? result : readMcpError(item.error) } : {}
|
|
5541
5545
|
};
|
|
5542
5546
|
}
|
|
5543
5547
|
if (type === "web_search") {
|
|
@@ -5577,9 +5581,38 @@ function toStatus(v) {
|
|
|
5577
5581
|
}
|
|
5578
5582
|
function readMcpResult(result) {
|
|
5579
5583
|
const rec2 = asRecord2(result);
|
|
5580
|
-
if (rec2
|
|
5584
|
+
if (!rec2) return result;
|
|
5585
|
+
if (hasOnlyNonTextContentBlocks(rec2.content)) return rec2.content;
|
|
5586
|
+
if (rec2.structured_content !== void 0 && rec2.structured_content !== null) {
|
|
5587
|
+
return rec2.structured_content;
|
|
5588
|
+
}
|
|
5589
|
+
if (rec2.structuredContent !== void 0 && rec2.structuredContent !== null) {
|
|
5590
|
+
return rec2.structuredContent;
|
|
5591
|
+
}
|
|
5592
|
+
if ("content" in rec2) return rec2.content;
|
|
5581
5593
|
return result;
|
|
5582
5594
|
}
|
|
5595
|
+
function hasOnlyNonTextContentBlocks(content) {
|
|
5596
|
+
if (!Array.isArray(content) || content.length === 0) return false;
|
|
5597
|
+
let sawTypedBlock = false;
|
|
5598
|
+
for (const raw of content) {
|
|
5599
|
+
const block = asRecord2(raw);
|
|
5600
|
+
const type = block && str(block.type);
|
|
5601
|
+
if (!type) continue;
|
|
5602
|
+
sawTypedBlock = true;
|
|
5603
|
+
if (type === "text" && typeof block?.text === "string" && block.text.trim().length > 0) {
|
|
5604
|
+
return false;
|
|
5605
|
+
}
|
|
5606
|
+
}
|
|
5607
|
+
return sawTypedBlock;
|
|
5608
|
+
}
|
|
5609
|
+
function hasMcpReceipt(result) {
|
|
5610
|
+
if (result === void 0 || result === null) return false;
|
|
5611
|
+
if (typeof result === "string") return result.trim().length > 0;
|
|
5612
|
+
if (Array.isArray(result)) return result.length > 0;
|
|
5613
|
+
if (typeof result === "object") return Object.keys(result).length > 0;
|
|
5614
|
+
return true;
|
|
5615
|
+
}
|
|
5583
5616
|
function readMcpError(error) {
|
|
5584
5617
|
const rec2 = asRecord2(error);
|
|
5585
5618
|
return rec2 && str(rec2.message) || "error";
|
|
@@ -6103,6 +6136,25 @@ var errorItem = (id, message) => ({
|
|
|
6103
6136
|
type: "item.completed",
|
|
6104
6137
|
item: { id, type: "error", message }
|
|
6105
6138
|
});
|
|
6139
|
+
var SDK_SUCCESS = { ok: true, result: { source: "native" }, logs: "", truncated: false };
|
|
6140
|
+
var SDK_FAILURE = {
|
|
6141
|
+
ok: false,
|
|
6142
|
+
result: null,
|
|
6143
|
+
preview: '{"rows":[1,2,3]}',
|
|
6144
|
+
logs: "",
|
|
6145
|
+
truncated: false,
|
|
6146
|
+
error: { kind: "output_too_large", bytes: 169167, limit: 65536 },
|
|
6147
|
+
warnings: [],
|
|
6148
|
+
committed: {
|
|
6149
|
+
calls: [
|
|
6150
|
+
{
|
|
6151
|
+
method: "conversations.update",
|
|
6152
|
+
args: { id: "conv-1", opts: { title: "Renamed before failure" } }
|
|
6153
|
+
}
|
|
6154
|
+
]
|
|
6155
|
+
},
|
|
6156
|
+
inFlight: null
|
|
6157
|
+
};
|
|
6106
6158
|
var sessionEvent3 = (threadId, cwd = DIR2, degraded = false, promptFingerprint = fingerprintPrompt(PROMPT)) => ({
|
|
6107
6159
|
type: "session",
|
|
6108
6160
|
state: encodeSession3({ threadId, cwd, promptFingerprint }),
|
|
@@ -6182,6 +6234,194 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6182
6234
|
{ type: "result", ok: true }
|
|
6183
6235
|
]
|
|
6184
6236
|
},
|
|
6237
|
+
{
|
|
6238
|
+
// CT1211: recorded SDK success shape. Codex exposes the text compatibility
|
|
6239
|
+
// copy and the native object together; normalization must keep the object.
|
|
6240
|
+
name: "MCP success prefers native structured content",
|
|
6241
|
+
request: makeRequest3(),
|
|
6242
|
+
nativeStream: [
|
|
6243
|
+
threadStarted(NEW_THREAD_ID),
|
|
6244
|
+
toolFrame("item.started", {
|
|
6245
|
+
id: "sdk-success",
|
|
6246
|
+
type: "mcp_tool_call",
|
|
6247
|
+
server: "cabane",
|
|
6248
|
+
tool: "sdk",
|
|
6249
|
+
arguments: { code: 'return { source: "native" }' },
|
|
6250
|
+
status: "in_progress"
|
|
6251
|
+
}),
|
|
6252
|
+
toolFrame("item.completed", {
|
|
6253
|
+
id: "sdk-success",
|
|
6254
|
+
type: "mcp_tool_call",
|
|
6255
|
+
server: "cabane",
|
|
6256
|
+
tool: "sdk",
|
|
6257
|
+
arguments: { code: 'return { source: "native" }' },
|
|
6258
|
+
status: "completed",
|
|
6259
|
+
result: {
|
|
6260
|
+
content: [{ type: "text", text: JSON.stringify(SDK_SUCCESS) }],
|
|
6261
|
+
structured_content: SDK_SUCCESS
|
|
6262
|
+
}
|
|
6263
|
+
}),
|
|
6264
|
+
turnCompleted()
|
|
6265
|
+
],
|
|
6266
|
+
expected: [
|
|
6267
|
+
sessionEvent3(NEW_THREAD_ID),
|
|
6268
|
+
{
|
|
6269
|
+
type: "tool",
|
|
6270
|
+
id: "sdk-success",
|
|
6271
|
+
name: "sdk",
|
|
6272
|
+
phase: "start",
|
|
6273
|
+
summary: "",
|
|
6274
|
+
input: { code: 'return { source: "native" }' }
|
|
6275
|
+
},
|
|
6276
|
+
{
|
|
6277
|
+
type: "tool",
|
|
6278
|
+
id: "sdk-success",
|
|
6279
|
+
name: "sdk",
|
|
6280
|
+
phase: "done",
|
|
6281
|
+
summary: "",
|
|
6282
|
+
input: { code: 'return { source: "native" }' },
|
|
6283
|
+
result: SDK_SUCCESS
|
|
6284
|
+
},
|
|
6285
|
+
{ type: "result", ok: true }
|
|
6286
|
+
]
|
|
6287
|
+
},
|
|
6288
|
+
{
|
|
6289
|
+
// CT1211 incident shape: MCP `isError` keeps the failed phase, while the raw
|
|
6290
|
+
// camel-case structured payload survives instead of the generic `error` token.
|
|
6291
|
+
name: "failed SDK call preserves structured receipt while remaining error phase",
|
|
6292
|
+
request: makeRequest3(),
|
|
6293
|
+
nativeStream: [
|
|
6294
|
+
threadStarted(NEW_THREAD_ID),
|
|
6295
|
+
toolFrame("item.started", {
|
|
6296
|
+
id: "sdk-failed",
|
|
6297
|
+
type: "mcp_tool_call",
|
|
6298
|
+
server: "cabane",
|
|
6299
|
+
tool: "sdk",
|
|
6300
|
+
arguments: { code: "return largeResult" },
|
|
6301
|
+
status: "in_progress"
|
|
6302
|
+
}),
|
|
6303
|
+
toolFrame("item.completed", {
|
|
6304
|
+
id: "sdk-failed",
|
|
6305
|
+
type: "mcp_tool_call",
|
|
6306
|
+
server: "cabane",
|
|
6307
|
+
tool: "sdk",
|
|
6308
|
+
arguments: { code: "return largeResult" },
|
|
6309
|
+
status: "failed",
|
|
6310
|
+
result: {
|
|
6311
|
+
content: [{ type: "text", text: JSON.stringify(SDK_FAILURE) }],
|
|
6312
|
+
structuredContent: SDK_FAILURE,
|
|
6313
|
+
isError: true
|
|
6314
|
+
},
|
|
6315
|
+
error: { message: "error" }
|
|
6316
|
+
}),
|
|
6317
|
+
turnCompleted()
|
|
6318
|
+
],
|
|
6319
|
+
expected: [
|
|
6320
|
+
sessionEvent3(NEW_THREAD_ID),
|
|
6321
|
+
{
|
|
6322
|
+
type: "tool",
|
|
6323
|
+
id: "sdk-failed",
|
|
6324
|
+
name: "sdk",
|
|
6325
|
+
phase: "start",
|
|
6326
|
+
summary: "",
|
|
6327
|
+
input: { code: "return largeResult" }
|
|
6328
|
+
},
|
|
6329
|
+
{
|
|
6330
|
+
type: "tool",
|
|
6331
|
+
id: "sdk-failed",
|
|
6332
|
+
name: "sdk",
|
|
6333
|
+
phase: "error",
|
|
6334
|
+
summary: "",
|
|
6335
|
+
input: { code: "return largeResult" },
|
|
6336
|
+
result: SDK_FAILURE
|
|
6337
|
+
},
|
|
6338
|
+
{ type: "result", ok: true }
|
|
6339
|
+
]
|
|
6340
|
+
},
|
|
6341
|
+
{
|
|
6342
|
+
// A transport/tool failure has no MCP result payload and still uses the real
|
|
6343
|
+
// adapter error message. CT1211 must not replace this fallback with `undefined`.
|
|
6344
|
+
name: "failed MCP call without result keeps error message fallback",
|
|
6345
|
+
request: makeRequest3(),
|
|
6346
|
+
nativeStream: [
|
|
6347
|
+
threadStarted(NEW_THREAD_ID),
|
|
6348
|
+
toolFrame("item.completed", {
|
|
6349
|
+
id: "mcp-failed",
|
|
6350
|
+
type: "mcp_tool_call",
|
|
6351
|
+
server: "other",
|
|
6352
|
+
tool: "lookup",
|
|
6353
|
+
arguments: { q: "receipt" },
|
|
6354
|
+
status: "failed",
|
|
6355
|
+
result: { content: [], isError: true },
|
|
6356
|
+
error: { message: "connection closed" }
|
|
6357
|
+
}),
|
|
6358
|
+
turnCompleted()
|
|
6359
|
+
],
|
|
6360
|
+
expected: [
|
|
6361
|
+
sessionEvent3(NEW_THREAD_ID),
|
|
6362
|
+
{
|
|
6363
|
+
type: "tool",
|
|
6364
|
+
id: "mcp-failed",
|
|
6365
|
+
name: "other.lookup",
|
|
6366
|
+
phase: "start",
|
|
6367
|
+
summary: "receipt",
|
|
6368
|
+
input: { q: "receipt" }
|
|
6369
|
+
},
|
|
6370
|
+
{
|
|
6371
|
+
type: "tool",
|
|
6372
|
+
id: "mcp-failed",
|
|
6373
|
+
name: "other.lookup",
|
|
6374
|
+
phase: "error",
|
|
6375
|
+
summary: "receipt",
|
|
6376
|
+
input: { q: "receipt" },
|
|
6377
|
+
result: "connection closed"
|
|
6378
|
+
},
|
|
6379
|
+
{ type: "result", ok: true }
|
|
6380
|
+
]
|
|
6381
|
+
},
|
|
6382
|
+
{
|
|
6383
|
+
// CT1147 stays intact when a connector includes structured metadata beside an
|
|
6384
|
+
// image-only MCP response: no image/base64 payload enters activity receipts.
|
|
6385
|
+
name: "image-only MCP result remains empty despite structured metadata",
|
|
6386
|
+
request: makeRequest3(),
|
|
6387
|
+
nativeStream: [
|
|
6388
|
+
threadStarted(NEW_THREAD_ID),
|
|
6389
|
+
toolFrame("item.completed", {
|
|
6390
|
+
id: "mcp-image",
|
|
6391
|
+
type: "mcp_tool_call",
|
|
6392
|
+
server: "other",
|
|
6393
|
+
tool: "render",
|
|
6394
|
+
arguments: {},
|
|
6395
|
+
status: "completed",
|
|
6396
|
+
result: {
|
|
6397
|
+
content: [{ type: "image", data: "base64-bytes", mimeType: "image/png" }],
|
|
6398
|
+
structured_content: { image: "base64-bytes" }
|
|
6399
|
+
}
|
|
6400
|
+
}),
|
|
6401
|
+
turnCompleted()
|
|
6402
|
+
],
|
|
6403
|
+
expected: [
|
|
6404
|
+
sessionEvent3(NEW_THREAD_ID),
|
|
6405
|
+
{
|
|
6406
|
+
type: "tool",
|
|
6407
|
+
id: "mcp-image",
|
|
6408
|
+
name: "other.render",
|
|
6409
|
+
phase: "start",
|
|
6410
|
+
summary: "",
|
|
6411
|
+
input: {}
|
|
6412
|
+
},
|
|
6413
|
+
{
|
|
6414
|
+
type: "tool",
|
|
6415
|
+
id: "mcp-image",
|
|
6416
|
+
name: "other.render",
|
|
6417
|
+
phase: "done",
|
|
6418
|
+
summary: "",
|
|
6419
|
+
input: {},
|
|
6420
|
+
result: [{ type: "image", data: "base64-bytes", mimeType: "image/png" }]
|
|
6421
|
+
},
|
|
6422
|
+
{ type: "result", ok: true }
|
|
6423
|
+
]
|
|
6424
|
+
},
|
|
6185
6425
|
{
|
|
6186
6426
|
// A streaming tool whose args arrive on a later snapshot: `item.started` carries
|
|
6187
6427
|
// empty args, `item.updated` fills them, `item.completed` finishes. The `start`
|
|
@@ -6738,6 +6978,7 @@ var COMPANION_LOCAL_TOOL_GLOB = `mcp__${COMPANION_LOCAL_MCP_SERVER}__*`;
|
|
|
6738
6978
|
var SKIP_TURN_TOOL = "skip_turn";
|
|
6739
6979
|
var ASK_TOOL = "ask";
|
|
6740
6980
|
var ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${ASK_TOOL}`;
|
|
6981
|
+
var MAX_ASK_ITEMS = 20;
|
|
6741
6982
|
var SUB_AGENT_TOOL = "sub_agent";
|
|
6742
6983
|
var SUB_AGENT_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${SUB_AGENT_TOOL}`;
|
|
6743
6984
|
var WAKE_ME_TOOL = "wake_me";
|
|
@@ -6809,7 +7050,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
6809
7050
|
...askState ? [
|
|
6810
7051
|
tool(
|
|
6811
7052
|
ASK_TOOL,
|
|
6812
|
-
|
|
7053
|
+
`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? }\`. **Prefer the list over cramming the extra decisions into prose or dropping them** \u2014 end the turn with one ask carrying every question, never pick one and bury the rest. Each question keeps the same form rules: a one-sentence \`headline\`, a short \`body\` frame (NOT a report \u2014 your status, links, and detail go in your REPLY, and the body renders inline markdown only: links/emphasis/inline code, no bulleted 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. An option can be a short button label or a whole sentence. Provide EITHER \`question\` (single) or \`questions\` (array), never both. The ask is a first-class attention item aimed at that person; your final reply carries the surrounding CONTEXT (what you found, why you're stuck), the ask carries the QUESTION(S). An open ask marks you as blocked until EVERY question is answered, so raise one only when you truly can't proceed \u2014 never ceremonially. One ask per turn (last call wins). After asking, stop \u2014 when the person replies addressed to you, the ask resolves and you resume; other people's or agents' messages may wake you but leave it open. Targets a human only; to hand work to another AGENT use summon/dispatch instead.`,
|
|
6813
7054
|
{
|
|
6814
7055
|
targetUserId: z13.string().uuid().describe(
|
|
6815
7056
|
"The workspace member (human) to ask \u2014 a user id, from your turn context's roster."
|
|
@@ -6829,8 +7070,8 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
6829
7070
|
body: z13.string().min(1).max(400).optional().describe("Optional short framing beneath the headline. NOT a report."),
|
|
6830
7071
|
options: z13.array(z13.string().min(1).max(200)).min(2).max(4).optional().describe("Optional 2\u20134 one-click answers for this question.")
|
|
6831
7072
|
})
|
|
6832
|
-
).min(1).max(
|
|
6833
|
-
|
|
7073
|
+
).min(1).max(MAX_ASK_ITEMS).optional().describe(
|
|
7074
|
+
`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.`
|
|
6834
7075
|
)
|
|
6835
7076
|
},
|
|
6836
7077
|
async (args) => {
|
|
@@ -8010,7 +8251,8 @@ ${reason}`,
|
|
|
8010
8251
|
}
|
|
8011
8252
|
const turnEnv = {
|
|
8012
8253
|
...hookEnv,
|
|
8013
|
-
CABANE_HOST_ACCESS: turnContext.policy.hostFs ? "1" : "0"
|
|
8254
|
+
CABANE_HOST_ACCESS: turnContext.policy.hostFs ? "1" : "0",
|
|
8255
|
+
CABANE_COMPANION_HOME: agentCompanionHome()
|
|
8014
8256
|
};
|
|
8015
8257
|
const key = runKey(payload.conversationId, payload.agentId);
|
|
8016
8258
|
const abortController = new AbortController();
|
package/dist/runtime.js
CHANGED
|
@@ -224,6 +224,9 @@ function realAccountHome() {
|
|
|
224
224
|
}
|
|
225
225
|
return realHomeCache;
|
|
226
226
|
}
|
|
227
|
+
function agentCompanionHome() {
|
|
228
|
+
return join(cabaneDir(), "agent-home");
|
|
229
|
+
}
|
|
227
230
|
function cabaneDir() {
|
|
228
231
|
const dir2 = join(process.env.CABANE_COMPANION_HOME || homedir(), ".cabane");
|
|
229
232
|
if (process.env.VITEST) {
|
|
@@ -5029,6 +5032,7 @@ function readToolItem(item, eventType) {
|
|
|
5029
5032
|
if (type === "mcp_tool_call") {
|
|
5030
5033
|
const tool2 = str(item.tool) ?? "tool";
|
|
5031
5034
|
const server = str(item.server);
|
|
5035
|
+
const result = readMcpResult(item.result);
|
|
5032
5036
|
return {
|
|
5033
5037
|
id,
|
|
5034
5038
|
// A cabane tool reads best as its bare verb (`read`); a non-cabane server's
|
|
@@ -5036,7 +5040,7 @@ function readToolItem(item, eventType) {
|
|
|
5036
5040
|
name: server && server !== "cabane" ? `${server}.${tool2}` : tool2,
|
|
5037
5041
|
input: item.arguments,
|
|
5038
5042
|
status: toStatus(item.status),
|
|
5039
|
-
...item.status === "completed" ? { result
|
|
5043
|
+
...item.status === "completed" ? { result } : item.status === "failed" ? { result: hasMcpReceipt(result) ? result : readMcpError(item.error) } : {}
|
|
5040
5044
|
};
|
|
5041
5045
|
}
|
|
5042
5046
|
if (type === "web_search") {
|
|
@@ -5076,9 +5080,38 @@ function toStatus(v) {
|
|
|
5076
5080
|
}
|
|
5077
5081
|
function readMcpResult(result) {
|
|
5078
5082
|
const rec = asRecord2(result);
|
|
5079
|
-
if (rec
|
|
5083
|
+
if (!rec) return result;
|
|
5084
|
+
if (hasOnlyNonTextContentBlocks(rec.content)) return rec.content;
|
|
5085
|
+
if (rec.structured_content !== void 0 && rec.structured_content !== null) {
|
|
5086
|
+
return rec.structured_content;
|
|
5087
|
+
}
|
|
5088
|
+
if (rec.structuredContent !== void 0 && rec.structuredContent !== null) {
|
|
5089
|
+
return rec.structuredContent;
|
|
5090
|
+
}
|
|
5091
|
+
if ("content" in rec) return rec.content;
|
|
5080
5092
|
return result;
|
|
5081
5093
|
}
|
|
5094
|
+
function hasOnlyNonTextContentBlocks(content) {
|
|
5095
|
+
if (!Array.isArray(content) || content.length === 0) return false;
|
|
5096
|
+
let sawTypedBlock = false;
|
|
5097
|
+
for (const raw of content) {
|
|
5098
|
+
const block = asRecord2(raw);
|
|
5099
|
+
const type = block && str(block.type);
|
|
5100
|
+
if (!type) continue;
|
|
5101
|
+
sawTypedBlock = true;
|
|
5102
|
+
if (type === "text" && typeof block?.text === "string" && block.text.trim().length > 0) {
|
|
5103
|
+
return false;
|
|
5104
|
+
}
|
|
5105
|
+
}
|
|
5106
|
+
return sawTypedBlock;
|
|
5107
|
+
}
|
|
5108
|
+
function hasMcpReceipt(result) {
|
|
5109
|
+
if (result === void 0 || result === null) return false;
|
|
5110
|
+
if (typeof result === "string") return result.trim().length > 0;
|
|
5111
|
+
if (Array.isArray(result)) return result.length > 0;
|
|
5112
|
+
if (typeof result === "object") return Object.keys(result).length > 0;
|
|
5113
|
+
return true;
|
|
5114
|
+
}
|
|
5082
5115
|
function readMcpError(error) {
|
|
5083
5116
|
const rec = asRecord2(error);
|
|
5084
5117
|
return rec && str(rec.message) || "error";
|
|
@@ -5602,6 +5635,25 @@ var errorItem = (id, message) => ({
|
|
|
5602
5635
|
type: "item.completed",
|
|
5603
5636
|
item: { id, type: "error", message }
|
|
5604
5637
|
});
|
|
5638
|
+
var SDK_SUCCESS = { ok: true, result: { source: "native" }, logs: "", truncated: false };
|
|
5639
|
+
var SDK_FAILURE = {
|
|
5640
|
+
ok: false,
|
|
5641
|
+
result: null,
|
|
5642
|
+
preview: '{"rows":[1,2,3]}',
|
|
5643
|
+
logs: "",
|
|
5644
|
+
truncated: false,
|
|
5645
|
+
error: { kind: "output_too_large", bytes: 169167, limit: 65536 },
|
|
5646
|
+
warnings: [],
|
|
5647
|
+
committed: {
|
|
5648
|
+
calls: [
|
|
5649
|
+
{
|
|
5650
|
+
method: "conversations.update",
|
|
5651
|
+
args: { id: "conv-1", opts: { title: "Renamed before failure" } }
|
|
5652
|
+
}
|
|
5653
|
+
]
|
|
5654
|
+
},
|
|
5655
|
+
inFlight: null
|
|
5656
|
+
};
|
|
5605
5657
|
var sessionEvent3 = (threadId, cwd = DIR2, degraded = false, promptFingerprint = fingerprintPrompt(PROMPT)) => ({
|
|
5606
5658
|
type: "session",
|
|
5607
5659
|
state: encodeSession3({ threadId, cwd, promptFingerprint }),
|
|
@@ -5681,6 +5733,194 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
5681
5733
|
{ type: "result", ok: true }
|
|
5682
5734
|
]
|
|
5683
5735
|
},
|
|
5736
|
+
{
|
|
5737
|
+
// CT1211: recorded SDK success shape. Codex exposes the text compatibility
|
|
5738
|
+
// copy and the native object together; normalization must keep the object.
|
|
5739
|
+
name: "MCP success prefers native structured content",
|
|
5740
|
+
request: makeRequest3(),
|
|
5741
|
+
nativeStream: [
|
|
5742
|
+
threadStarted(NEW_THREAD_ID),
|
|
5743
|
+
toolFrame("item.started", {
|
|
5744
|
+
id: "sdk-success",
|
|
5745
|
+
type: "mcp_tool_call",
|
|
5746
|
+
server: "cabane",
|
|
5747
|
+
tool: "sdk",
|
|
5748
|
+
arguments: { code: 'return { source: "native" }' },
|
|
5749
|
+
status: "in_progress"
|
|
5750
|
+
}),
|
|
5751
|
+
toolFrame("item.completed", {
|
|
5752
|
+
id: "sdk-success",
|
|
5753
|
+
type: "mcp_tool_call",
|
|
5754
|
+
server: "cabane",
|
|
5755
|
+
tool: "sdk",
|
|
5756
|
+
arguments: { code: 'return { source: "native" }' },
|
|
5757
|
+
status: "completed",
|
|
5758
|
+
result: {
|
|
5759
|
+
content: [{ type: "text", text: JSON.stringify(SDK_SUCCESS) }],
|
|
5760
|
+
structured_content: SDK_SUCCESS
|
|
5761
|
+
}
|
|
5762
|
+
}),
|
|
5763
|
+
turnCompleted()
|
|
5764
|
+
],
|
|
5765
|
+
expected: [
|
|
5766
|
+
sessionEvent3(NEW_THREAD_ID),
|
|
5767
|
+
{
|
|
5768
|
+
type: "tool",
|
|
5769
|
+
id: "sdk-success",
|
|
5770
|
+
name: "sdk",
|
|
5771
|
+
phase: "start",
|
|
5772
|
+
summary: "",
|
|
5773
|
+
input: { code: 'return { source: "native" }' }
|
|
5774
|
+
},
|
|
5775
|
+
{
|
|
5776
|
+
type: "tool",
|
|
5777
|
+
id: "sdk-success",
|
|
5778
|
+
name: "sdk",
|
|
5779
|
+
phase: "done",
|
|
5780
|
+
summary: "",
|
|
5781
|
+
input: { code: 'return { source: "native" }' },
|
|
5782
|
+
result: SDK_SUCCESS
|
|
5783
|
+
},
|
|
5784
|
+
{ type: "result", ok: true }
|
|
5785
|
+
]
|
|
5786
|
+
},
|
|
5787
|
+
{
|
|
5788
|
+
// CT1211 incident shape: MCP `isError` keeps the failed phase, while the raw
|
|
5789
|
+
// camel-case structured payload survives instead of the generic `error` token.
|
|
5790
|
+
name: "failed SDK call preserves structured receipt while remaining error phase",
|
|
5791
|
+
request: makeRequest3(),
|
|
5792
|
+
nativeStream: [
|
|
5793
|
+
threadStarted(NEW_THREAD_ID),
|
|
5794
|
+
toolFrame("item.started", {
|
|
5795
|
+
id: "sdk-failed",
|
|
5796
|
+
type: "mcp_tool_call",
|
|
5797
|
+
server: "cabane",
|
|
5798
|
+
tool: "sdk",
|
|
5799
|
+
arguments: { code: "return largeResult" },
|
|
5800
|
+
status: "in_progress"
|
|
5801
|
+
}),
|
|
5802
|
+
toolFrame("item.completed", {
|
|
5803
|
+
id: "sdk-failed",
|
|
5804
|
+
type: "mcp_tool_call",
|
|
5805
|
+
server: "cabane",
|
|
5806
|
+
tool: "sdk",
|
|
5807
|
+
arguments: { code: "return largeResult" },
|
|
5808
|
+
status: "failed",
|
|
5809
|
+
result: {
|
|
5810
|
+
content: [{ type: "text", text: JSON.stringify(SDK_FAILURE) }],
|
|
5811
|
+
structuredContent: SDK_FAILURE,
|
|
5812
|
+
isError: true
|
|
5813
|
+
},
|
|
5814
|
+
error: { message: "error" }
|
|
5815
|
+
}),
|
|
5816
|
+
turnCompleted()
|
|
5817
|
+
],
|
|
5818
|
+
expected: [
|
|
5819
|
+
sessionEvent3(NEW_THREAD_ID),
|
|
5820
|
+
{
|
|
5821
|
+
type: "tool",
|
|
5822
|
+
id: "sdk-failed",
|
|
5823
|
+
name: "sdk",
|
|
5824
|
+
phase: "start",
|
|
5825
|
+
summary: "",
|
|
5826
|
+
input: { code: "return largeResult" }
|
|
5827
|
+
},
|
|
5828
|
+
{
|
|
5829
|
+
type: "tool",
|
|
5830
|
+
id: "sdk-failed",
|
|
5831
|
+
name: "sdk",
|
|
5832
|
+
phase: "error",
|
|
5833
|
+
summary: "",
|
|
5834
|
+
input: { code: "return largeResult" },
|
|
5835
|
+
result: SDK_FAILURE
|
|
5836
|
+
},
|
|
5837
|
+
{ type: "result", ok: true }
|
|
5838
|
+
]
|
|
5839
|
+
},
|
|
5840
|
+
{
|
|
5841
|
+
// A transport/tool failure has no MCP result payload and still uses the real
|
|
5842
|
+
// adapter error message. CT1211 must not replace this fallback with `undefined`.
|
|
5843
|
+
name: "failed MCP call without result keeps error message fallback",
|
|
5844
|
+
request: makeRequest3(),
|
|
5845
|
+
nativeStream: [
|
|
5846
|
+
threadStarted(NEW_THREAD_ID),
|
|
5847
|
+
toolFrame("item.completed", {
|
|
5848
|
+
id: "mcp-failed",
|
|
5849
|
+
type: "mcp_tool_call",
|
|
5850
|
+
server: "other",
|
|
5851
|
+
tool: "lookup",
|
|
5852
|
+
arguments: { q: "receipt" },
|
|
5853
|
+
status: "failed",
|
|
5854
|
+
result: { content: [], isError: true },
|
|
5855
|
+
error: { message: "connection closed" }
|
|
5856
|
+
}),
|
|
5857
|
+
turnCompleted()
|
|
5858
|
+
],
|
|
5859
|
+
expected: [
|
|
5860
|
+
sessionEvent3(NEW_THREAD_ID),
|
|
5861
|
+
{
|
|
5862
|
+
type: "tool",
|
|
5863
|
+
id: "mcp-failed",
|
|
5864
|
+
name: "other.lookup",
|
|
5865
|
+
phase: "start",
|
|
5866
|
+
summary: "receipt",
|
|
5867
|
+
input: { q: "receipt" }
|
|
5868
|
+
},
|
|
5869
|
+
{
|
|
5870
|
+
type: "tool",
|
|
5871
|
+
id: "mcp-failed",
|
|
5872
|
+
name: "other.lookup",
|
|
5873
|
+
phase: "error",
|
|
5874
|
+
summary: "receipt",
|
|
5875
|
+
input: { q: "receipt" },
|
|
5876
|
+
result: "connection closed"
|
|
5877
|
+
},
|
|
5878
|
+
{ type: "result", ok: true }
|
|
5879
|
+
]
|
|
5880
|
+
},
|
|
5881
|
+
{
|
|
5882
|
+
// CT1147 stays intact when a connector includes structured metadata beside an
|
|
5883
|
+
// image-only MCP response: no image/base64 payload enters activity receipts.
|
|
5884
|
+
name: "image-only MCP result remains empty despite structured metadata",
|
|
5885
|
+
request: makeRequest3(),
|
|
5886
|
+
nativeStream: [
|
|
5887
|
+
threadStarted(NEW_THREAD_ID),
|
|
5888
|
+
toolFrame("item.completed", {
|
|
5889
|
+
id: "mcp-image",
|
|
5890
|
+
type: "mcp_tool_call",
|
|
5891
|
+
server: "other",
|
|
5892
|
+
tool: "render",
|
|
5893
|
+
arguments: {},
|
|
5894
|
+
status: "completed",
|
|
5895
|
+
result: {
|
|
5896
|
+
content: [{ type: "image", data: "base64-bytes", mimeType: "image/png" }],
|
|
5897
|
+
structured_content: { image: "base64-bytes" }
|
|
5898
|
+
}
|
|
5899
|
+
}),
|
|
5900
|
+
turnCompleted()
|
|
5901
|
+
],
|
|
5902
|
+
expected: [
|
|
5903
|
+
sessionEvent3(NEW_THREAD_ID),
|
|
5904
|
+
{
|
|
5905
|
+
type: "tool",
|
|
5906
|
+
id: "mcp-image",
|
|
5907
|
+
name: "other.render",
|
|
5908
|
+
phase: "start",
|
|
5909
|
+
summary: "",
|
|
5910
|
+
input: {}
|
|
5911
|
+
},
|
|
5912
|
+
{
|
|
5913
|
+
type: "tool",
|
|
5914
|
+
id: "mcp-image",
|
|
5915
|
+
name: "other.render",
|
|
5916
|
+
phase: "done",
|
|
5917
|
+
summary: "",
|
|
5918
|
+
input: {},
|
|
5919
|
+
result: [{ type: "image", data: "base64-bytes", mimeType: "image/png" }]
|
|
5920
|
+
},
|
|
5921
|
+
{ type: "result", ok: true }
|
|
5922
|
+
]
|
|
5923
|
+
},
|
|
5684
5924
|
{
|
|
5685
5925
|
// A streaming tool whose args arrive on a later snapshot: `item.started` carries
|
|
5686
5926
|
// empty args, `item.updated` fills them, `item.completed` finishes. The `start`
|
|
@@ -6237,6 +6477,7 @@ var COMPANION_LOCAL_TOOL_GLOB = `mcp__${COMPANION_LOCAL_MCP_SERVER}__*`;
|
|
|
6237
6477
|
var SKIP_TURN_TOOL = "skip_turn";
|
|
6238
6478
|
var ASK_TOOL = "ask";
|
|
6239
6479
|
var ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${ASK_TOOL}`;
|
|
6480
|
+
var MAX_ASK_ITEMS = 20;
|
|
6240
6481
|
var SUB_AGENT_TOOL = "sub_agent";
|
|
6241
6482
|
var SUB_AGENT_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${SUB_AGENT_TOOL}`;
|
|
6242
6483
|
var WAKE_ME_TOOL = "wake_me";
|
|
@@ -6308,7 +6549,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
6308
6549
|
...askState ? [
|
|
6309
6550
|
tool(
|
|
6310
6551
|
ASK_TOOL,
|
|
6311
|
-
|
|
6552
|
+
`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? }\`. **Prefer the list over cramming the extra decisions into prose or dropping them** \u2014 end the turn with one ask carrying every question, never pick one and bury the rest. Each question keeps the same form rules: a one-sentence \`headline\`, a short \`body\` frame (NOT a report \u2014 your status, links, and detail go in your REPLY, and the body renders inline markdown only: links/emphasis/inline code, no bulleted 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. An option can be a short button label or a whole sentence. Provide EITHER \`question\` (single) or \`questions\` (array), never both. The ask is a first-class attention item aimed at that person; your final reply carries the surrounding CONTEXT (what you found, why you're stuck), the ask carries the QUESTION(S). An open ask marks you as blocked until EVERY question is answered, so raise one only when you truly can't proceed \u2014 never ceremonially. One ask per turn (last call wins). After asking, stop \u2014 when the person replies addressed to you, the ask resolves and you resume; other people's or agents' messages may wake you but leave it open. Targets a human only; to hand work to another AGENT use summon/dispatch instead.`,
|
|
6312
6553
|
{
|
|
6313
6554
|
targetUserId: z13.string().uuid().describe(
|
|
6314
6555
|
"The workspace member (human) to ask \u2014 a user id, from your turn context's roster."
|
|
@@ -6328,8 +6569,8 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
6328
6569
|
body: z13.string().min(1).max(400).optional().describe("Optional short framing beneath the headline. NOT a report."),
|
|
6329
6570
|
options: z13.array(z13.string().min(1).max(200)).min(2).max(4).optional().describe("Optional 2\u20134 one-click answers for this question.")
|
|
6330
6571
|
})
|
|
6331
|
-
).min(1).max(
|
|
6332
|
-
|
|
6572
|
+
).min(1).max(MAX_ASK_ITEMS).optional().describe(
|
|
6573
|
+
`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.`
|
|
6333
6574
|
)
|
|
6334
6575
|
},
|
|
6335
6576
|
async (args) => {
|
|
@@ -7509,7 +7750,8 @@ ${reason}`,
|
|
|
7509
7750
|
}
|
|
7510
7751
|
const turnEnv = {
|
|
7511
7752
|
...hookEnv,
|
|
7512
|
-
CABANE_HOST_ACCESS: turnContext.policy.hostFs ? "1" : "0"
|
|
7753
|
+
CABANE_HOST_ACCESS: turnContext.policy.hostFs ? "1" : "0",
|
|
7754
|
+
CABANE_COMPANION_HOME: agentCompanionHome()
|
|
7513
7755
|
};
|
|
7514
7756
|
const key = runKey(payload.conversationId, payload.agentId);
|
|
7515
7757
|
const abortController = new AbortController();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.54",
|
|
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",
|