@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.
- package/dist/index.mjs +160 -195
- 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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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 ??
|
|
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);
|