@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.
Files changed (2) hide show
  1. package/dist/index.mjs +329 -268
  2. 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$14 = logger.child({ module: "local-tools-extension" });
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$14.info({
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$14.error({
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$14.warn({
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$14.error({
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$13 = logger.child({ module: "mcp-extension" });
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$13.info({
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$13.error({
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$13.warn({
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$13.error({
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$12 = logger.child({ module: "resource-pressure-warning" });
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$12.warn({
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$12.error({ err }, "resource pressure check failed");
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$11 = logger.child({ module: "disk-guard" });
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$11.warn({
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$10 = logger.child({ module: "platform-ext" });
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$10.debug({
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$10.debug({
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$10.debug({
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$10.warn({
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$10.warn({
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$10.warn({
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$10.warn({
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$10.debug({
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$10.debug({
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$10.debug({
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$10.warn({
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$10.debug({
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$10.info({
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$10.info({ sessionId }, "session ending");
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$9 = logger.child({ module: "feature-flags-poll" });
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$9.warn({
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$9.debug({ err }, "feature-flag poll threw");
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$8 = logger.child({ module: "context-management-extension" });
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$8.info({
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$8.info({
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$8.info({
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$8.error({
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$7 = logger.child({ module: "current-time-extension" });
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$7.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)");
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$6 = logger.child({ module: "memory-extension" });
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$6.info({
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$6.warn({
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$6.warn({
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$5 = logger.child({ module: "platform-memory-extension" });
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$5.debug({ event: "resolve_turn_user_no_api_url" }, "no API url in env; withholding user memory");
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$5.warn({
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$5.warn({
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$5.warn({
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$4 = logger.child({ module: "self-trace-extension" });
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$4.info({
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$3 = logger.child({ module: "soul-extension" });
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$3.warn({
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$2 = logger.child({ module: "subagent-ext" });
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$2.info({
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$2.warn({
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$2.info({ event: "subagent_enabled" }, "subagent tool registered");
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skydiveai/pi-extensions",
3
- "version": "0.1.0-beta.1728",
3
+ "version": "0.1.0-beta.1729",
4
4
  "homepage": "https://skydive.com",
5
5
  "license": "MIT",
6
6
  "author": "Create, Inc.",