@skydiveai/pi-extensions 0.1.0-beta.751 → 0.1.0-beta.753
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 +264 -48
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -21,6 +21,9 @@ import { BatchSpanProcessor, NodeTracerProvider } from "@opentelemetry/sdk-trace
|
|
|
21
21
|
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
|
|
22
22
|
import { hc } from "hono/client";
|
|
23
23
|
import { parse } from "yaml";
|
|
24
|
+
import { execFile } from "node:child_process";
|
|
25
|
+
import { availableParallelism } from "node:os";
|
|
26
|
+
import { promisify } from "node:util";
|
|
24
27
|
import { quote } from "shell-quote";
|
|
25
28
|
import { createWriteStream } from "node:fs";
|
|
26
29
|
import { finished } from "node:stream/promises";
|
|
@@ -256,7 +259,7 @@ function createHealthHandler({ metadata }) {
|
|
|
256
259
|
* read on the hot path before every LLM call), it falls back to the default
|
|
257
260
|
* for that knob and logs once.
|
|
258
261
|
*/
|
|
259
|
-
const log$
|
|
262
|
+
const log$14 = logger.child({ module: "context-management-config" });
|
|
260
263
|
const DEFAULT_CONTEXT_MANAGEMENT_CONFIG = {
|
|
261
264
|
enabled: false,
|
|
262
265
|
perResultMaxBytes: 16 * 1024,
|
|
@@ -304,7 +307,7 @@ function resolveContextManagementConfig(env = process.env) {
|
|
|
304
307
|
maxModelCallsPerTurn: env.SKYDIVE_CTX_MAX_MODEL_CALLS
|
|
305
308
|
});
|
|
306
309
|
if (!parsed.success) {
|
|
307
|
-
log$
|
|
310
|
+
log$14.warn({
|
|
308
311
|
event: "context_management_config_invalid",
|
|
309
312
|
err: parsed.error
|
|
310
313
|
}, "falling back to default context-management config");
|
|
@@ -438,7 +441,7 @@ const CAPABILITY_SOUL_NUDGE = "New capability gained — once the current task i
|
|
|
438
441
|
* or `ToolDefinition[]`. Files starting with `_` or `.` are skipped, so
|
|
439
442
|
* `tools/_example.ts` documents the shape without registering.
|
|
440
443
|
*/
|
|
441
|
-
const log$
|
|
444
|
+
const log$13 = logger.child({ module: "local-tools-extension" });
|
|
442
445
|
const TOOLS_DIRNAME = "tools";
|
|
443
446
|
const fileState = /* @__PURE__ */ new Map();
|
|
444
447
|
let pendingLocalToolsUpdate = null;
|
|
@@ -582,7 +585,7 @@ async function reconcileAndQueue({ pi, dir, reason }) {
|
|
|
582
585
|
dir
|
|
583
586
|
});
|
|
584
587
|
if (reason !== "session_start" && summaryHasChanges$1(summary)) pendingLocalToolsUpdate = summary;
|
|
585
|
-
log$
|
|
588
|
+
log$13.info({
|
|
586
589
|
event: "local_tools_reconcile",
|
|
587
590
|
reason,
|
|
588
591
|
total_tools: summary.totalTools,
|
|
@@ -604,7 +607,7 @@ const localToolsExtension = (pi) => {
|
|
|
604
607
|
reason: "session_start"
|
|
605
608
|
});
|
|
606
609
|
} catch (err) {
|
|
607
|
-
log$
|
|
610
|
+
log$13.error({
|
|
608
611
|
err,
|
|
609
612
|
event: "local_tools_reconcile_failed"
|
|
610
613
|
}, "local tools reconcile failed");
|
|
@@ -616,7 +619,7 @@ const localToolsExtension = (pi) => {
|
|
|
616
619
|
try {
|
|
617
620
|
current = await listToolFiles(dir);
|
|
618
621
|
} catch (err) {
|
|
619
|
-
log$
|
|
622
|
+
log$13.warn({
|
|
620
623
|
err,
|
|
621
624
|
event: "local_tools_listing_failed"
|
|
622
625
|
}, "tools/ listing failed");
|
|
@@ -638,7 +641,7 @@ const localToolsExtension = (pi) => {
|
|
|
638
641
|
reason: "auto_reload"
|
|
639
642
|
});
|
|
640
643
|
} catch (err) {
|
|
641
|
-
log$
|
|
644
|
+
log$13.error({
|
|
642
645
|
err,
|
|
643
646
|
event: "local_tools_auto_reload_failed"
|
|
644
647
|
}, "auto-reload after tools/ change failed");
|
|
@@ -863,7 +866,7 @@ async function loadMcpConfig(path) {
|
|
|
863
866
|
* Clients are keyed by JSON-stringified config and reused across
|
|
864
867
|
* reloads — only changed configs reconnect.
|
|
865
868
|
*/
|
|
866
|
-
const log$
|
|
869
|
+
const log$12 = logger.child({ module: "mcp-extension" });
|
|
867
870
|
async function closeConnected(connected) {
|
|
868
871
|
try {
|
|
869
872
|
await connected.client.close();
|
|
@@ -1227,7 +1230,7 @@ var McpExtension = class {
|
|
|
1227
1230
|
});
|
|
1228
1231
|
this.lastConfigMtimeMs = await readConfigMtimeMs(configPath);
|
|
1229
1232
|
if (reason !== "session_start" && summaryHasChanges(summary)) this.pendingMcpUpdate = summary;
|
|
1230
|
-
log$
|
|
1233
|
+
log$12.info({
|
|
1231
1234
|
event: "mcp_reconcile",
|
|
1232
1235
|
reason,
|
|
1233
1236
|
total_tools: summary.totalTools,
|
|
@@ -1250,7 +1253,7 @@ var McpExtension = class {
|
|
|
1250
1253
|
reason: "session_start"
|
|
1251
1254
|
});
|
|
1252
1255
|
} catch (err) {
|
|
1253
|
-
log$
|
|
1256
|
+
log$12.error({
|
|
1254
1257
|
err,
|
|
1255
1258
|
event: "mcp_reconcile_failed"
|
|
1256
1259
|
}, "MCP reconcile failed");
|
|
@@ -1262,7 +1265,7 @@ var McpExtension = class {
|
|
|
1262
1265
|
try {
|
|
1263
1266
|
mtime = await readConfigMtimeMs(configPath);
|
|
1264
1267
|
} catch (err) {
|
|
1265
|
-
log$
|
|
1268
|
+
log$12.warn({
|
|
1266
1269
|
err,
|
|
1267
1270
|
event: "mcp_mtime_check_failed"
|
|
1268
1271
|
}, "mtime check on mcp.config.json failed");
|
|
@@ -1276,7 +1279,7 @@ var McpExtension = class {
|
|
|
1276
1279
|
reason: "auto_reload"
|
|
1277
1280
|
});
|
|
1278
1281
|
} catch (err) {
|
|
1279
|
-
log$
|
|
1282
|
+
log$12.error({
|
|
1280
1283
|
err,
|
|
1281
1284
|
event: "mcp_auto_reload_failed"
|
|
1282
1285
|
}, "auto-reload after mcp.config.json change failed");
|
|
@@ -1684,7 +1687,7 @@ const HEARTBEAT_THROTTLE_MS = 6e4;
|
|
|
1684
1687
|
const TOOL_HEARTBEAT_INTERVAL_MS = 5e3;
|
|
1685
1688
|
const MAX_TOOL_HEARTBEATS = 1440 * 60 * 1e3 / TOOL_HEARTBEAT_INTERVAL_MS;
|
|
1686
1689
|
const DAEMON_URL = "http://localhost:38994";
|
|
1687
|
-
const log$
|
|
1690
|
+
const log$11 = logger.child({ module: "platform-ext" });
|
|
1688
1691
|
function sandboxClient() {
|
|
1689
1692
|
const apiUrl = apiBaseUrl();
|
|
1690
1693
|
if (!apiUrl) return null;
|
|
@@ -1704,7 +1707,7 @@ async function fetchHarnessFlags() {
|
|
|
1704
1707
|
try {
|
|
1705
1708
|
const res = await client["feature-flags"].$get();
|
|
1706
1709
|
if (!res.ok) {
|
|
1707
|
-
log$
|
|
1710
|
+
log$11.debug({
|
|
1708
1711
|
status: res.status,
|
|
1709
1712
|
event: "feature_flags_fetch_failed"
|
|
1710
1713
|
}, "feature-flags fetch failed");
|
|
@@ -1712,7 +1715,7 @@ async function fetchHarnessFlags() {
|
|
|
1712
1715
|
}
|
|
1713
1716
|
return { contextManagement: (await res.json()).contextManagement ?? null };
|
|
1714
1717
|
} catch (err) {
|
|
1715
|
-
log$
|
|
1718
|
+
log$11.debug({
|
|
1716
1719
|
err,
|
|
1717
1720
|
event: "feature_flags_fetch_error"
|
|
1718
1721
|
}, "feature-flags request errored");
|
|
@@ -1723,7 +1726,7 @@ function postHeartbeat({ messageId }) {
|
|
|
1723
1726
|
const client = sandboxClient();
|
|
1724
1727
|
if (!client) return;
|
|
1725
1728
|
client.heartbeat.$post({ json: { messageId } }).catch((err) => {
|
|
1726
|
-
log$
|
|
1729
|
+
log$11.debug({
|
|
1727
1730
|
err,
|
|
1728
1731
|
event: "heartbeat_failed"
|
|
1729
1732
|
}, "heartbeat failed");
|
|
@@ -1735,7 +1738,7 @@ async function resolveConversationFromApi(messageId) {
|
|
|
1735
1738
|
try {
|
|
1736
1739
|
const res = await client["message-conversation"].$get({ query: { messageId } });
|
|
1737
1740
|
if (!res.ok) {
|
|
1738
|
-
log$
|
|
1741
|
+
log$11.warn({
|
|
1739
1742
|
status: res.status,
|
|
1740
1743
|
messageId,
|
|
1741
1744
|
event: "resolve_conversation_failed"
|
|
@@ -1744,7 +1747,7 @@ async function resolveConversationFromApi(messageId) {
|
|
|
1744
1747
|
}
|
|
1745
1748
|
return (await res.json()).conversationId ?? null;
|
|
1746
1749
|
} catch (err) {
|
|
1747
|
-
log$
|
|
1750
|
+
log$11.warn({
|
|
1748
1751
|
err,
|
|
1749
1752
|
messageId,
|
|
1750
1753
|
event: "resolve_conversation_error"
|
|
@@ -1822,7 +1825,7 @@ function createToolHeartbeat({ messageId }) {
|
|
|
1822
1825
|
}
|
|
1823
1826
|
heartbeatCount++;
|
|
1824
1827
|
if (heartbeatCount > MAX_TOOL_HEARTBEATS) {
|
|
1825
|
-
log$
|
|
1828
|
+
log$11.warn({
|
|
1826
1829
|
heartbeatCount,
|
|
1827
1830
|
activeToolCalls: [...activeToolCalls]
|
|
1828
1831
|
}, "tool heartbeat max reached, stopping");
|
|
@@ -1853,7 +1856,7 @@ function postToDaemon(path, body) {
|
|
|
1853
1856
|
headers: { "content-type": "application/json" },
|
|
1854
1857
|
body: JSON.stringify(body)
|
|
1855
1858
|
}).catch((err) => {
|
|
1856
|
-
log$
|
|
1859
|
+
log$11.debug({
|
|
1857
1860
|
err,
|
|
1858
1861
|
path,
|
|
1859
1862
|
event: "daemon_post_failed"
|
|
@@ -1862,7 +1865,7 @@ function postToDaemon(path, body) {
|
|
|
1862
1865
|
}
|
|
1863
1866
|
function createPlatformExtensions({ sessionId, channelContext }) {
|
|
1864
1867
|
return (pi) => {
|
|
1865
|
-
log$
|
|
1868
|
+
log$11.info({
|
|
1866
1869
|
sessionId,
|
|
1867
1870
|
hasChannelContext: Boolean(channelContext)
|
|
1868
1871
|
}, "platform extension initialized");
|
|
@@ -1911,7 +1914,7 @@ function createPlatformExtensions({ sessionId, channelContext }) {
|
|
|
1911
1914
|
});
|
|
1912
1915
|
});
|
|
1913
1916
|
pi.on("agent_end", () => {
|
|
1914
|
-
log$
|
|
1917
|
+
log$11.info({ sessionId }, "session ending");
|
|
1915
1918
|
postToDaemon("/session/end", { sessionId });
|
|
1916
1919
|
});
|
|
1917
1920
|
};
|
|
@@ -1937,7 +1940,7 @@ function createPlatformExtensions({ sessionId, channelContext }) {
|
|
|
1937
1940
|
* alive and an indeterminate result (no api url / transient failure) leaves the
|
|
1938
1941
|
* last-known values untouched so a blip can't silently flip behavior.
|
|
1939
1942
|
*/
|
|
1940
|
-
const log$
|
|
1943
|
+
const log$10 = logger.child({ module: "feature-flags-poll" });
|
|
1941
1944
|
const FLAG_POLL_INTERVAL_MS = 6e4;
|
|
1942
1945
|
let contextManagement = null;
|
|
1943
1946
|
const subscribers = { contextManagement: /* @__PURE__ */ new Set() };
|
|
@@ -1972,7 +1975,7 @@ function apply(name, next) {
|
|
|
1972
1975
|
if (next !== prev) for (const cb of subscribers[name]) try {
|
|
1973
1976
|
cb(next);
|
|
1974
1977
|
} catch (err) {
|
|
1975
|
-
log$
|
|
1978
|
+
log$10.warn({
|
|
1976
1979
|
err,
|
|
1977
1980
|
flag: name
|
|
1978
1981
|
}, "flag subscriber threw");
|
|
@@ -1984,7 +1987,7 @@ async function pollOnce() {
|
|
|
1984
1987
|
if (!flags) return;
|
|
1985
1988
|
apply("contextManagement", flags.contextManagement ?? null);
|
|
1986
1989
|
} catch (err) {
|
|
1987
|
-
log$
|
|
1990
|
+
log$10.debug({ err }, "feature-flag poll threw");
|
|
1988
1991
|
}
|
|
1989
1992
|
}
|
|
1990
1993
|
/**
|
|
@@ -2091,7 +2094,7 @@ function transformContextMessages(messages, config, now) {
|
|
|
2091
2094
|
}
|
|
2092
2095
|
//#endregion
|
|
2093
2096
|
//#region src/extensions/context-management.ts
|
|
2094
|
-
const log$
|
|
2097
|
+
const log$9 = logger.child({ module: "context-management-extension" });
|
|
2095
2098
|
function isAnthropicMessagesPayload(payload) {
|
|
2096
2099
|
if (typeof payload !== "object" || payload === null) return false;
|
|
2097
2100
|
const candidate = payload;
|
|
@@ -2156,13 +2159,13 @@ function createContextManagementExtension() {
|
|
|
2156
2159
|
setContextManagementFlagOverride(getPolledFlag("contextManagement"));
|
|
2157
2160
|
onFlagChange("contextManagement", (enabled) => {
|
|
2158
2161
|
setContextManagementFlagOverride(enabled);
|
|
2159
|
-
log$
|
|
2162
|
+
log$9.info({
|
|
2160
2163
|
event: "context_management_flag_update",
|
|
2161
2164
|
enabled
|
|
2162
2165
|
}, "context-management flag updated from platform");
|
|
2163
2166
|
});
|
|
2164
2167
|
startFeatureFlagPoller();
|
|
2165
|
-
log$
|
|
2168
|
+
log$9.info({
|
|
2166
2169
|
event: "context_management_registered",
|
|
2167
2170
|
enabled: initial.enabled,
|
|
2168
2171
|
flagSource: hasFlagSource(),
|
|
@@ -2174,13 +2177,13 @@ function createContextManagementExtension() {
|
|
|
2174
2177
|
const { messages } = event;
|
|
2175
2178
|
try {
|
|
2176
2179
|
const result = transformContextIfEnabled(messages, getContextManagementConfig(), Date.now());
|
|
2177
|
-
if (result.stats && (result.stats.clearedResults > 0 || result.stats.trimmedResults > 0)) log$
|
|
2180
|
+
if (result.stats && (result.stats.clearedResults > 0 || result.stats.trimmedResults > 0)) log$9.info({
|
|
2178
2181
|
event: "context_management_applied",
|
|
2179
2182
|
...result.stats
|
|
2180
2183
|
}, "trimmed/cleared tool output before LLM call");
|
|
2181
2184
|
return { messages: result.messages };
|
|
2182
2185
|
} catch (err) {
|
|
2183
|
-
log$
|
|
2186
|
+
log$9.error({
|
|
2184
2187
|
err,
|
|
2185
2188
|
event: "context_management_transform_failed"
|
|
2186
2189
|
}, "context transform failed; passing messages through unchanged");
|
|
@@ -2192,7 +2195,7 @@ function createContextManagementExtension() {
|
|
|
2192
2195
|
}
|
|
2193
2196
|
//#endregion
|
|
2194
2197
|
//#region src/extensions/current-time.ts
|
|
2195
|
-
const log$
|
|
2198
|
+
const log$8 = logger.child({ module: "current-time-extension" });
|
|
2196
2199
|
const PI_DATE_LINE = /^Current date:.*$/m;
|
|
2197
2200
|
function formatCurrentTimeLine(now) {
|
|
2198
2201
|
return `Current date: ${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, "0")}-${String(now.getUTCDate()).padStart(2, "0")} (${new Intl.DateTimeFormat("en-US", {
|
|
@@ -2205,7 +2208,7 @@ const currentTimeExtension = (pi) => {
|
|
|
2205
2208
|
const line = formatCurrentTimeLine(/* @__PURE__ */ new Date());
|
|
2206
2209
|
const base = event.systemPrompt;
|
|
2207
2210
|
if (PI_DATE_LINE.test(base)) {
|
|
2208
|
-
log$
|
|
2211
|
+
log$8.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)");
|
|
2209
2212
|
return { systemPrompt: base.replace(PI_DATE_LINE, line) };
|
|
2210
2213
|
}
|
|
2211
2214
|
return { systemPrompt: `${base}\n${line}` };
|
|
@@ -2442,7 +2445,7 @@ function renderIndex(entries) {
|
|
|
2442
2445
|
}
|
|
2443
2446
|
//#endregion
|
|
2444
2447
|
//#region src/extensions/memory.ts
|
|
2445
|
-
const log$
|
|
2448
|
+
const log$7 = logger.child({ module: "memory-extension" });
|
|
2446
2449
|
/**
|
|
2447
2450
|
* The standing instructions for the memory system. Always injected (even with
|
|
2448
2451
|
* an empty `.memory/`) so the agent knows it can persist notes. `users/` is
|
|
@@ -2476,7 +2479,7 @@ const memoryExtension = (pi) => {
|
|
|
2476
2479
|
index
|
|
2477
2480
|
});
|
|
2478
2481
|
} catch (err) {
|
|
2479
|
-
log$
|
|
2482
|
+
log$7.warn({
|
|
2480
2483
|
err,
|
|
2481
2484
|
event: "memory_index_failed"
|
|
2482
2485
|
}, "memory index build failed; injecting instructions only");
|
|
@@ -2490,7 +2493,7 @@ const memoryExtension = (pi) => {
|
|
|
2490
2493
|
};
|
|
2491
2494
|
//#endregion
|
|
2492
2495
|
//#region src/extensions/platform-memory.ts
|
|
2493
|
-
const log$
|
|
2496
|
+
const log$6 = logger.child({ module: "platform-memory-extension" });
|
|
2494
2497
|
/**
|
|
2495
2498
|
* Resolve the human on this turn via the API, keyed by the message id.
|
|
2496
2499
|
* `/sandbox/channel-context` only returns a sender for a platform-known
|
|
@@ -2503,13 +2506,13 @@ const log$5 = logger.child({ module: "platform-memory-extension" });
|
|
|
2503
2506
|
async function resolveTurnUser(messageId) {
|
|
2504
2507
|
const client = sandboxClient();
|
|
2505
2508
|
if (!client) {
|
|
2506
|
-
log$
|
|
2509
|
+
log$6.debug({ event: "resolve_turn_user_no_api_url" }, "no API url in env; withholding user memory");
|
|
2507
2510
|
return null;
|
|
2508
2511
|
}
|
|
2509
2512
|
try {
|
|
2510
2513
|
const res = await client["channel-context"].$get({ query: { messageId } });
|
|
2511
2514
|
if (!res.ok) {
|
|
2512
|
-
log$
|
|
2515
|
+
log$6.warn({
|
|
2513
2516
|
event: "resolve_turn_user_failed",
|
|
2514
2517
|
status: res.status
|
|
2515
2518
|
}, "channel-context returned non-ok; withholding user memory");
|
|
@@ -2522,7 +2525,7 @@ async function resolveTurnUser(messageId) {
|
|
|
2522
2525
|
displayName: sender.displayName
|
|
2523
2526
|
};
|
|
2524
2527
|
} catch (err) {
|
|
2525
|
-
log$
|
|
2528
|
+
log$6.warn({
|
|
2526
2529
|
err,
|
|
2527
2530
|
event: "resolve_turn_user_failed"
|
|
2528
2531
|
}, "failed to resolve current user; withholding user memory");
|
|
@@ -2569,7 +2572,7 @@ function createPlatformMemoryExtension({ channelContext }) {
|
|
|
2569
2572
|
user
|
|
2570
2573
|
});
|
|
2571
2574
|
} catch (err) {
|
|
2572
|
-
log$
|
|
2575
|
+
log$6.warn({
|
|
2573
2576
|
err,
|
|
2574
2577
|
event: "user_memory_index_failed"
|
|
2575
2578
|
}, "user memory index build failed; skipping injection");
|
|
@@ -2584,7 +2587,7 @@ function createPlatformMemoryExtension({ channelContext }) {
|
|
|
2584
2587
|
}
|
|
2585
2588
|
//#endregion
|
|
2586
2589
|
//#region src/extensions/self-trace.ts
|
|
2587
|
-
const log$
|
|
2590
|
+
const log$5 = logger.child({ module: "self-trace-extension" });
|
|
2588
2591
|
/**
|
|
2589
2592
|
* Reports the agent's own execution as OpenTelemetry spans:
|
|
2590
2593
|
* agent.session → agent.run → agent.turn.N → tool.NAME, with token/cost
|
|
@@ -2609,7 +2612,7 @@ const selfTraceExtension = (pi) => {
|
|
|
2609
2612
|
sessionSpan = tracer.startSpan("agent.session", { attributes: { "agent.model": modelId } }, remoteCtx);
|
|
2610
2613
|
sessionCtx = trace.setSpan(remoteCtx, sessionSpan);
|
|
2611
2614
|
const sc = sessionSpan.spanContext();
|
|
2612
|
-
log$
|
|
2615
|
+
log$5.info({
|
|
2613
2616
|
event: "self_trace_session_start",
|
|
2614
2617
|
trace_id: sc.traceId,
|
|
2615
2618
|
span_id: sc.spanId,
|
|
@@ -2721,13 +2724,13 @@ const selfTraceExtension = (pi) => {
|
|
|
2721
2724
|
* Lives in the harness package — soul.md is content from the agent's
|
|
2722
2725
|
* own git repo, not from the platform — so its handling stays here.
|
|
2723
2726
|
*/
|
|
2724
|
-
const log$
|
|
2727
|
+
const log$4 = logger.child({ module: "soul-extension" });
|
|
2725
2728
|
async function readSoul(cwd) {
|
|
2726
2729
|
try {
|
|
2727
2730
|
return (await readFile(join(cwd, "soul.md"), "utf8")).trim() || null;
|
|
2728
2731
|
} catch (err) {
|
|
2729
2732
|
if (err?.code === "ENOENT") return null;
|
|
2730
|
-
log$
|
|
2733
|
+
log$4.warn({
|
|
2731
2734
|
err,
|
|
2732
2735
|
event: "soul_read_failed"
|
|
2733
2736
|
}, "soul.md read failed");
|
|
@@ -2759,7 +2762,7 @@ const soulExtension = (pi) => {
|
|
|
2759
2762
|
};
|
|
2760
2763
|
//#endregion
|
|
2761
2764
|
//#region src/extensions/subagent/index.ts
|
|
2762
|
-
const log$
|
|
2765
|
+
const log$3 = logger.child({ module: "subagent-ext" });
|
|
2763
2766
|
const MAX_TASKS = 8;
|
|
2764
2767
|
const TaskItem = Type.Object({
|
|
2765
2768
|
task: Type.String({ description: "The task to delegate to a subagent run." }),
|
|
@@ -2818,7 +2821,7 @@ function buildTool(messageId) {
|
|
|
2818
2821
|
tasks: spawnTasks
|
|
2819
2822
|
});
|
|
2820
2823
|
const { taskIds } = spawned;
|
|
2821
|
-
log$
|
|
2824
|
+
log$3.info({
|
|
2822
2825
|
event: "subagent_spawned",
|
|
2823
2826
|
count: taskIds.length
|
|
2824
2827
|
}, "subagent tasks queued");
|
|
@@ -2841,7 +2844,7 @@ function buildTool(messageId) {
|
|
|
2841
2844
|
};
|
|
2842
2845
|
} catch (err) {
|
|
2843
2846
|
const message = err instanceof Error ? err.message : String(err);
|
|
2844
|
-
log$
|
|
2847
|
+
log$3.warn({
|
|
2845
2848
|
err,
|
|
2846
2849
|
event: "subagent_spawn_failed"
|
|
2847
2850
|
}, "subagent spawn failed");
|
|
@@ -2872,7 +2875,7 @@ function createSubagentExtension({ channelContext }) {
|
|
|
2872
2875
|
if (registered) return;
|
|
2873
2876
|
registered = true;
|
|
2874
2877
|
pi.registerTool(buildTool(messageId));
|
|
2875
|
-
log$
|
|
2878
|
+
log$3.info({ event: "subagent_enabled" }, "subagent tool registered");
|
|
2876
2879
|
};
|
|
2877
2880
|
pi.on("session_start", () => {
|
|
2878
2881
|
registerOnce();
|
|
@@ -2880,6 +2883,218 @@ function createSubagentExtension({ channelContext }) {
|
|
|
2880
2883
|
};
|
|
2881
2884
|
}
|
|
2882
2885
|
//#endregion
|
|
2886
|
+
//#region src/extensions/resource-pressure-warning.ts
|
|
2887
|
+
/**
|
|
2888
|
+
* Mid-run resource-pressure warning to the agent.
|
|
2889
|
+
*
|
|
2890
|
+
* The sandbox already detects pressure — the boot scripts cap the
|
|
2891
|
+
* user-workload cgroup (memory.high/memory.max) and watchers log warn/crit
|
|
2892
|
+
* edges for memory and disk — but nothing told the *agent*, so a turn burned
|
|
2893
|
+
* straight to the OOM kill (or a full disk) and only learned about it from
|
|
2894
|
+
* the post-mortem notice. This extension closes that gap in-process: while a
|
|
2895
|
+
* turn is active it polls the agent cgroup and the root filesystem and, the
|
|
2896
|
+
* first time usage crosses a warn threshold, folds a system notification into
|
|
2897
|
+
* the open turn so the agent can checkpoint, shed work (constrain
|
|
2898
|
+
* parallelism, kill a background hog, clean scratch space), or request a
|
|
2899
|
+
* bigger tier BEFORE the kill.
|
|
2900
|
+
*
|
|
2901
|
+
* The notification is triggered by the two conditions that actually kill
|
|
2902
|
+
* work — memory near the cgroup hard cap, disk near full — and reports a
|
|
2903
|
+
* snapshot of all the relevant stats (memory, CPU utilization, disk) so the
|
|
2904
|
+
* agent can tell which resource is the problem and how much headroom the
|
|
2905
|
+
* others have.
|
|
2906
|
+
*
|
|
2907
|
+
* Edge-triggered, once per trigger per turn: the fired flags reset on
|
|
2908
|
+
* agent_start, so a turn that rides a threshold gets one warning per
|
|
2909
|
+
* resource, not a stream. Polling only runs while the agent is active — an
|
|
2910
|
+
* idle sandbox's resource usage is not the agent's problem and there is no
|
|
2911
|
+
* open turn to deliver into anyway.
|
|
2912
|
+
*
|
|
2913
|
+
* Best-effort throughout: any read failure (cgroup absent, controller not
|
|
2914
|
+
* delegated, non-cgroup-v2 host, df missing) reads as "no signal" for that
|
|
2915
|
+
* stat and the extension warns on what it can see — it must never break a
|
|
2916
|
+
* turn over an observability feature.
|
|
2917
|
+
*/
|
|
2918
|
+
const execFileAsync = promisify(execFile);
|
|
2919
|
+
const log$2 = logger.child({ module: "resource-pressure-warning" });
|
|
2920
|
+
const POLL_INTERVAL_MS = 1e4;
|
|
2921
|
+
function envOverride(name) {
|
|
2922
|
+
for (const prefix of ["SKYDIVE_", "ANYONE_"]) {
|
|
2923
|
+
const value = process.env[`${prefix}${name}`];
|
|
2924
|
+
if (value != null && value !== "") return value;
|
|
2925
|
+
}
|
|
2926
|
+
return null;
|
|
2927
|
+
}
|
|
2928
|
+
function cgroupDir() {
|
|
2929
|
+
return envOverride("AGENT_CGROUP") ?? "/sys/fs/cgroup/agent";
|
|
2930
|
+
}
|
|
2931
|
+
function diskRoot() {
|
|
2932
|
+
return envOverride("DISK_ROOT") ?? "/";
|
|
2933
|
+
}
|
|
2934
|
+
/**
|
|
2935
|
+
* Read a cgroup v2 scalar file. Returns a number, or null for "max"
|
|
2936
|
+
* (uncapped), an empty/absent file, or any read/parse error — an uncapped or
|
|
2937
|
+
* unreadable limit means there is nothing meaningful to warn against.
|
|
2938
|
+
*/
|
|
2939
|
+
async function readScalar(file) {
|
|
2940
|
+
try {
|
|
2941
|
+
const raw = (await readFile(`${cgroupDir()}/${file}`, "utf8")).trim();
|
|
2942
|
+
if (raw === "" || raw === "max") return null;
|
|
2943
|
+
const n = Number(raw);
|
|
2944
|
+
return Number.isFinite(n) ? n : null;
|
|
2945
|
+
} catch (_error) {
|
|
2946
|
+
return null;
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
/**
|
|
2950
|
+
* Read a cgroup v2 "flat keyed" file (one `key value` pair per line, e.g.
|
|
2951
|
+
* cpu.stat) and return the counter for `key`, or null when absent.
|
|
2952
|
+
*/
|
|
2953
|
+
async function readKeyedCounter(file, key) {
|
|
2954
|
+
try {
|
|
2955
|
+
const raw = await readFile(`${cgroupDir()}/${file}`, "utf8");
|
|
2956
|
+
for (const line of raw.split("\n")) {
|
|
2957
|
+
const [k, v] = line.trim().split(/\s+/);
|
|
2958
|
+
if (k === key) {
|
|
2959
|
+
const n = Number(v);
|
|
2960
|
+
return Number.isFinite(n) ? n : null;
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
return null;
|
|
2964
|
+
} catch (_error) {
|
|
2965
|
+
return null;
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
/**
|
|
2969
|
+
* Live memory usage as an integer percent of the hard cap, or null when
|
|
2970
|
+
* either side is unreadable/uncapped. Exported for tests.
|
|
2971
|
+
*/
|
|
2972
|
+
async function readMemUsePct() {
|
|
2973
|
+
const [current, max] = await Promise.all([readScalar("memory.current"), readScalar("memory.max")]);
|
|
2974
|
+
if (current === null || max === null || max <= 0) return null;
|
|
2975
|
+
return {
|
|
2976
|
+
pct: Math.floor(current / max * 100),
|
|
2977
|
+
currentBytes: current,
|
|
2978
|
+
maxBytes: max
|
|
2979
|
+
};
|
|
2980
|
+
}
|
|
2981
|
+
/**
|
|
2982
|
+
* Root filesystem used% (df -P Capacity column), or null on any failure.
|
|
2983
|
+
* Exported for tests.
|
|
2984
|
+
*/
|
|
2985
|
+
async function readDiskUsePct() {
|
|
2986
|
+
try {
|
|
2987
|
+
const { stdout } = await execFileAsync("df", ["-P", diskRoot()]);
|
|
2988
|
+
const dataRow = stdout.trim().split("\n")[1];
|
|
2989
|
+
if (dataRow == null) return null;
|
|
2990
|
+
const capacity = dataRow.trim().split(/\s+/)[4];
|
|
2991
|
+
if (capacity == null) return null;
|
|
2992
|
+
const pct = Number(capacity.replace("%", ""));
|
|
2993
|
+
return Number.isFinite(pct) ? pct : null;
|
|
2994
|
+
} catch (_error) {
|
|
2995
|
+
return null;
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
/**
|
|
2999
|
+
* CPU utilization sampler. cgroup v2 exposes cumulative CPU time
|
|
3000
|
+
* (cpu.stat usage_usec); utilization is the delta between two samples over
|
|
3001
|
+
* the wall time between them, normalized by core count. The first call after
|
|
3002
|
+
* construction has no previous sample and returns null.
|
|
3003
|
+
*/
|
|
3004
|
+
function createCpuSampler() {
|
|
3005
|
+
let prevUsageUsec = null;
|
|
3006
|
+
let prevAtMs = null;
|
|
3007
|
+
return async () => {
|
|
3008
|
+
const usage = await readKeyedCounter("cpu.stat", "usage_usec");
|
|
3009
|
+
const now = Date.now();
|
|
3010
|
+
const prev = prevUsageUsec;
|
|
3011
|
+
const prevAt = prevAtMs;
|
|
3012
|
+
prevUsageUsec = usage;
|
|
3013
|
+
prevAtMs = now;
|
|
3014
|
+
if (usage === null || prev === null || prevAt === null) return null;
|
|
3015
|
+
const wallUsec = (now - prevAt) * 1e3;
|
|
3016
|
+
if (wallUsec <= 0) return null;
|
|
3017
|
+
const cores = availableParallelism();
|
|
3018
|
+
const pct = Math.round((usage - prev) / (wallUsec * cores) * 100);
|
|
3019
|
+
return Math.max(0, Math.min(100, pct));
|
|
3020
|
+
};
|
|
3021
|
+
}
|
|
3022
|
+
function fmtMb(bytes) {
|
|
3023
|
+
return Math.round(bytes / 1024 / 1024);
|
|
3024
|
+
}
|
|
3025
|
+
/** The model-facing warning text. Exported for tests. */
|
|
3026
|
+
function resourcePressureWarningText(trigger, { mem, cpuPct, diskPct }) {
|
|
3027
|
+
const stats = [];
|
|
3028
|
+
if (mem) stats.push(`memory ${mem.pct}% of cap (${fmtMb(mem.currentBytes)}/${fmtMb(mem.maxBytes)} MB)`);
|
|
3029
|
+
if (cpuPct !== null) stats.push(`CPU ${cpuPct}%`);
|
|
3030
|
+
if (diskPct !== null) stats.push(`disk ${diskPct}% full`);
|
|
3031
|
+
const lead = trigger === "memory" ? `Your sandbox is at ${mem?.pct}% of its memory cap. If usage keeps climbing, the kernel will kill the offending process and this turn may die with it.` : `Your sandbox's disk is ${diskPct}% full. If it fills completely, writes will start failing and this turn may die with them.`;
|
|
3032
|
+
const remedy = trigger === "memory" ? "checkpoint in-flight work (commit and push), then reduce the footprint — constrain parallelism, run heavy steps sequentially, or kill background processes you no longer need." : "checkpoint in-flight work (commit and push), then free space — clean build artifacts, caches, and scratch files you no longer need.";
|
|
3033
|
+
return `<system_notification>${lead} Current usage: ${stats.join(", ")}. Act now: ${remedy} If the workload genuinely needs more resources, request a bigger sandbox with \`platform compute request\`. This is an automated resource warning, not a message from the user; continue the task, adjusted.</system_notification>`;
|
|
3034
|
+
}
|
|
3035
|
+
const resourcePressureWarningExtension = (pi) => {
|
|
3036
|
+
let agentActive = false;
|
|
3037
|
+
let warnedMemThisTurn = false;
|
|
3038
|
+
let warnedDiskThisTurn = false;
|
|
3039
|
+
let timer = null;
|
|
3040
|
+
const sampleCpu = createCpuSampler();
|
|
3041
|
+
async function checkOnce() {
|
|
3042
|
+
if (!agentActive || warnedMemThisTurn && warnedDiskThisTurn) return;
|
|
3043
|
+
const [mem, cpuPct, diskPct] = await Promise.all([
|
|
3044
|
+
readMemUsePct(),
|
|
3045
|
+
sampleCpu(),
|
|
3046
|
+
readDiskUsePct()
|
|
3047
|
+
]);
|
|
3048
|
+
let trigger = null;
|
|
3049
|
+
if (!warnedMemThisTurn && mem !== null && mem.pct >= 80) {
|
|
3050
|
+
trigger = "memory";
|
|
3051
|
+
warnedMemThisTurn = true;
|
|
3052
|
+
} else if (!warnedDiskThisTurn && diskPct !== null && diskPct >= 80) {
|
|
3053
|
+
trigger = "disk";
|
|
3054
|
+
warnedDiskThisTurn = true;
|
|
3055
|
+
}
|
|
3056
|
+
if (trigger === null) return;
|
|
3057
|
+
log$2.warn({
|
|
3058
|
+
trigger,
|
|
3059
|
+
mem,
|
|
3060
|
+
cpuPct,
|
|
3061
|
+
diskPct
|
|
3062
|
+
}, "resource pressure warning delivered to agent");
|
|
3063
|
+
await pi.sendMessage({
|
|
3064
|
+
customType: "anyone-resource-pressure-warning",
|
|
3065
|
+
content: resourcePressureWarningText(trigger, {
|
|
3066
|
+
mem,
|
|
3067
|
+
cpuPct,
|
|
3068
|
+
diskPct
|
|
3069
|
+
}),
|
|
3070
|
+
display: false
|
|
3071
|
+
}, {
|
|
3072
|
+
triggerTurn: true,
|
|
3073
|
+
deliverAs: "followUp"
|
|
3074
|
+
});
|
|
3075
|
+
}
|
|
3076
|
+
pi.on("agent_start", async () => {
|
|
3077
|
+
agentActive = true;
|
|
3078
|
+
warnedMemThisTurn = false;
|
|
3079
|
+
warnedDiskThisTurn = false;
|
|
3080
|
+
if (!timer) {
|
|
3081
|
+
timer = setInterval(() => {
|
|
3082
|
+
checkOnce().catch((err) => {
|
|
3083
|
+
log$2.error({ err }, "resource pressure check failed");
|
|
3084
|
+
});
|
|
3085
|
+
}, POLL_INTERVAL_MS);
|
|
3086
|
+
timer.unref?.();
|
|
3087
|
+
}
|
|
3088
|
+
});
|
|
3089
|
+
pi.on("agent_end", async () => {
|
|
3090
|
+
agentActive = false;
|
|
3091
|
+
if (timer) {
|
|
3092
|
+
clearInterval(timer);
|
|
3093
|
+
timer = null;
|
|
3094
|
+
}
|
|
3095
|
+
});
|
|
3096
|
+
};
|
|
3097
|
+
//#endregion
|
|
2883
3098
|
//#region src/extensions/tool-call-env.ts
|
|
2884
3099
|
const TOOL_CALL_ID_VAR = "TOOL_CALL_ID";
|
|
2885
3100
|
function shellQuoteValue(value) {
|
|
@@ -3653,7 +3868,8 @@ function platformExtensions({ sessionId, channelContext }) {
|
|
|
3653
3868
|
selfTraceExtension,
|
|
3654
3869
|
createBackgroundTasksExtension({ channelContext }),
|
|
3655
3870
|
createSubagentExtension({ channelContext }),
|
|
3656
|
-
createContextManagementExtension()
|
|
3871
|
+
createContextManagementExtension(),
|
|
3872
|
+
resourcePressureWarningExtension
|
|
3657
3873
|
];
|
|
3658
3874
|
}
|
|
3659
3875
|
//#endregion
|