@skydiveai/pi-extensions 0.1.0-beta.2591 → 0.1.0-beta.2595

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 +160 -195
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -4,8 +4,8 @@ import { UserBuilder, restHandler } from "@a2a-js/sdk/server/express";
4
4
  import { buildAgentCard, chainMiddleware, composeHandlers, createAgentExecutor, createProtocolHandlers, getCurrentTraceparent, logger, mountAt, requestHeaders, requestUrl, webHandlerToMiddleware } from "@skydiveai/pi-server";
5
5
  import { mkdir, open, readFile, readdir, stat, unlink, writeFile } from "node:fs/promises";
6
6
  import { basename, dirname, join, relative, resolve } from "node:path";
7
- import { z } from "zod";
8
7
  import { pathToFileURL } from "node:url";
8
+ import { z } from "zod";
9
9
  import { CallToolResultSchema } from "@modelcontextprotocol/sdk/types.js";
10
10
  import { Type } from "typebox";
11
11
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
@@ -242,167 +242,6 @@ function createHealthHandler({ metadata }) {
242
242
  };
243
243
  }
244
244
  //#endregion
245
- //#region src/extensions/context-management-config.ts
246
- /**
247
- * Configuration for the context-management capability (tool-output trimming
248
- * and client-side microcompact). See the design notes in the extension for
249
- * what each layer does; this module is purely the env → config surface.
250
- *
251
- * The harness is provider-agnostic and runs inside the sandbox, where its
252
- * only config channel is the environment (loaded by the platform env
253
- * middleware before any session starts — see platform-env-middleware.ts). So
254
- * the LaunchDarkly flag `harness-context-management-enabled` is resolved by
255
- * the platform when it provisions the sandbox and passed through as
256
- * `SKYDIVE_CONTEXT_MANAGEMENT`; the individual knobs override the defaults
257
- * below when present.
258
- *
259
- * Resolution is defensive: a malformed value never throws (this config is
260
- * read on the hot path before every LLM call), it falls back to the default
261
- * for that knob and logs once.
262
- */
263
- const log$15 = logger.child({ module: "context-management-config" });
264
- const DEFAULT_CONTEXT_MANAGEMENT_CONFIG = {
265
- enabled: false,
266
- perResultMaxBytes: 16 * 1024,
267
- keepRecentToolResults: 3,
268
- coldCacheGapSeconds: 240,
269
- warmClearTriggerTokens: 5e4,
270
- clearAtLeastTokens: 1e4,
271
- excludeTools: [],
272
- maxModelCallsPerTurn: 80,
273
- nativeAnthropicEdits: false
274
- };
275
- const boolFromEnv = (value, fallback) => {
276
- if (value === void 0) return fallback;
277
- const normalized = value.trim().toLowerCase();
278
- if (normalized === "1" || normalized === "true") return true;
279
- if (normalized === "0" || normalized === "false") return false;
280
- return fallback;
281
- };
282
- const positiveInt = (fallback) => z.coerce.number().int().positive().catch(fallback);
283
- const nonNegativeInt = (fallback) => z.coerce.number().int().nonnegative().catch(fallback);
284
- const configSchema = z.object({
285
- perResultMaxBytes: positiveInt(DEFAULT_CONTEXT_MANAGEMENT_CONFIG.perResultMaxBytes),
286
- keepRecentToolResults: nonNegativeInt(DEFAULT_CONTEXT_MANAGEMENT_CONFIG.keepRecentToolResults),
287
- coldCacheGapSeconds: positiveInt(DEFAULT_CONTEXT_MANAGEMENT_CONFIG.coldCacheGapSeconds),
288
- warmClearTriggerTokens: positiveInt(DEFAULT_CONTEXT_MANAGEMENT_CONFIG.warmClearTriggerTokens),
289
- clearAtLeastTokens: positiveInt(DEFAULT_CONTEXT_MANAGEMENT_CONFIG.clearAtLeastTokens),
290
- maxModelCallsPerTurn: positiveInt(DEFAULT_CONTEXT_MANAGEMENT_CONFIG.maxModelCallsPerTurn)
291
- });
292
- const parseExcludeTools = (value) => {
293
- if (!value) return DEFAULT_CONTEXT_MANAGEMENT_CONFIG.excludeTools;
294
- return value.split(",").map((name) => name.trim()).filter((name) => name.length > 0);
295
- };
296
- /**
297
- * Reads the context-management config from `env` (defaults to `process.env`).
298
- * Never throws — invalid values fall back to defaults.
299
- */
300
- function resolveContextManagementConfig(env = process.env) {
301
- if (!boolFromEnv(env.SKYDIVE_CONTEXT_MANAGEMENT, false)) return { ...DEFAULT_CONTEXT_MANAGEMENT_CONFIG };
302
- const parsed = configSchema.safeParse({
303
- perResultMaxBytes: env.SKYDIVE_CTX_PER_RESULT_MAX_BYTES,
304
- keepRecentToolResults: env.SKYDIVE_CTX_KEEP_RECENT,
305
- coldCacheGapSeconds: env.SKYDIVE_CTX_COLD_GAP_SECONDS,
306
- warmClearTriggerTokens: env.SKYDIVE_CTX_WARM_TRIGGER_TOKENS,
307
- clearAtLeastTokens: env.SKYDIVE_CTX_CLEAR_AT_LEAST_TOKENS,
308
- maxModelCallsPerTurn: env.SKYDIVE_CTX_MAX_MODEL_CALLS
309
- });
310
- if (!parsed.success) {
311
- log$15.warn({
312
- event: "context_management_config_invalid",
313
- err: parsed.error
314
- }, "falling back to default context-management config");
315
- return {
316
- ...DEFAULT_CONTEXT_MANAGEMENT_CONFIG,
317
- enabled: true
318
- };
319
- }
320
- return {
321
- enabled: true,
322
- ...parsed.data,
323
- excludeTools: parseExcludeTools(env.SKYDIVE_CTX_EXCLUDE_TOOLS),
324
- nativeAnthropicEdits: boolFromEnv(env.SKYDIVE_CTX_NATIVE_ANTHROPIC_EDITS, DEFAULT_CONTEXT_MANAGEMENT_CONFIG.nativeAnthropicEdits)
325
- };
326
- }
327
- //#endregion
328
- //#region src/extensions/context-management-runtime.ts
329
- /**
330
- * Live, mutable view of the context-management config.
331
- *
332
- * The env-derived config (context-management-config.ts) is the boot-time
333
- * default. On top of it, the platform can deliver a global on/off at runtime
334
- * via the LaunchDarkly flag `harness-context-management-enabled` — resolved
335
- * server-side and polled by the harness (see the poller in
336
- * context-management.ts). This holder is where that override lands so a flip
337
- * (especially a kill-switch) reaches long-lived sandboxes without a restart.
338
- *
339
- * Only the master `enabled` toggle is overridable at runtime; the per-knob
340
- * tunables stay env-derived. `enabled` resolves to the flag override when the
341
- * platform has reported one, else the env value.
342
- */
343
- let baseConfig = null;
344
- let flagOverride = null;
345
- function base() {
346
- baseConfig ??= resolveContextManagementConfig();
347
- return baseConfig;
348
- }
349
- /** The effective config, with the runtime flag override applied to `enabled`. */
350
- function getContextManagementConfig() {
351
- const resolved = base();
352
- return {
353
- ...resolved,
354
- enabled: flagOverride ?? resolved.enabled
355
- };
356
- }
357
- /**
358
- * Apply the platform-reported flag value. `null` clears the override (fall back
359
- * to the env default) — used when the phone-home result is indeterminate so a
360
- * transient failure never silently changes behavior.
361
- */
362
- function setContextManagementFlagOverride(enabled) {
363
- flagOverride = enabled;
364
- }
365
- /**
366
- * Whether a phone-home channel exists to learn the flag at runtime. When false
367
- * (e.g. a bare local CLI with no platform API), the env value is the only
368
- * source and there's nothing to poll.
369
- */
370
- function hasFlagSource(env = process.env) {
371
- return Boolean(env.SKYDIVE_API_URL ?? env.ANYONE_API_URL);
372
- }
373
- //#endregion
374
- //#region src/iteration-cap.ts
375
- function installIterationCap({ session, log }, configOverride = null) {
376
- const readConfig = () => configOverride ?? getContextManagementConfig();
377
- if (!readConfig().enabled && (configOverride !== null || !hasFlagSource())) return;
378
- const agent = session.agent;
379
- if (typeof agent.createLoopConfig !== "function") throw new Error("installIterationCap: session.agent.createLoopConfig is missing — pi-agent-core internals changed; update iteration-cap.ts.");
380
- const original = agent.createLoopConfig.bind(agent);
381
- agent.createLoopConfig = (options) => {
382
- const loopConfig = original(options);
383
- const previousStop = loopConfig.shouldStopAfterTurn;
384
- let modelCalls = 0;
385
- return {
386
- ...loopConfig,
387
- shouldStopAfterTurn: async (ctx) => {
388
- if (previousStop && await previousStop(ctx)) return true;
389
- const config = readConfig();
390
- if (!config.enabled) return false;
391
- modelCalls += 1;
392
- if (modelCalls >= config.maxModelCallsPerTurn) {
393
- log.warn({
394
- event: "iteration_cap_reached",
395
- modelCalls,
396
- max: config.maxModelCallsPerTurn
397
- }, "reached per-turn model-call cap; stopping turn gracefully");
398
- return true;
399
- }
400
- return false;
401
- }
402
- };
403
- };
404
- }
405
- //#endregion
406
245
  //#region src/extensions/capability-soul-nudge.ts
407
246
  /**
408
247
  * One-line reminder appended to a tool-update continuation when the agent
@@ -420,7 +259,7 @@ function installIterationCap({ session, log }, configOverride = null) {
420
259
  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).";
421
260
  //#endregion
422
261
  //#region src/extensions/tool-call-summary.ts
423
- const log$14 = logger.child({ module: "tool-call-summary-extension" });
262
+ const log$15 = logger.child({ module: "tool-call-summary-extension" });
424
263
  /**
425
264
  * The injected parameter name: a namespaced sentinel, so it can never collide
426
265
  * with a real tool argument and is unmistakable in transcripts and logs. The
@@ -628,7 +467,7 @@ function withSummaryStrippedBeforeValidation(tool) {
628
467
  try {
629
468
  stripped = withoutInjectedSummary(args);
630
469
  } catch (err) {
631
- log$14.error({
470
+ log$15.error({
632
471
  err,
633
472
  event: "tool_call_summary_prepare_strip_failed",
634
473
  toolName: tool.name
@@ -658,7 +497,7 @@ function stripInjectedSummary(pi, toolName, input) {
658
497
  if (toolDeclaresSummaryParam(pi, toolName)) return;
659
498
  delete input[TOOL_CALL_SUMMARY_FIELD];
660
499
  } catch (err) {
661
- log$14.error({
500
+ log$15.error({
662
501
  err,
663
502
  event: "tool_call_summary_strip_failed",
664
503
  toolName
@@ -674,7 +513,7 @@ function buildInjectedPayload(pi, payload) {
674
513
  try {
675
514
  return injectToolCallSummary(payload, getStrictToolNames(pi));
676
515
  } catch (err) {
677
- log$14.error({
516
+ log$15.error({
678
517
  err,
679
518
  event: "tool_call_summary_injection_failed"
680
519
  }, "tool_call_summary injection failed; passing payload through unchanged");
@@ -711,7 +550,7 @@ const toolCallSummaryExtension = (pi) => {
711
550
  * or `ToolDefinition[]`. Files starting with `_` or `.` are skipped, so
712
551
  * `tools/_example.ts` documents the shape without registering.
713
552
  */
714
- const log$13 = logger.child({ module: "local-tools-extension" });
553
+ const log$14 = logger.child({ module: "local-tools-extension" });
715
554
  const TOOLS_DIRNAME = "tools";
716
555
  const fileState = /* @__PURE__ */ new Map();
717
556
  let pendingLocalToolsUpdate = null;
@@ -855,7 +694,7 @@ async function reconcileAndQueue({ pi, dir, reason }) {
855
694
  dir
856
695
  });
857
696
  if (reason !== "session_start" && summaryHasChanges$1(summary)) pendingLocalToolsUpdate = summary;
858
- log$13.info({
697
+ log$14.info({
859
698
  event: "local_tools_reconcile",
860
699
  reason,
861
700
  total_tools: summary.totalTools,
@@ -877,7 +716,7 @@ const localToolsExtension = (pi) => {
877
716
  reason: "session_start"
878
717
  });
879
718
  } catch (err) {
880
- log$13.error({
719
+ log$14.error({
881
720
  err,
882
721
  event: "local_tools_reconcile_failed"
883
722
  }, "local tools reconcile failed");
@@ -889,7 +728,7 @@ const localToolsExtension = (pi) => {
889
728
  try {
890
729
  current = await listToolFiles(dir);
891
730
  } catch (err) {
892
- log$13.warn({
731
+ log$14.warn({
893
732
  err,
894
733
  event: "local_tools_listing_failed"
895
734
  }, "tools/ listing failed");
@@ -911,7 +750,7 @@ const localToolsExtension = (pi) => {
911
750
  reason: "auto_reload"
912
751
  });
913
752
  } catch (err) {
914
- log$13.error({
753
+ log$14.error({
915
754
  err,
916
755
  event: "local_tools_auto_reload_failed"
917
756
  }, "auto-reload after tools/ change failed");
@@ -1368,7 +1207,7 @@ async function loadMcpConfig(path) {
1368
1207
  * Clients are keyed by JSON-stringified config and reused across
1369
1208
  * reloads — only changed configs reconnect.
1370
1209
  */
1371
- const log$12 = logger.child({ module: "mcp-extension" });
1210
+ const log$13 = logger.child({ module: "mcp-extension" });
1372
1211
  async function closeConnected(connected) {
1373
1212
  try {
1374
1213
  await connected.client.close();
@@ -1899,7 +1738,7 @@ var McpExtension = class {
1899
1738
  });
1900
1739
  this.lastConfigMtimeMs = await readConfigMtimeMs(configPath);
1901
1740
  if (reason !== "session_start" && summaryHasChanges(summary)) this.pendingMcpUpdate = summary;
1902
- log$12.info({
1741
+ log$13.info({
1903
1742
  event: "mcp_reconcile",
1904
1743
  reason,
1905
1744
  total_tools: summary.totalTools,
@@ -1926,7 +1765,7 @@ var McpExtension = class {
1926
1765
  contextWindow: ctx.model?.contextWindow
1927
1766
  });
1928
1767
  } catch (err) {
1929
- log$12.error({
1768
+ log$13.error({
1930
1769
  err,
1931
1770
  event: "mcp_reconcile_failed"
1932
1771
  }, "MCP reconcile failed");
@@ -1938,7 +1777,7 @@ var McpExtension = class {
1938
1777
  try {
1939
1778
  mtime = await readConfigMtimeMs(configPath);
1940
1779
  } catch (err) {
1941
- log$12.warn({
1780
+ log$13.warn({
1942
1781
  err,
1943
1782
  event: "mcp_mtime_check_failed"
1944
1783
  }, "mtime check on mcp.config.json failed");
@@ -1953,7 +1792,7 @@ var McpExtension = class {
1953
1792
  contextWindow: ctx.model?.contextWindow
1954
1793
  });
1955
1794
  } catch (err) {
1956
- log$12.error({
1795
+ log$13.error({
1957
1796
  err,
1958
1797
  event: "mcp_auto_reload_failed"
1959
1798
  }, "auto-reload after mcp.config.json change failed");
@@ -2284,7 +2123,7 @@ const HEARTBEAT_THROTTLE_MS = 6e4;
2284
2123
  const TOOL_HEARTBEAT_INTERVAL_MS = 5e3;
2285
2124
  const MAX_TOOL_HEARTBEATS = 1440 * 60 * 1e3 / TOOL_HEARTBEAT_INTERVAL_MS;
2286
2125
  const DAEMON_URL = "http://localhost:38994";
2287
- const log$11 = logger.child({ module: "platform-ext" });
2126
+ const log$12 = logger.child({ module: "platform-ext" });
2288
2127
  function sandboxClient() {
2289
2128
  const apiUrl = apiBaseUrl();
2290
2129
  if (!apiUrl) return null;
@@ -2352,7 +2191,7 @@ async function fetchHarnessFlags() {
2352
2191
  try {
2353
2192
  const res = await client["feature-flags"].$get();
2354
2193
  if (!res.ok) {
2355
- log$11.debug({
2194
+ log$12.debug({
2356
2195
  status: res.status,
2357
2196
  event: "feature_flags_fetch_failed"
2358
2197
  }, "feature-flags fetch failed");
@@ -2364,7 +2203,7 @@ async function fetchHarnessFlags() {
2364
2203
  closingText: body.closingText ?? null
2365
2204
  };
2366
2205
  } catch (err) {
2367
- log$11.debug({
2206
+ log$12.debug({
2368
2207
  err,
2369
2208
  event: "feature_flags_fetch_error"
2370
2209
  }, "feature-flags request errored");
@@ -2375,7 +2214,7 @@ function postHeartbeat({ messageId }) {
2375
2214
  const client = sandboxClient();
2376
2215
  if (!client) return;
2377
2216
  client.heartbeat.$post({ json: { messageId } }).catch((err) => {
2378
- log$11.debug({
2217
+ log$12.debug({
2379
2218
  err,
2380
2219
  event: "heartbeat_failed"
2381
2220
  }, "heartbeat failed");
@@ -2387,7 +2226,7 @@ async function resolveConversationFromApi(messageId) {
2387
2226
  try {
2388
2227
  const res = await client["message-conversation"].$get({ query: { messageId } });
2389
2228
  if (!res.ok) {
2390
- log$11.warn({
2229
+ log$12.warn({
2391
2230
  status: res.status,
2392
2231
  messageId,
2393
2232
  event: "resolve_conversation_failed"
@@ -2396,7 +2235,7 @@ async function resolveConversationFromApi(messageId) {
2396
2235
  }
2397
2236
  return (await res.json()).conversationId ?? null;
2398
2237
  } catch (err) {
2399
- log$11.warn({
2238
+ log$12.warn({
2400
2239
  err,
2401
2240
  messageId,
2402
2241
  event: "resolve_conversation_error"
@@ -2421,13 +2260,13 @@ async function putBackgroundTaskJournalSpec({ messageId, spec }) {
2421
2260
  messageId,
2422
2261
  spec
2423
2262
  } });
2424
- if (!res.ok) log$11.warn({
2263
+ if (!res.ok) log$12.warn({
2425
2264
  status: res.status,
2426
2265
  taskId: spec.id,
2427
2266
  event: "bg_journal_put_failed"
2428
2267
  }, "bg-task journal PUT failed");
2429
2268
  } catch (err) {
2430
- log$11.warn({
2269
+ log$12.warn({
2431
2270
  err,
2432
2271
  taskId: spec.id,
2433
2272
  event: "bg_journal_put_failed"
@@ -2443,7 +2282,7 @@ async function deleteBackgroundTaskJournalSpec({ messageId, taskId }) {
2443
2282
  taskId
2444
2283
  } });
2445
2284
  } catch (err) {
2446
- log$11.debug({
2285
+ log$12.debug({
2447
2286
  err,
2448
2287
  taskId,
2449
2288
  event: "bg_journal_delete_failed"
@@ -2458,7 +2297,7 @@ async function listBackgroundTaskJournalSpecs({ messageId }) {
2458
2297
  if (!res.ok) return [];
2459
2298
  return (await res.json()).specs ?? [];
2460
2299
  } catch (err) {
2461
- log$11.debug({
2300
+ log$12.debug({
2462
2301
  err,
2463
2302
  event: "bg_journal_list_failed"
2464
2303
  }, "bg-task journal GET failed");
@@ -2472,7 +2311,7 @@ function postBackgroundTasksSnapshot({ messageId, tasks }) {
2472
2311
  messageId,
2473
2312
  tasks
2474
2313
  } }).catch((err) => {
2475
- log$11.debug({
2314
+ log$12.debug({
2476
2315
  err,
2477
2316
  event: "bg_tasks_snapshot_failed"
2478
2317
  }, "bg-tasks snapshot publish failed");
@@ -2546,7 +2385,7 @@ function createToolHeartbeat({ messageId }) {
2546
2385
  }
2547
2386
  heartbeatCount++;
2548
2387
  if (heartbeatCount > MAX_TOOL_HEARTBEATS) {
2549
- log$11.warn({
2388
+ log$12.warn({
2550
2389
  heartbeatCount,
2551
2390
  activeToolCalls: [...activeToolCalls]
2552
2391
  }, "tool heartbeat max reached, stopping");
@@ -2577,7 +2416,7 @@ function postToDaemon(path, body) {
2577
2416
  headers: { "content-type": "application/json" },
2578
2417
  body: JSON.stringify(body)
2579
2418
  }).catch((err) => {
2580
- log$11.debug({
2419
+ log$12.debug({
2581
2420
  err,
2582
2421
  path,
2583
2422
  event: "daemon_post_failed"
@@ -2586,7 +2425,7 @@ function postToDaemon(path, body) {
2586
2425
  }
2587
2426
  function createPlatformExtensions({ sessionId, channelContext }) {
2588
2427
  return (pi) => {
2589
- log$11.info({
2428
+ log$12.info({
2590
2429
  sessionId,
2591
2430
  hasChannelContext: Boolean(channelContext)
2592
2431
  }, "platform extension initialized");
@@ -2635,12 +2474,141 @@ function createPlatformExtensions({ sessionId, channelContext }) {
2635
2474
  });
2636
2475
  });
2637
2476
  pi.on("agent_end", () => {
2638
- log$11.info({ sessionId }, "session ending");
2477
+ log$12.info({ sessionId }, "session ending");
2639
2478
  postToDaemon("/session/end", { sessionId });
2640
2479
  });
2641
2480
  };
2642
2481
  }
2643
2482
  //#endregion
2483
+ //#region src/extensions/context-management-config.ts
2484
+ /**
2485
+ * Configuration for the context-management capability (tool-output trimming
2486
+ * and client-side microcompact). See the design notes in the extension for
2487
+ * what each layer does; this module is purely the env → config surface.
2488
+ *
2489
+ * The harness is provider-agnostic and runs inside the sandbox, where its
2490
+ * only config channel is the environment (loaded by the platform env
2491
+ * middleware before any session starts — see platform-env-middleware.ts).
2492
+ * `SKYDIVE_CONTEXT_MANAGEMENT` is the env-side master toggle; the individual
2493
+ * `SKYDIVE_CTX_*` knobs override the defaults below only when that toggle is
2494
+ * set. Prod never sets it: the LaunchDarkly flag
2495
+ * `harness-context-management-enabled` reaches running sandboxes through the
2496
+ * feature-flag poller (context-management-runtime.ts), which overlays only
2497
+ * `enabled`. So in prod every knob resolves to its default regardless of what
2498
+ * the platform forwards — see the knob-gating follow-up before relying on one.
2499
+ *
2500
+ * Resolution is defensive: a malformed value never throws (this config is
2501
+ * read on the hot path before every LLM call), it falls back to the default
2502
+ * for that knob and logs once.
2503
+ */
2504
+ const log$11 = logger.child({ module: "context-management-config" });
2505
+ const DEFAULT_CONTEXT_MANAGEMENT_CONFIG = {
2506
+ enabled: false,
2507
+ perResultMaxBytes: 16 * 1024,
2508
+ keepRecentToolResults: 3,
2509
+ coldCacheGapSeconds: 240,
2510
+ warmClearTriggerTokens: 5e4,
2511
+ clearAtLeastTokens: 1e4,
2512
+ excludeTools: [],
2513
+ nativeAnthropicEdits: false
2514
+ };
2515
+ const boolFromEnv = (value, fallback) => {
2516
+ if (value === void 0) return fallback;
2517
+ const normalized = value.trim().toLowerCase();
2518
+ if (normalized === "1" || normalized === "true") return true;
2519
+ if (normalized === "0" || normalized === "false") return false;
2520
+ return fallback;
2521
+ };
2522
+ const positiveInt = (fallback) => z.coerce.number().int().positive().catch(fallback);
2523
+ const nonNegativeInt = (fallback) => z.coerce.number().int().nonnegative().catch(fallback);
2524
+ const configSchema = z.object({
2525
+ perResultMaxBytes: positiveInt(DEFAULT_CONTEXT_MANAGEMENT_CONFIG.perResultMaxBytes),
2526
+ keepRecentToolResults: nonNegativeInt(DEFAULT_CONTEXT_MANAGEMENT_CONFIG.keepRecentToolResults),
2527
+ coldCacheGapSeconds: positiveInt(DEFAULT_CONTEXT_MANAGEMENT_CONFIG.coldCacheGapSeconds),
2528
+ warmClearTriggerTokens: positiveInt(DEFAULT_CONTEXT_MANAGEMENT_CONFIG.warmClearTriggerTokens),
2529
+ clearAtLeastTokens: positiveInt(DEFAULT_CONTEXT_MANAGEMENT_CONFIG.clearAtLeastTokens)
2530
+ });
2531
+ const parseExcludeTools = (value) => {
2532
+ if (!value) return DEFAULT_CONTEXT_MANAGEMENT_CONFIG.excludeTools;
2533
+ return value.split(",").map((name) => name.trim()).filter((name) => name.length > 0);
2534
+ };
2535
+ /**
2536
+ * Reads the context-management config from `env` (defaults to `process.env`).
2537
+ * Never throws — invalid values fall back to defaults.
2538
+ */
2539
+ function resolveContextManagementConfig(env = process.env) {
2540
+ if (!boolFromEnv(env.SKYDIVE_CONTEXT_MANAGEMENT, false)) return { ...DEFAULT_CONTEXT_MANAGEMENT_CONFIG };
2541
+ const parsed = configSchema.safeParse({
2542
+ perResultMaxBytes: env.SKYDIVE_CTX_PER_RESULT_MAX_BYTES,
2543
+ keepRecentToolResults: env.SKYDIVE_CTX_KEEP_RECENT,
2544
+ coldCacheGapSeconds: env.SKYDIVE_CTX_COLD_GAP_SECONDS,
2545
+ warmClearTriggerTokens: env.SKYDIVE_CTX_WARM_TRIGGER_TOKENS,
2546
+ clearAtLeastTokens: env.SKYDIVE_CTX_CLEAR_AT_LEAST_TOKENS
2547
+ });
2548
+ if (!parsed.success) {
2549
+ log$11.warn({
2550
+ event: "context_management_config_invalid",
2551
+ err: parsed.error
2552
+ }, "falling back to default context-management config");
2553
+ return {
2554
+ ...DEFAULT_CONTEXT_MANAGEMENT_CONFIG,
2555
+ enabled: true
2556
+ };
2557
+ }
2558
+ return {
2559
+ enabled: true,
2560
+ ...parsed.data,
2561
+ excludeTools: parseExcludeTools(env.SKYDIVE_CTX_EXCLUDE_TOOLS),
2562
+ nativeAnthropicEdits: boolFromEnv(env.SKYDIVE_CTX_NATIVE_ANTHROPIC_EDITS, DEFAULT_CONTEXT_MANAGEMENT_CONFIG.nativeAnthropicEdits)
2563
+ };
2564
+ }
2565
+ //#endregion
2566
+ //#region src/extensions/context-management-runtime.ts
2567
+ /**
2568
+ * Live, mutable view of the context-management config.
2569
+ *
2570
+ * The env-derived config (context-management-config.ts) is the boot-time
2571
+ * default. On top of it, the platform can deliver a global on/off at runtime
2572
+ * via the LaunchDarkly flag `harness-context-management-enabled` — resolved
2573
+ * server-side and polled by the harness (see the poller in
2574
+ * context-management.ts). This holder is where that override lands so a flip
2575
+ * (especially a kill-switch) reaches long-lived sandboxes without a restart.
2576
+ *
2577
+ * Only the master `enabled` toggle is overridable at runtime; the per-knob
2578
+ * tunables stay env-derived. `enabled` resolves to the flag override when the
2579
+ * platform has reported one, else the env value.
2580
+ */
2581
+ let baseConfig = null;
2582
+ let flagOverride = null;
2583
+ function base() {
2584
+ baseConfig ??= resolveContextManagementConfig();
2585
+ return baseConfig;
2586
+ }
2587
+ /** The effective config, with the runtime flag override applied to `enabled`. */
2588
+ function getContextManagementConfig() {
2589
+ const resolved = base();
2590
+ return {
2591
+ ...resolved,
2592
+ enabled: flagOverride ?? resolved.enabled
2593
+ };
2594
+ }
2595
+ /**
2596
+ * Apply the platform-reported flag value. `null` clears the override (fall back
2597
+ * to the env default) — used when the phone-home result is indeterminate so a
2598
+ * transient failure never silently changes behavior.
2599
+ */
2600
+ function setContextManagementFlagOverride(enabled) {
2601
+ flagOverride = enabled;
2602
+ }
2603
+ /**
2604
+ * Whether a phone-home channel exists to learn the flag at runtime. When false
2605
+ * (e.g. a bare local CLI with no platform API), the env value is the only
2606
+ * source and there's nothing to poll.
2607
+ */
2608
+ function hasFlagSource(env = process.env) {
2609
+ return Boolean(env.SKYDIVE_API_URL ?? env.ANYONE_API_URL);
2610
+ }
2611
+ //#endregion
2644
2612
  //#region src/extensions/feature-flags-poll.ts
2645
2613
  /**
2646
2614
  * Shared harness feature-flag poll.
@@ -2855,10 +2823,7 @@ function createHarness(options) {
2855
2823
  }, "platform runtime versions"));
2856
2824
  const serverOptions = {
2857
2825
  ...options,
2858
- onSessionSetup: options.onSessionSetup ?? ((args) => {
2859
- installToolUpdateAutoStop(args);
2860
- installIterationCap(args);
2861
- }),
2826
+ onSessionSetup: options.onSessionSetup ?? installToolUpdateAutoStop,
2862
2827
  postPrompt: options.postPrompt ?? (async (args) => {
2863
2828
  await runToolUpdateLoop(args);
2864
2829
  await runClosingTextLoop(args);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skydiveai/pi-extensions",
3
- "version": "0.1.0-beta.2591",
3
+ "version": "0.1.0-beta.2595",
4
4
  "homepage": "https://skydive.com",
5
5
  "license": "MIT",
6
6
  "author": "Create, Inc.",