@skydiveai/pi-extensions 0.1.0-beta.1728 → 0.1.0-beta.1729
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/index.mjs +329 -268
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -418,6 +418,275 @@ function installIterationCap({ session, log }, configOverride = null) {
|
|
|
418
418
|
*/
|
|
419
419
|
const CAPABILITY_SOUL_NUDGE = "New capability gained — once the current task is done, if this changes what you can do for the user, record it in `soul.md` so it carries into future conversations rather than being rediscovered from scratch (then commit and push).";
|
|
420
420
|
//#endregion
|
|
421
|
+
//#region src/extensions/tool-call-summary.ts
|
|
422
|
+
const log$14 = logger.child({ module: "tool-call-summary-extension" });
|
|
423
|
+
/**
|
|
424
|
+
* The injected parameter name: a namespaced sentinel, so it can never collide
|
|
425
|
+
* with a real tool argument and is unmistakable in transcripts and logs. The
|
|
426
|
+
* frontend renderer (ANY-2723) duplicates this literal — keep the two in sync.
|
|
427
|
+
*/
|
|
428
|
+
const TOOL_CALL_SUMMARY_FIELD = "__skydive_summary__";
|
|
429
|
+
/** JSON Schema fragment for the injected parameter. */
|
|
430
|
+
const SUMMARY_PROPERTY = {
|
|
431
|
+
type: "string",
|
|
432
|
+
description: "Required for every tool call. A concise, specific summary (max ~8 words) of what THIS call does and why, written for a person watching the conversation, e.g. \"Searching feedback for billing complaints\" or \"Reading the auth middleware\". Address the user directly in second person: the summary is read by the user, so refer to their things as \"your\", never in third person — \"Reading your emails\", not \"Reading his emails\". Always use the present progressive tense, since it is shown while the call runs: \"Updating your Slack\", never \"Updated your Slack\". Make each summary distinct from your other tool calls; never reuse a generic label like \"Search query\" or \"Running command\"."
|
|
433
|
+
};
|
|
434
|
+
const jsonSchemaObjectSchema = z.object({
|
|
435
|
+
type: z.unknown().optional(),
|
|
436
|
+
properties: z.record(z.string(), z.unknown()).optional(),
|
|
437
|
+
required: z.array(z.string()).optional(),
|
|
438
|
+
additionalProperties: z.unknown().optional()
|
|
439
|
+
}).passthrough();
|
|
440
|
+
const toolEntrySchema = z.object({
|
|
441
|
+
name: z.string().optional(),
|
|
442
|
+
input_schema: jsonSchemaObjectSchema.optional(),
|
|
443
|
+
parameters: jsonSchemaObjectSchema.optional(),
|
|
444
|
+
function: z.object({
|
|
445
|
+
name: z.string().optional(),
|
|
446
|
+
parameters: jsonSchemaObjectSchema.optional()
|
|
447
|
+
}).passthrough().optional()
|
|
448
|
+
}).passthrough();
|
|
449
|
+
const payloadWithToolsSchema = z.object({ tools: z.array(z.unknown()) }).passthrough();
|
|
450
|
+
/**
|
|
451
|
+
* Add the summary property to one JSON Schema object. Returns the augmented
|
|
452
|
+
* copy, or `null` when the tool should be left untouched: a strict schema
|
|
453
|
+
* (`additionalProperties: false`) whose validation would reject the extra
|
|
454
|
+
* field, or one that already declares a `__skydive_summary__` property of its own.
|
|
455
|
+
*/
|
|
456
|
+
function augmentSchema(schema) {
|
|
457
|
+
if (schema.additionalProperties === false) return null;
|
|
458
|
+
const properties = schema.properties ?? {};
|
|
459
|
+
if ("__skydive_summary__" in properties) return null;
|
|
460
|
+
const required = schema.required ?? [];
|
|
461
|
+
return {
|
|
462
|
+
...schema,
|
|
463
|
+
type: schema.type ?? "object",
|
|
464
|
+
properties: {
|
|
465
|
+
[TOOL_CALL_SUMMARY_FIELD]: SUMMARY_PROPERTY,
|
|
466
|
+
...properties
|
|
467
|
+
},
|
|
468
|
+
required: required.includes("__skydive_summary__") ? required : [...required, TOOL_CALL_SUMMARY_FIELD]
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Augment a single tool entry, dispatching on which provider shape it is.
|
|
473
|
+
* Returns the (possibly rebuilt) entry and whether anything changed. Skipped
|
|
474
|
+
* tools — wrong shape, strict, or name in `strictToolNames` — return unchanged.
|
|
475
|
+
*/
|
|
476
|
+
function augmentToolEntry(entry, strictToolNames) {
|
|
477
|
+
const parsed = toolEntrySchema.safeParse(entry);
|
|
478
|
+
if (!parsed.success) return {
|
|
479
|
+
entry,
|
|
480
|
+
changed: false
|
|
481
|
+
};
|
|
482
|
+
const tool = parsed.data;
|
|
483
|
+
const name = tool.name ?? tool.function?.name ?? null;
|
|
484
|
+
if (name !== null && strictToolNames.has(name)) return {
|
|
485
|
+
entry,
|
|
486
|
+
changed: false
|
|
487
|
+
};
|
|
488
|
+
if (tool.input_schema) {
|
|
489
|
+
const augmented = augmentSchema(tool.input_schema);
|
|
490
|
+
if (!augmented) return {
|
|
491
|
+
entry,
|
|
492
|
+
changed: false
|
|
493
|
+
};
|
|
494
|
+
return {
|
|
495
|
+
entry: {
|
|
496
|
+
...tool,
|
|
497
|
+
input_schema: augmented
|
|
498
|
+
},
|
|
499
|
+
changed: true
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
if (tool.parameters) {
|
|
503
|
+
const augmented = augmentSchema(tool.parameters);
|
|
504
|
+
if (!augmented) return {
|
|
505
|
+
entry,
|
|
506
|
+
changed: false
|
|
507
|
+
};
|
|
508
|
+
return {
|
|
509
|
+
entry: {
|
|
510
|
+
...tool,
|
|
511
|
+
parameters: augmented
|
|
512
|
+
},
|
|
513
|
+
changed: true
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
if (tool.function?.parameters) {
|
|
517
|
+
const augmented = augmentSchema(tool.function.parameters);
|
|
518
|
+
if (!augmented) return {
|
|
519
|
+
entry,
|
|
520
|
+
changed: false
|
|
521
|
+
};
|
|
522
|
+
return {
|
|
523
|
+
entry: {
|
|
524
|
+
...tool,
|
|
525
|
+
function: {
|
|
526
|
+
...tool.function,
|
|
527
|
+
parameters: augmented
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
changed: true
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
return {
|
|
534
|
+
entry,
|
|
535
|
+
changed: false
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Inject the summary field into every eligible tool in a provider payload.
|
|
540
|
+
* Returns a new payload when at least one tool was augmented, or `undefined`
|
|
541
|
+
* to signal "no change" (which keeps the original payload, per the
|
|
542
|
+
* `before_provider_request` contract).
|
|
543
|
+
*
|
|
544
|
+
* @param payload The outgoing provider payload (shape varies by provider).
|
|
545
|
+
* @param strictToolNames Names of tools whose registered schema is strict and
|
|
546
|
+
* must be skipped to avoid validation errors.
|
|
547
|
+
*/
|
|
548
|
+
function injectToolCallSummary(payload, strictToolNames) {
|
|
549
|
+
const parsed = payloadWithToolsSchema.safeParse(payload);
|
|
550
|
+
if (!parsed.success || parsed.data.tools.length === 0) return void 0;
|
|
551
|
+
let changed = false;
|
|
552
|
+
const tools = parsed.data.tools.map((entry) => {
|
|
553
|
+
const result = augmentToolEntry(entry, strictToolNames);
|
|
554
|
+
if (result.changed) changed = true;
|
|
555
|
+
return result.entry;
|
|
556
|
+
});
|
|
557
|
+
if (!changed) return void 0;
|
|
558
|
+
return {
|
|
559
|
+
...parsed.data,
|
|
560
|
+
tools
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Names of registered tools whose schema sets `additionalProperties: false`.
|
|
565
|
+
* Pi validates the model's tool args against this registered schema, so the
|
|
566
|
+
* injected field would make a strict tool's call fail validation — skip them.
|
|
567
|
+
*/
|
|
568
|
+
function getStrictToolNames(pi) {
|
|
569
|
+
const names = /* @__PURE__ */ new Set();
|
|
570
|
+
for (const tool of pi.getAllTools()) {
|
|
571
|
+
const parsed = jsonSchemaObjectSchema.safeParse(tool.parameters);
|
|
572
|
+
if (parsed.success && parsed.data.additionalProperties === false) names.add(tool.name);
|
|
573
|
+
}
|
|
574
|
+
return names;
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Whether a tool's own schema declares a `__skydive_summary__` property. We
|
|
578
|
+
* never inject into such a tool, so any value it carries is a real argument
|
|
579
|
+
* and must be left alone.
|
|
580
|
+
*/
|
|
581
|
+
function schemaDeclaresSummary(parameters) {
|
|
582
|
+
const parsed = jsonSchemaObjectSchema.safeParse(parameters);
|
|
583
|
+
return parsed.success && parsed.data.properties != null && "__skydive_summary__" in parsed.data.properties;
|
|
584
|
+
}
|
|
585
|
+
function toolDeclaresSummaryParam(pi, toolName) {
|
|
586
|
+
const tool = pi.getAllTools().find((candidate) => candidate.name === toolName);
|
|
587
|
+
if (!tool) return false;
|
|
588
|
+
return schemaDeclaresSummary(tool.parameters);
|
|
589
|
+
}
|
|
590
|
+
/** A copy of `args` without the injected summary. Never mutates its input. */
|
|
591
|
+
function withoutInjectedSummary(args) {
|
|
592
|
+
if (args == null || typeof args !== "object" || Array.isArray(args)) return args;
|
|
593
|
+
if (!("__skydive_summary__" in args)) return args;
|
|
594
|
+
const { [TOOL_CALL_SUMMARY_FIELD]: _summary, ...rest } = args;
|
|
595
|
+
return rest;
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Register a tool so the injected summary is removed *before* pi validates
|
|
599
|
+
* the model's arguments against the tool's schema.
|
|
600
|
+
*
|
|
601
|
+
* Needed because the `tool_call` strip below runs too late. pi-agent-core's
|
|
602
|
+
* `prepareToolCall` goes: `tool.prepareArguments` → `validateToolArguments` →
|
|
603
|
+
* `beforeToolCall` (which is what dispatches `tool_call`). A tool whose schema
|
|
604
|
+
* sets `additionalProperties: false` therefore rejects the summary and errors
|
|
605
|
+
* the call — client-side, before any request reaches the server — while the
|
|
606
|
+
* strip meant to prevent exactly that sits one step further down.
|
|
607
|
+
*
|
|
608
|
+
* `augmentSchema` already skips strict tools, but that only controls what the
|
|
609
|
+
* model is *told*. It still emits the field on those tools, because every
|
|
610
|
+
* other tool in the list declares it as required for every call. So a server
|
|
611
|
+
* can connect, bind its tools, present a healthy inventory, and have every
|
|
612
|
+
* call fail — and it reads as the server's fault when it is ours.
|
|
613
|
+
*
|
|
614
|
+
* Apply to tools registered from schemas we do not author: MCP servers and
|
|
615
|
+
* local `tools/*.ts`. Tools built here with `Type.Object(...)` do not need it
|
|
616
|
+
* (TypeBox emits no `additionalProperties`, so the field validates fine and
|
|
617
|
+
* the `tool_call` strip removes it in time).
|
|
618
|
+
*
|
|
619
|
+
* Fails open, like the rest of this module: if the strip throws, the original
|
|
620
|
+
* arguments are used rather than failing the call.
|
|
621
|
+
*/
|
|
622
|
+
function withSummaryStrippedBeforeValidation(tool) {
|
|
623
|
+
if (schemaDeclaresSummary(tool.parameters)) return tool;
|
|
624
|
+
const toolPrepare = tool.prepareArguments;
|
|
625
|
+
const prepareArguments = ((args) => {
|
|
626
|
+
let stripped = args;
|
|
627
|
+
try {
|
|
628
|
+
stripped = withoutInjectedSummary(args);
|
|
629
|
+
} catch (err) {
|
|
630
|
+
log$14.error({
|
|
631
|
+
err,
|
|
632
|
+
event: "tool_call_summary_prepare_strip_failed",
|
|
633
|
+
toolName: tool.name
|
|
634
|
+
}, "tool_call_summary pre-validation strip failed; leaving arguments untouched");
|
|
635
|
+
}
|
|
636
|
+
return toolPrepare ? toolPrepare(stripped) : stripped;
|
|
637
|
+
});
|
|
638
|
+
return {
|
|
639
|
+
...tool,
|
|
640
|
+
prepareArguments
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Remove the injected summary from a tool's execution input. No-op when the
|
|
645
|
+
* field is absent, or when the tool genuinely declares a `__skydive_summary__`
|
|
646
|
+
* parameter of its own (which we never inject into, so its value is real).
|
|
647
|
+
* Mutates `input` in place, matching the `tool_call` contract.
|
|
648
|
+
*
|
|
649
|
+
* Fails open: this runs on the critical path of tool execution, and the
|
|
650
|
+
* `getAllTools()` lookup can throw. On any error we leave `input` untouched
|
|
651
|
+
* (the sentinel may pass through to the tool, but a bug here can never break
|
|
652
|
+
* tool execution).
|
|
653
|
+
*/
|
|
654
|
+
function stripInjectedSummary(pi, toolName, input) {
|
|
655
|
+
try {
|
|
656
|
+
if (!("__skydive_summary__" in input)) return;
|
|
657
|
+
if (toolDeclaresSummaryParam(pi, toolName)) return;
|
|
658
|
+
delete input[TOOL_CALL_SUMMARY_FIELD];
|
|
659
|
+
} catch (err) {
|
|
660
|
+
log$14.error({
|
|
661
|
+
err,
|
|
662
|
+
event: "tool_call_summary_strip_failed",
|
|
663
|
+
toolName
|
|
664
|
+
}, "tool_call_summary strip failed; leaving tool input untouched");
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Compute the rewritten payload for a `before_provider_request` event, failing
|
|
669
|
+
* open: on any error the original payload is left untouched so a bug here can
|
|
670
|
+
* never break an LLM call.
|
|
671
|
+
*/
|
|
672
|
+
function buildInjectedPayload(pi, payload) {
|
|
673
|
+
try {
|
|
674
|
+
return injectToolCallSummary(payload, getStrictToolNames(pi));
|
|
675
|
+
} catch (err) {
|
|
676
|
+
log$14.error({
|
|
677
|
+
err,
|
|
678
|
+
event: "tool_call_summary_injection_failed"
|
|
679
|
+
}, "tool_call_summary injection failed; passing payload through unchanged");
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
const toolCallSummaryExtension = (pi) => {
|
|
684
|
+
pi.on("before_provider_request", (event) => buildInjectedPayload(pi, event.payload));
|
|
685
|
+
pi.on("tool_call", (event) => {
|
|
686
|
+
stripInjectedSummary(pi, event.toolName, event.input);
|
|
687
|
+
});
|
|
688
|
+
};
|
|
689
|
+
//#endregion
|
|
421
690
|
//#region src/extensions/local-tools.ts
|
|
422
691
|
/**
|
|
423
692
|
* Local-tools adapter as a pi extension. Mirrors the mcp.ts hot-reload pattern
|
|
@@ -441,7 +710,7 @@ const CAPABILITY_SOUL_NUDGE = "New capability gained — once the current task i
|
|
|
441
710
|
* or `ToolDefinition[]`. Files starting with `_` or `.` are skipped, so
|
|
442
711
|
* `tools/_example.ts` documents the shape without registering.
|
|
443
712
|
*/
|
|
444
|
-
const log$
|
|
713
|
+
const log$13 = logger.child({ module: "local-tools-extension" });
|
|
445
714
|
const TOOLS_DIRNAME = "tools";
|
|
446
715
|
const fileState = /* @__PURE__ */ new Map();
|
|
447
716
|
let pendingLocalToolsUpdate = null;
|
|
@@ -568,7 +837,7 @@ async function reconcileLocalTools({ pi, dir }) {
|
|
|
568
837
|
action = existing ? "refreshed" : "added";
|
|
569
838
|
}
|
|
570
839
|
for (const tool of tools) {
|
|
571
|
-
pi.registerTool(withDefaultPromptSnippet(tool));
|
|
840
|
+
pi.registerTool(withSummaryStrippedBeforeValidation(withDefaultPromptSnippet(tool)));
|
|
572
841
|
summary.totalTools++;
|
|
573
842
|
}
|
|
574
843
|
if (action === "added") summary.added.push(file);
|
|
@@ -585,7 +854,7 @@ async function reconcileAndQueue({ pi, dir, reason }) {
|
|
|
585
854
|
dir
|
|
586
855
|
});
|
|
587
856
|
if (reason !== "session_start" && summaryHasChanges$1(summary)) pendingLocalToolsUpdate = summary;
|
|
588
|
-
log$
|
|
857
|
+
log$13.info({
|
|
589
858
|
event: "local_tools_reconcile",
|
|
590
859
|
reason,
|
|
591
860
|
total_tools: summary.totalTools,
|
|
@@ -607,7 +876,7 @@ const localToolsExtension = (pi) => {
|
|
|
607
876
|
reason: "session_start"
|
|
608
877
|
});
|
|
609
878
|
} catch (err) {
|
|
610
|
-
log$
|
|
879
|
+
log$13.error({
|
|
611
880
|
err,
|
|
612
881
|
event: "local_tools_reconcile_failed"
|
|
613
882
|
}, "local tools reconcile failed");
|
|
@@ -619,7 +888,7 @@ const localToolsExtension = (pi) => {
|
|
|
619
888
|
try {
|
|
620
889
|
current = await listToolFiles(dir);
|
|
621
890
|
} catch (err) {
|
|
622
|
-
log$
|
|
891
|
+
log$13.warn({
|
|
623
892
|
err,
|
|
624
893
|
event: "local_tools_listing_failed"
|
|
625
894
|
}, "tools/ listing failed");
|
|
@@ -641,7 +910,7 @@ const localToolsExtension = (pi) => {
|
|
|
641
910
|
reason: "auto_reload"
|
|
642
911
|
});
|
|
643
912
|
} catch (err) {
|
|
644
|
-
log$
|
|
913
|
+
log$13.error({
|
|
645
914
|
err,
|
|
646
915
|
event: "local_tools_auto_reload_failed"
|
|
647
916
|
}, "auto-reload after tools/ change failed");
|
|
@@ -879,7 +1148,7 @@ async function loadMcpConfig(path) {
|
|
|
879
1148
|
* Clients are keyed by JSON-stringified config and reused across
|
|
880
1149
|
* reloads — only changed configs reconnect.
|
|
881
1150
|
*/
|
|
882
|
-
const log$
|
|
1151
|
+
const log$12 = logger.child({ module: "mcp-extension" });
|
|
883
1152
|
async function closeConnected(connected) {
|
|
884
1153
|
try {
|
|
885
1154
|
await connected.client.close();
|
|
@@ -969,7 +1238,7 @@ var McpExtension = class {
|
|
|
969
1238
|
const parameters = Type.Unsafe(tool.inputSchema);
|
|
970
1239
|
const description = tool.description?.trim() ?? "";
|
|
971
1240
|
const promptSnippet = description.length > 0 ? description : `MCP tool from server "${serverId}".`;
|
|
972
|
-
pi.registerTool({
|
|
1241
|
+
pi.registerTool(withSummaryStrippedBeforeValidation({
|
|
973
1242
|
name,
|
|
974
1243
|
label: `MCP: ${serverId}/${tool.name}`,
|
|
975
1244
|
description,
|
|
@@ -1003,7 +1272,7 @@ var McpExtension = class {
|
|
|
1003
1272
|
};
|
|
1004
1273
|
}
|
|
1005
1274
|
}
|
|
1006
|
-
});
|
|
1275
|
+
}));
|
|
1007
1276
|
this.registeredMcpToolNames.add(name);
|
|
1008
1277
|
}
|
|
1009
1278
|
async reconcile({ pi, configPath, connectTimeoutMs }) {
|
|
@@ -1301,7 +1570,7 @@ var McpExtension = class {
|
|
|
1301
1570
|
});
|
|
1302
1571
|
this.lastConfigMtimeMs = await readConfigMtimeMs(configPath);
|
|
1303
1572
|
if (reason !== "session_start" && summaryHasChanges(summary)) this.pendingMcpUpdate = summary;
|
|
1304
|
-
log$
|
|
1573
|
+
log$12.info({
|
|
1305
1574
|
event: "mcp_reconcile",
|
|
1306
1575
|
reason,
|
|
1307
1576
|
total_tools: summary.totalTools,
|
|
@@ -1324,7 +1593,7 @@ var McpExtension = class {
|
|
|
1324
1593
|
reason: "session_start"
|
|
1325
1594
|
});
|
|
1326
1595
|
} catch (err) {
|
|
1327
|
-
log$
|
|
1596
|
+
log$12.error({
|
|
1328
1597
|
err,
|
|
1329
1598
|
event: "mcp_reconcile_failed"
|
|
1330
1599
|
}, "MCP reconcile failed");
|
|
@@ -1336,7 +1605,7 @@ var McpExtension = class {
|
|
|
1336
1605
|
try {
|
|
1337
1606
|
mtime = await readConfigMtimeMs(configPath);
|
|
1338
1607
|
} catch (err) {
|
|
1339
|
-
log$
|
|
1608
|
+
log$12.warn({
|
|
1340
1609
|
err,
|
|
1341
1610
|
event: "mcp_mtime_check_failed"
|
|
1342
1611
|
}, "mtime check on mcp.config.json failed");
|
|
@@ -1350,7 +1619,7 @@ var McpExtension = class {
|
|
|
1350
1619
|
reason: "auto_reload"
|
|
1351
1620
|
});
|
|
1352
1621
|
} catch (err) {
|
|
1353
|
-
log$
|
|
1622
|
+
log$12.error({
|
|
1354
1623
|
err,
|
|
1355
1624
|
event: "mcp_auto_reload_failed"
|
|
1356
1625
|
}, "auto-reload after mcp.config.json change failed");
|
|
@@ -1746,7 +2015,7 @@ const bashDefaultTimeoutExtension = (pi) => {
|
|
|
1746
2015
|
* turn over an observability feature.
|
|
1747
2016
|
*/
|
|
1748
2017
|
const execFileAsync = promisify(execFile);
|
|
1749
|
-
const log$
|
|
2018
|
+
const log$11 = logger.child({ module: "resource-pressure-warning" });
|
|
1750
2019
|
const POLL_INTERVAL_MS = 1e4;
|
|
1751
2020
|
function envOverride(name) {
|
|
1752
2021
|
for (const prefix of ["SKYDIVE_", "ANYONE_"]) {
|
|
@@ -1884,7 +2153,7 @@ const resourcePressureWarningExtension = (pi) => {
|
|
|
1884
2153
|
warnedDiskThisTurn = true;
|
|
1885
2154
|
}
|
|
1886
2155
|
if (trigger === null) return;
|
|
1887
|
-
log$
|
|
2156
|
+
log$11.warn({
|
|
1888
2157
|
trigger,
|
|
1889
2158
|
mem,
|
|
1890
2159
|
cpuPct,
|
|
@@ -1910,7 +2179,7 @@ const resourcePressureWarningExtension = (pi) => {
|
|
|
1910
2179
|
if (!timer) {
|
|
1911
2180
|
timer = setInterval(() => {
|
|
1912
2181
|
checkOnce().catch((err) => {
|
|
1913
|
-
log$
|
|
2182
|
+
log$11.error({ err }, "resource pressure check failed");
|
|
1914
2183
|
});
|
|
1915
2184
|
}, POLL_INTERVAL_MS);
|
|
1916
2185
|
timer.unref?.();
|
|
@@ -1926,7 +2195,7 @@ const resourcePressureWarningExtension = (pi) => {
|
|
|
1926
2195
|
};
|
|
1927
2196
|
//#endregion
|
|
1928
2197
|
//#region src/extensions/disk-guard.ts
|
|
1929
|
-
const log$
|
|
2198
|
+
const log$10 = logger.child({ module: "disk-guard" });
|
|
1930
2199
|
/**
|
|
1931
2200
|
* In-band bypass. The guard is a safety net, not a jail: when the agent knows
|
|
1932
2201
|
* a flagged command is genuinely safe (writing to a different mount, a tiny
|
|
@@ -2066,7 +2335,7 @@ const diskGuardExtension = (pi) => {
|
|
|
2066
2335
|
if (!isSpaceHungryCommand(command)) return;
|
|
2067
2336
|
const diskPct = await readDiskUsePct();
|
|
2068
2337
|
if (!shouldBlockForDisk(command, diskPct)) return;
|
|
2069
|
-
log$
|
|
2338
|
+
log$10.warn({
|
|
2070
2339
|
diskPct,
|
|
2071
2340
|
command: command.slice(0, 200)
|
|
2072
2341
|
}, "blocked space-hungry bash command on near-full disk");
|
|
@@ -2139,7 +2408,7 @@ const HEARTBEAT_THROTTLE_MS = 6e4;
|
|
|
2139
2408
|
const TOOL_HEARTBEAT_INTERVAL_MS = 5e3;
|
|
2140
2409
|
const MAX_TOOL_HEARTBEATS = 1440 * 60 * 1e3 / TOOL_HEARTBEAT_INTERVAL_MS;
|
|
2141
2410
|
const DAEMON_URL = "http://localhost:38994";
|
|
2142
|
-
const log$
|
|
2411
|
+
const log$9 = logger.child({ module: "platform-ext" });
|
|
2143
2412
|
function sandboxClient() {
|
|
2144
2413
|
const apiUrl = apiBaseUrl();
|
|
2145
2414
|
if (!apiUrl) return null;
|
|
@@ -2207,7 +2476,7 @@ async function fetchHarnessFlags() {
|
|
|
2207
2476
|
try {
|
|
2208
2477
|
const res = await client["feature-flags"].$get();
|
|
2209
2478
|
if (!res.ok) {
|
|
2210
|
-
log$
|
|
2479
|
+
log$9.debug({
|
|
2211
2480
|
status: res.status,
|
|
2212
2481
|
event: "feature_flags_fetch_failed"
|
|
2213
2482
|
}, "feature-flags fetch failed");
|
|
@@ -2215,7 +2484,7 @@ async function fetchHarnessFlags() {
|
|
|
2215
2484
|
}
|
|
2216
2485
|
return { contextManagement: (await res.json()).contextManagement ?? null };
|
|
2217
2486
|
} catch (err) {
|
|
2218
|
-
log$
|
|
2487
|
+
log$9.debug({
|
|
2219
2488
|
err,
|
|
2220
2489
|
event: "feature_flags_fetch_error"
|
|
2221
2490
|
}, "feature-flags request errored");
|
|
@@ -2226,7 +2495,7 @@ function postHeartbeat({ messageId }) {
|
|
|
2226
2495
|
const client = sandboxClient();
|
|
2227
2496
|
if (!client) return;
|
|
2228
2497
|
client.heartbeat.$post({ json: { messageId } }).catch((err) => {
|
|
2229
|
-
log$
|
|
2498
|
+
log$9.debug({
|
|
2230
2499
|
err,
|
|
2231
2500
|
event: "heartbeat_failed"
|
|
2232
2501
|
}, "heartbeat failed");
|
|
@@ -2238,7 +2507,7 @@ async function resolveConversationFromApi(messageId) {
|
|
|
2238
2507
|
try {
|
|
2239
2508
|
const res = await client["message-conversation"].$get({ query: { messageId } });
|
|
2240
2509
|
if (!res.ok) {
|
|
2241
|
-
log$
|
|
2510
|
+
log$9.warn({
|
|
2242
2511
|
status: res.status,
|
|
2243
2512
|
messageId,
|
|
2244
2513
|
event: "resolve_conversation_failed"
|
|
@@ -2247,7 +2516,7 @@ async function resolveConversationFromApi(messageId) {
|
|
|
2247
2516
|
}
|
|
2248
2517
|
return (await res.json()).conversationId ?? null;
|
|
2249
2518
|
} catch (err) {
|
|
2250
|
-
log$
|
|
2519
|
+
log$9.warn({
|
|
2251
2520
|
err,
|
|
2252
2521
|
messageId,
|
|
2253
2522
|
event: "resolve_conversation_error"
|
|
@@ -2272,13 +2541,13 @@ async function putBackgroundTaskJournalSpec({ messageId, spec }) {
|
|
|
2272
2541
|
messageId,
|
|
2273
2542
|
spec
|
|
2274
2543
|
} });
|
|
2275
|
-
if (!res.ok) log$
|
|
2544
|
+
if (!res.ok) log$9.warn({
|
|
2276
2545
|
status: res.status,
|
|
2277
2546
|
taskId: spec.id,
|
|
2278
2547
|
event: "bg_journal_put_failed"
|
|
2279
2548
|
}, "bg-task journal PUT failed");
|
|
2280
2549
|
} catch (err) {
|
|
2281
|
-
log$
|
|
2550
|
+
log$9.warn({
|
|
2282
2551
|
err,
|
|
2283
2552
|
taskId: spec.id,
|
|
2284
2553
|
event: "bg_journal_put_failed"
|
|
@@ -2294,7 +2563,7 @@ async function deleteBackgroundTaskJournalSpec({ messageId, taskId }) {
|
|
|
2294
2563
|
taskId
|
|
2295
2564
|
} });
|
|
2296
2565
|
} catch (err) {
|
|
2297
|
-
log$
|
|
2566
|
+
log$9.debug({
|
|
2298
2567
|
err,
|
|
2299
2568
|
taskId,
|
|
2300
2569
|
event: "bg_journal_delete_failed"
|
|
@@ -2309,7 +2578,7 @@ async function listBackgroundTaskJournalSpecs({ messageId }) {
|
|
|
2309
2578
|
if (!res.ok) return [];
|
|
2310
2579
|
return (await res.json()).specs ?? [];
|
|
2311
2580
|
} catch (err) {
|
|
2312
|
-
log$
|
|
2581
|
+
log$9.debug({
|
|
2313
2582
|
err,
|
|
2314
2583
|
event: "bg_journal_list_failed"
|
|
2315
2584
|
}, "bg-task journal GET failed");
|
|
@@ -2323,7 +2592,7 @@ function postBackgroundTasksSnapshot({ messageId, tasks }) {
|
|
|
2323
2592
|
messageId,
|
|
2324
2593
|
tasks
|
|
2325
2594
|
} }).catch((err) => {
|
|
2326
|
-
log$
|
|
2595
|
+
log$9.debug({
|
|
2327
2596
|
err,
|
|
2328
2597
|
event: "bg_tasks_snapshot_failed"
|
|
2329
2598
|
}, "bg-tasks snapshot publish failed");
|
|
@@ -2397,7 +2666,7 @@ function createToolHeartbeat({ messageId }) {
|
|
|
2397
2666
|
}
|
|
2398
2667
|
heartbeatCount++;
|
|
2399
2668
|
if (heartbeatCount > MAX_TOOL_HEARTBEATS) {
|
|
2400
|
-
log$
|
|
2669
|
+
log$9.warn({
|
|
2401
2670
|
heartbeatCount,
|
|
2402
2671
|
activeToolCalls: [...activeToolCalls]
|
|
2403
2672
|
}, "tool heartbeat max reached, stopping");
|
|
@@ -2428,7 +2697,7 @@ function postToDaemon(path, body) {
|
|
|
2428
2697
|
headers: { "content-type": "application/json" },
|
|
2429
2698
|
body: JSON.stringify(body)
|
|
2430
2699
|
}).catch((err) => {
|
|
2431
|
-
log$
|
|
2700
|
+
log$9.debug({
|
|
2432
2701
|
err,
|
|
2433
2702
|
path,
|
|
2434
2703
|
event: "daemon_post_failed"
|
|
@@ -2437,7 +2706,7 @@ function postToDaemon(path, body) {
|
|
|
2437
2706
|
}
|
|
2438
2707
|
function createPlatformExtensions({ sessionId, channelContext }) {
|
|
2439
2708
|
return (pi) => {
|
|
2440
|
-
log$
|
|
2709
|
+
log$9.info({
|
|
2441
2710
|
sessionId,
|
|
2442
2711
|
hasChannelContext: Boolean(channelContext)
|
|
2443
2712
|
}, "platform extension initialized");
|
|
@@ -2486,7 +2755,7 @@ function createPlatformExtensions({ sessionId, channelContext }) {
|
|
|
2486
2755
|
});
|
|
2487
2756
|
});
|
|
2488
2757
|
pi.on("agent_end", () => {
|
|
2489
|
-
log$
|
|
2758
|
+
log$9.info({ sessionId }, "session ending");
|
|
2490
2759
|
postToDaemon("/session/end", { sessionId });
|
|
2491
2760
|
});
|
|
2492
2761
|
};
|
|
@@ -2512,7 +2781,7 @@ function createPlatformExtensions({ sessionId, channelContext }) {
|
|
|
2512
2781
|
* alive and an indeterminate result (no api url / transient failure) leaves the
|
|
2513
2782
|
* last-known values untouched so a blip can't silently flip behavior.
|
|
2514
2783
|
*/
|
|
2515
|
-
const log$
|
|
2784
|
+
const log$8 = logger.child({ module: "feature-flags-poll" });
|
|
2516
2785
|
const FLAG_POLL_INTERVAL_MS = 6e4;
|
|
2517
2786
|
let contextManagement = null;
|
|
2518
2787
|
const subscribers = { contextManagement: /* @__PURE__ */ new Set() };
|
|
@@ -2547,7 +2816,7 @@ function apply(name, next) {
|
|
|
2547
2816
|
if (next !== prev) for (const cb of subscribers[name]) try {
|
|
2548
2817
|
cb(next);
|
|
2549
2818
|
} catch (err) {
|
|
2550
|
-
log$
|
|
2819
|
+
log$8.warn({
|
|
2551
2820
|
err,
|
|
2552
2821
|
flag: name
|
|
2553
2822
|
}, "flag subscriber threw");
|
|
@@ -2559,7 +2828,7 @@ async function pollOnce() {
|
|
|
2559
2828
|
if (!flags) return;
|
|
2560
2829
|
apply("contextManagement", flags.contextManagement ?? null);
|
|
2561
2830
|
} catch (err) {
|
|
2562
|
-
log$
|
|
2831
|
+
log$8.debug({ err }, "feature-flag poll threw");
|
|
2563
2832
|
}
|
|
2564
2833
|
}
|
|
2565
2834
|
/**
|
|
@@ -2666,7 +2935,7 @@ function transformContextMessages(messages, config, now) {
|
|
|
2666
2935
|
}
|
|
2667
2936
|
//#endregion
|
|
2668
2937
|
//#region src/extensions/context-management.ts
|
|
2669
|
-
const log$
|
|
2938
|
+
const log$7 = logger.child({ module: "context-management-extension" });
|
|
2670
2939
|
function isAnthropicMessagesPayload(payload) {
|
|
2671
2940
|
if (typeof payload !== "object" || payload === null) return false;
|
|
2672
2941
|
const candidate = payload;
|
|
@@ -2731,13 +3000,13 @@ function createContextManagementExtension() {
|
|
|
2731
3000
|
setContextManagementFlagOverride(getPolledFlag("contextManagement"));
|
|
2732
3001
|
onFlagChange("contextManagement", (enabled) => {
|
|
2733
3002
|
setContextManagementFlagOverride(enabled);
|
|
2734
|
-
log$
|
|
3003
|
+
log$7.info({
|
|
2735
3004
|
event: "context_management_flag_update",
|
|
2736
3005
|
enabled
|
|
2737
3006
|
}, "context-management flag updated from platform");
|
|
2738
3007
|
});
|
|
2739
3008
|
startFeatureFlagPoller();
|
|
2740
|
-
log$
|
|
3009
|
+
log$7.info({
|
|
2741
3010
|
event: "context_management_registered",
|
|
2742
3011
|
enabled: initial.enabled,
|
|
2743
3012
|
flagSource: hasFlagSource(),
|
|
@@ -2749,13 +3018,13 @@ function createContextManagementExtension() {
|
|
|
2749
3018
|
const { messages } = event;
|
|
2750
3019
|
try {
|
|
2751
3020
|
const result = transformContextIfEnabled(messages, getContextManagementConfig(), Date.now());
|
|
2752
|
-
if (result.stats && (result.stats.clearedResults > 0 || result.stats.trimmedResults > 0)) log$
|
|
3021
|
+
if (result.stats && (result.stats.clearedResults > 0 || result.stats.trimmedResults > 0)) log$7.info({
|
|
2753
3022
|
event: "context_management_applied",
|
|
2754
3023
|
...result.stats
|
|
2755
3024
|
}, "trimmed/cleared tool output before LLM call");
|
|
2756
3025
|
return { messages: result.messages };
|
|
2757
3026
|
} catch (err) {
|
|
2758
|
-
log$
|
|
3027
|
+
log$7.error({
|
|
2759
3028
|
err,
|
|
2760
3029
|
event: "context_management_transform_failed"
|
|
2761
3030
|
}, "context transform failed; passing messages through unchanged");
|
|
@@ -2767,7 +3036,7 @@ function createContextManagementExtension() {
|
|
|
2767
3036
|
}
|
|
2768
3037
|
//#endregion
|
|
2769
3038
|
//#region src/extensions/current-time.ts
|
|
2770
|
-
const log$
|
|
3039
|
+
const log$6 = logger.child({ module: "current-time-extension" });
|
|
2771
3040
|
const PI_DATE_LINE = /^Current date:.*$/m;
|
|
2772
3041
|
function formatCurrentTimeLine(now) {
|
|
2773
3042
|
return `Current date: ${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, "0")}-${String(now.getUTCDate()).padStart(2, "0")} (${new Intl.DateTimeFormat("en-US", {
|
|
@@ -2780,7 +3049,7 @@ const currentTimeExtension = (pi) => {
|
|
|
2780
3049
|
const line = formatCurrentTimeLine(/* @__PURE__ */ new Date());
|
|
2781
3050
|
const base = event.systemPrompt;
|
|
2782
3051
|
if (PI_DATE_LINE.test(base)) {
|
|
2783
|
-
log$
|
|
3052
|
+
log$6.info({ event: "pi_date_line_present" }, "pi base prompt carries its own 'Current date:' line again; replacing it in place (pi prompt format may have changed)");
|
|
2784
3053
|
return { systemPrompt: base.replace(PI_DATE_LINE, line) };
|
|
2785
3054
|
}
|
|
2786
3055
|
return { systemPrompt: `${base}\n${line}` };
|
|
@@ -3168,7 +3437,7 @@ async function migrateMemory({ cwd }) {
|
|
|
3168
3437
|
}
|
|
3169
3438
|
//#endregion
|
|
3170
3439
|
//#region src/extensions/memory.ts
|
|
3171
|
-
const log$
|
|
3440
|
+
const log$5 = logger.child({ module: "memory-extension" });
|
|
3172
3441
|
/**
|
|
3173
3442
|
* The standing instructions for the memory system. Always injected (even with
|
|
3174
3443
|
* no `MEMORY.md`) so the agent knows it can persist notes and how. `users/` is
|
|
@@ -3205,13 +3474,13 @@ const memoryExtension = (pi) => {
|
|
|
3205
3474
|
pi.on("session_start", async (_event, ctx) => {
|
|
3206
3475
|
try {
|
|
3207
3476
|
const migrated = await migrateMemory({ cwd: ctx.cwd });
|
|
3208
|
-
if (migrated !== null) log$
|
|
3477
|
+
if (migrated !== null) log$5.info({
|
|
3209
3478
|
event: "memory_migrated",
|
|
3210
3479
|
from: migrated.from,
|
|
3211
3480
|
to: migrated.to
|
|
3212
3481
|
}, "migrated memory layout to current version");
|
|
3213
3482
|
} catch (err) {
|
|
3214
|
-
log$
|
|
3483
|
+
log$5.warn({
|
|
3215
3484
|
err,
|
|
3216
3485
|
event: "memory_migration_failed"
|
|
3217
3486
|
}, "memory migration failed; continuing with existing index");
|
|
@@ -3223,7 +3492,7 @@ const memoryExtension = (pi) => {
|
|
|
3223
3492
|
index
|
|
3224
3493
|
});
|
|
3225
3494
|
} catch (err) {
|
|
3226
|
-
log$
|
|
3495
|
+
log$5.warn({
|
|
3227
3496
|
err,
|
|
3228
3497
|
event: "memory_index_failed"
|
|
3229
3498
|
}, "memory index read failed; injecting instructions only");
|
|
@@ -3237,7 +3506,7 @@ const memoryExtension = (pi) => {
|
|
|
3237
3506
|
};
|
|
3238
3507
|
//#endregion
|
|
3239
3508
|
//#region src/extensions/platform-memory.ts
|
|
3240
|
-
const log$
|
|
3509
|
+
const log$4 = logger.child({ module: "platform-memory-extension" });
|
|
3241
3510
|
/**
|
|
3242
3511
|
* Resolve the human on this turn via the API, keyed by the message id.
|
|
3243
3512
|
* `/sandbox/channel-context` only returns a sender for a platform-known
|
|
@@ -3250,13 +3519,13 @@ const log$5 = logger.child({ module: "platform-memory-extension" });
|
|
|
3250
3519
|
async function resolveTurnUser(messageId) {
|
|
3251
3520
|
const client = sandboxClient();
|
|
3252
3521
|
if (!client) {
|
|
3253
|
-
log$
|
|
3522
|
+
log$4.debug({ event: "resolve_turn_user_no_api_url" }, "no API url in env; withholding user memory");
|
|
3254
3523
|
return null;
|
|
3255
3524
|
}
|
|
3256
3525
|
try {
|
|
3257
3526
|
const res = await client["channel-context"].$get({ query: { messageId } });
|
|
3258
3527
|
if (!res.ok) {
|
|
3259
|
-
log$
|
|
3528
|
+
log$4.warn({
|
|
3260
3529
|
event: "resolve_turn_user_failed",
|
|
3261
3530
|
status: res.status
|
|
3262
3531
|
}, "channel-context returned non-ok; withholding user memory");
|
|
@@ -3269,7 +3538,7 @@ async function resolveTurnUser(messageId) {
|
|
|
3269
3538
|
displayName: sender.displayName
|
|
3270
3539
|
};
|
|
3271
3540
|
} catch (err) {
|
|
3272
|
-
log$
|
|
3541
|
+
log$4.warn({
|
|
3273
3542
|
err,
|
|
3274
3543
|
event: "resolve_turn_user_failed"
|
|
3275
3544
|
}, "failed to resolve current user; withholding user memory");
|
|
@@ -3316,7 +3585,7 @@ function createPlatformMemoryExtension({ channelContext }) {
|
|
|
3316
3585
|
user
|
|
3317
3586
|
});
|
|
3318
3587
|
} catch (err) {
|
|
3319
|
-
log$
|
|
3588
|
+
log$4.warn({
|
|
3320
3589
|
err,
|
|
3321
3590
|
event: "user_memory_index_failed"
|
|
3322
3591
|
}, "user memory index build failed; skipping injection");
|
|
@@ -3331,7 +3600,7 @@ function createPlatformMemoryExtension({ channelContext }) {
|
|
|
3331
3600
|
}
|
|
3332
3601
|
//#endregion
|
|
3333
3602
|
//#region src/extensions/self-trace.ts
|
|
3334
|
-
const log$
|
|
3603
|
+
const log$3 = logger.child({ module: "self-trace-extension" });
|
|
3335
3604
|
/**
|
|
3336
3605
|
* Reports the agent's own execution as OpenTelemetry spans:
|
|
3337
3606
|
* agent.session → agent.run → agent.turn.N → tool.NAME, with token/cost
|
|
@@ -3356,7 +3625,7 @@ const selfTraceExtension = (pi) => {
|
|
|
3356
3625
|
sessionSpan = tracer.startSpan("agent.session", { attributes: { "agent.model": modelId } }, remoteCtx);
|
|
3357
3626
|
sessionCtx = trace.setSpan(remoteCtx, sessionSpan);
|
|
3358
3627
|
const sc = sessionSpan.spanContext();
|
|
3359
|
-
log$
|
|
3628
|
+
log$3.info({
|
|
3360
3629
|
event: "self_trace_session_start",
|
|
3361
3630
|
trace_id: sc.traceId,
|
|
3362
3631
|
span_id: sc.spanId,
|
|
@@ -3468,13 +3737,13 @@ const selfTraceExtension = (pi) => {
|
|
|
3468
3737
|
* Lives in the harness package — soul.md is content from the agent's
|
|
3469
3738
|
* own git repo, not from the platform — so its handling stays here.
|
|
3470
3739
|
*/
|
|
3471
|
-
const log$
|
|
3740
|
+
const log$2 = logger.child({ module: "soul-extension" });
|
|
3472
3741
|
async function readSoul(cwd) {
|
|
3473
3742
|
try {
|
|
3474
3743
|
return (await readFile(join(cwd, "soul.md"), "utf8")).trim() || null;
|
|
3475
3744
|
} catch (err) {
|
|
3476
3745
|
if (err?.code === "ENOENT") return null;
|
|
3477
|
-
log$
|
|
3746
|
+
log$2.warn({
|
|
3478
3747
|
err,
|
|
3479
3748
|
event: "soul_read_failed"
|
|
3480
3749
|
}, "soul.md read failed");
|
|
@@ -3506,7 +3775,7 @@ const soulExtension = (pi) => {
|
|
|
3506
3775
|
};
|
|
3507
3776
|
//#endregion
|
|
3508
3777
|
//#region src/extensions/subagent/index.ts
|
|
3509
|
-
const log$
|
|
3778
|
+
const log$1 = logger.child({ module: "subagent-ext" });
|
|
3510
3779
|
const MAX_TASKS = 8;
|
|
3511
3780
|
const TaskItem = Type.Object({
|
|
3512
3781
|
task: Type.String({ description: "The task to delegate to a subagent run." }),
|
|
@@ -3566,7 +3835,7 @@ function buildTool(messageId) {
|
|
|
3566
3835
|
tasks: spawnTasks
|
|
3567
3836
|
});
|
|
3568
3837
|
const { taskIds } = spawned;
|
|
3569
|
-
log$
|
|
3838
|
+
log$1.info({
|
|
3570
3839
|
event: "subagent_spawned",
|
|
3571
3840
|
count: taskIds.length
|
|
3572
3841
|
}, "subagent tasks queued");
|
|
@@ -3589,7 +3858,7 @@ function buildTool(messageId) {
|
|
|
3589
3858
|
};
|
|
3590
3859
|
} catch (err) {
|
|
3591
3860
|
const message = err instanceof Error ? err.message : String(err);
|
|
3592
|
-
log$
|
|
3861
|
+
log$1.warn({
|
|
3593
3862
|
err,
|
|
3594
3863
|
event: "subagent_spawn_failed"
|
|
3595
3864
|
}, "subagent spawn failed");
|
|
@@ -3620,7 +3889,7 @@ function createSubagentExtension({ channelContext }) {
|
|
|
3620
3889
|
if (registered) return;
|
|
3621
3890
|
registered = true;
|
|
3622
3891
|
pi.registerTool(buildTool(messageId));
|
|
3623
|
-
log$
|
|
3892
|
+
log$1.info({ event: "subagent_enabled" }, "subagent tool registered");
|
|
3624
3893
|
};
|
|
3625
3894
|
pi.on("session_start", () => {
|
|
3626
3895
|
registerOnce();
|
|
@@ -3654,214 +3923,6 @@ const toolCallEnvExtension = (pi) => {
|
|
|
3654
3923
|
});
|
|
3655
3924
|
};
|
|
3656
3925
|
//#endregion
|
|
3657
|
-
//#region src/extensions/tool-call-summary.ts
|
|
3658
|
-
const log$1 = logger.child({ module: "tool-call-summary-extension" });
|
|
3659
|
-
/**
|
|
3660
|
-
* The injected parameter name: a namespaced sentinel, so it can never collide
|
|
3661
|
-
* with a real tool argument and is unmistakable in transcripts and logs. The
|
|
3662
|
-
* frontend renderer (ANY-2723) duplicates this literal — keep the two in sync.
|
|
3663
|
-
*/
|
|
3664
|
-
const TOOL_CALL_SUMMARY_FIELD = "__skydive_summary__";
|
|
3665
|
-
/** JSON Schema fragment for the injected parameter. */
|
|
3666
|
-
const SUMMARY_PROPERTY = {
|
|
3667
|
-
type: "string",
|
|
3668
|
-
description: "Required for every tool call. A concise, specific summary (max ~8 words) of what THIS call does and why, written for a person watching the conversation, e.g. \"Searching feedback for billing complaints\" or \"Reading the auth middleware\". Address the user directly in second person: the summary is read by the user, so refer to their things as \"your\", never in third person — \"Reading your emails\", not \"Reading his emails\". Always use the present progressive tense, since it is shown while the call runs: \"Updating your Slack\", never \"Updated your Slack\". Make each summary distinct from your other tool calls; never reuse a generic label like \"Search query\" or \"Running command\"."
|
|
3669
|
-
};
|
|
3670
|
-
const jsonSchemaObjectSchema = z.object({
|
|
3671
|
-
type: z.unknown().optional(),
|
|
3672
|
-
properties: z.record(z.string(), z.unknown()).optional(),
|
|
3673
|
-
required: z.array(z.string()).optional(),
|
|
3674
|
-
additionalProperties: z.unknown().optional()
|
|
3675
|
-
}).passthrough();
|
|
3676
|
-
const toolEntrySchema = z.object({
|
|
3677
|
-
name: z.string().optional(),
|
|
3678
|
-
input_schema: jsonSchemaObjectSchema.optional(),
|
|
3679
|
-
parameters: jsonSchemaObjectSchema.optional(),
|
|
3680
|
-
function: z.object({
|
|
3681
|
-
name: z.string().optional(),
|
|
3682
|
-
parameters: jsonSchemaObjectSchema.optional()
|
|
3683
|
-
}).passthrough().optional()
|
|
3684
|
-
}).passthrough();
|
|
3685
|
-
const payloadWithToolsSchema = z.object({ tools: z.array(z.unknown()) }).passthrough();
|
|
3686
|
-
/**
|
|
3687
|
-
* Add the summary property to one JSON Schema object. Returns the augmented
|
|
3688
|
-
* copy, or `null` when the tool should be left untouched: a strict schema
|
|
3689
|
-
* (`additionalProperties: false`) whose validation would reject the extra
|
|
3690
|
-
* field, or one that already declares a `__skydive_summary__` property of its own.
|
|
3691
|
-
*/
|
|
3692
|
-
function augmentSchema(schema) {
|
|
3693
|
-
if (schema.additionalProperties === false) return null;
|
|
3694
|
-
const properties = schema.properties ?? {};
|
|
3695
|
-
if ("__skydive_summary__" in properties) return null;
|
|
3696
|
-
const required = schema.required ?? [];
|
|
3697
|
-
return {
|
|
3698
|
-
...schema,
|
|
3699
|
-
type: schema.type ?? "object",
|
|
3700
|
-
properties: {
|
|
3701
|
-
[TOOL_CALL_SUMMARY_FIELD]: SUMMARY_PROPERTY,
|
|
3702
|
-
...properties
|
|
3703
|
-
},
|
|
3704
|
-
required: required.includes("__skydive_summary__") ? required : [...required, TOOL_CALL_SUMMARY_FIELD]
|
|
3705
|
-
};
|
|
3706
|
-
}
|
|
3707
|
-
/**
|
|
3708
|
-
* Augment a single tool entry, dispatching on which provider shape it is.
|
|
3709
|
-
* Returns the (possibly rebuilt) entry and whether anything changed. Skipped
|
|
3710
|
-
* tools — wrong shape, strict, or name in `strictToolNames` — return unchanged.
|
|
3711
|
-
*/
|
|
3712
|
-
function augmentToolEntry(entry, strictToolNames) {
|
|
3713
|
-
const parsed = toolEntrySchema.safeParse(entry);
|
|
3714
|
-
if (!parsed.success) return {
|
|
3715
|
-
entry,
|
|
3716
|
-
changed: false
|
|
3717
|
-
};
|
|
3718
|
-
const tool = parsed.data;
|
|
3719
|
-
const name = tool.name ?? tool.function?.name ?? null;
|
|
3720
|
-
if (name !== null && strictToolNames.has(name)) return {
|
|
3721
|
-
entry,
|
|
3722
|
-
changed: false
|
|
3723
|
-
};
|
|
3724
|
-
if (tool.input_schema) {
|
|
3725
|
-
const augmented = augmentSchema(tool.input_schema);
|
|
3726
|
-
if (!augmented) return {
|
|
3727
|
-
entry,
|
|
3728
|
-
changed: false
|
|
3729
|
-
};
|
|
3730
|
-
return {
|
|
3731
|
-
entry: {
|
|
3732
|
-
...tool,
|
|
3733
|
-
input_schema: augmented
|
|
3734
|
-
},
|
|
3735
|
-
changed: true
|
|
3736
|
-
};
|
|
3737
|
-
}
|
|
3738
|
-
if (tool.parameters) {
|
|
3739
|
-
const augmented = augmentSchema(tool.parameters);
|
|
3740
|
-
if (!augmented) return {
|
|
3741
|
-
entry,
|
|
3742
|
-
changed: false
|
|
3743
|
-
};
|
|
3744
|
-
return {
|
|
3745
|
-
entry: {
|
|
3746
|
-
...tool,
|
|
3747
|
-
parameters: augmented
|
|
3748
|
-
},
|
|
3749
|
-
changed: true
|
|
3750
|
-
};
|
|
3751
|
-
}
|
|
3752
|
-
if (tool.function?.parameters) {
|
|
3753
|
-
const augmented = augmentSchema(tool.function.parameters);
|
|
3754
|
-
if (!augmented) return {
|
|
3755
|
-
entry,
|
|
3756
|
-
changed: false
|
|
3757
|
-
};
|
|
3758
|
-
return {
|
|
3759
|
-
entry: {
|
|
3760
|
-
...tool,
|
|
3761
|
-
function: {
|
|
3762
|
-
...tool.function,
|
|
3763
|
-
parameters: augmented
|
|
3764
|
-
}
|
|
3765
|
-
},
|
|
3766
|
-
changed: true
|
|
3767
|
-
};
|
|
3768
|
-
}
|
|
3769
|
-
return {
|
|
3770
|
-
entry,
|
|
3771
|
-
changed: false
|
|
3772
|
-
};
|
|
3773
|
-
}
|
|
3774
|
-
/**
|
|
3775
|
-
* Inject the summary field into every eligible tool in a provider payload.
|
|
3776
|
-
* Returns a new payload when at least one tool was augmented, or `undefined`
|
|
3777
|
-
* to signal "no change" (which keeps the original payload, per the
|
|
3778
|
-
* `before_provider_request` contract).
|
|
3779
|
-
*
|
|
3780
|
-
* @param payload The outgoing provider payload (shape varies by provider).
|
|
3781
|
-
* @param strictToolNames Names of tools whose registered schema is strict and
|
|
3782
|
-
* must be skipped to avoid validation errors.
|
|
3783
|
-
*/
|
|
3784
|
-
function injectToolCallSummary(payload, strictToolNames) {
|
|
3785
|
-
const parsed = payloadWithToolsSchema.safeParse(payload);
|
|
3786
|
-
if (!parsed.success || parsed.data.tools.length === 0) return void 0;
|
|
3787
|
-
let changed = false;
|
|
3788
|
-
const tools = parsed.data.tools.map((entry) => {
|
|
3789
|
-
const result = augmentToolEntry(entry, strictToolNames);
|
|
3790
|
-
if (result.changed) changed = true;
|
|
3791
|
-
return result.entry;
|
|
3792
|
-
});
|
|
3793
|
-
if (!changed) return void 0;
|
|
3794
|
-
return {
|
|
3795
|
-
...parsed.data,
|
|
3796
|
-
tools
|
|
3797
|
-
};
|
|
3798
|
-
}
|
|
3799
|
-
/**
|
|
3800
|
-
* Names of registered tools whose schema sets `additionalProperties: false`.
|
|
3801
|
-
* Pi validates the model's tool args against this registered schema, so the
|
|
3802
|
-
* injected field would make a strict tool's call fail validation — skip them.
|
|
3803
|
-
*/
|
|
3804
|
-
function getStrictToolNames(pi) {
|
|
3805
|
-
const names = /* @__PURE__ */ new Set();
|
|
3806
|
-
for (const tool of pi.getAllTools()) {
|
|
3807
|
-
const parsed = jsonSchemaObjectSchema.safeParse(tool.parameters);
|
|
3808
|
-
if (parsed.success && parsed.data.additionalProperties === false) names.add(tool.name);
|
|
3809
|
-
}
|
|
3810
|
-
return names;
|
|
3811
|
-
}
|
|
3812
|
-
function toolDeclaresSummaryParam(pi, toolName) {
|
|
3813
|
-
const tool = pi.getAllTools().find((candidate) => candidate.name === toolName);
|
|
3814
|
-
if (!tool) return false;
|
|
3815
|
-
const parsed = jsonSchemaObjectSchema.safeParse(tool.parameters);
|
|
3816
|
-
return parsed.success && parsed.data.properties != null && "__skydive_summary__" in parsed.data.properties;
|
|
3817
|
-
}
|
|
3818
|
-
/**
|
|
3819
|
-
* Remove the injected summary from a tool's execution input. No-op when the
|
|
3820
|
-
* field is absent, or when the tool genuinely declares a `__skydive_summary__`
|
|
3821
|
-
* parameter of its own (which we never inject into, so its value is real).
|
|
3822
|
-
* Mutates `input` in place, matching the `tool_call` contract.
|
|
3823
|
-
*
|
|
3824
|
-
* Fails open: this runs on the critical path of tool execution, and the
|
|
3825
|
-
* `getAllTools()` lookup can throw. On any error we leave `input` untouched
|
|
3826
|
-
* (the sentinel may pass through to the tool, but a bug here can never break
|
|
3827
|
-
* tool execution).
|
|
3828
|
-
*/
|
|
3829
|
-
function stripInjectedSummary(pi, toolName, input) {
|
|
3830
|
-
try {
|
|
3831
|
-
if (!("__skydive_summary__" in input)) return;
|
|
3832
|
-
if (toolDeclaresSummaryParam(pi, toolName)) return;
|
|
3833
|
-
delete input[TOOL_CALL_SUMMARY_FIELD];
|
|
3834
|
-
} catch (err) {
|
|
3835
|
-
log$1.error({
|
|
3836
|
-
err,
|
|
3837
|
-
event: "tool_call_summary_strip_failed",
|
|
3838
|
-
toolName
|
|
3839
|
-
}, "tool_call_summary strip failed; leaving tool input untouched");
|
|
3840
|
-
}
|
|
3841
|
-
}
|
|
3842
|
-
/**
|
|
3843
|
-
* Compute the rewritten payload for a `before_provider_request` event, failing
|
|
3844
|
-
* open: on any error the original payload is left untouched so a bug here can
|
|
3845
|
-
* never break an LLM call.
|
|
3846
|
-
*/
|
|
3847
|
-
function buildInjectedPayload(pi, payload) {
|
|
3848
|
-
try {
|
|
3849
|
-
return injectToolCallSummary(payload, getStrictToolNames(pi));
|
|
3850
|
-
} catch (err) {
|
|
3851
|
-
log$1.error({
|
|
3852
|
-
err,
|
|
3853
|
-
event: "tool_call_summary_injection_failed"
|
|
3854
|
-
}, "tool_call_summary injection failed; passing payload through unchanged");
|
|
3855
|
-
return;
|
|
3856
|
-
}
|
|
3857
|
-
}
|
|
3858
|
-
const toolCallSummaryExtension = (pi) => {
|
|
3859
|
-
pi.on("before_provider_request", (event) => buildInjectedPayload(pi, event.payload));
|
|
3860
|
-
pi.on("tool_call", (event) => {
|
|
3861
|
-
stripInjectedSummary(pi, event.toolName, event.input);
|
|
3862
|
-
});
|
|
3863
|
-
};
|
|
3864
|
-
//#endregion
|
|
3865
3926
|
//#region src/extensions/background-tasks.ts
|
|
3866
3927
|
/**
|
|
3867
3928
|
* Background bash tasks as a pi extension.
|