@skydiveai/pi-extensions 0.1.0-beta.75 → 0.1.0-beta.751
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 +55 -61
- package/package.json +2 -7
package/dist/index.mjs
CHANGED
|
@@ -21,6 +21,7 @@ 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 { quote } from "shell-quote";
|
|
24
25
|
import { createWriteStream } from "node:fs";
|
|
25
26
|
import { finished } from "node:stream/promises";
|
|
26
27
|
import { createLocalBashOperations } from "@earendil-works/pi-coding-agent";
|
|
@@ -1690,8 +1691,8 @@ function sandboxClient() {
|
|
|
1690
1691
|
return hc(`${apiUrl}/api/v1/sandbox`);
|
|
1691
1692
|
}
|
|
1692
1693
|
/**
|
|
1693
|
-
* Fetch every harness feature flag in one GET (`{ contextManagement,
|
|
1694
|
-
*
|
|
1694
|
+
* Fetch every harness feature flag in one GET (`{ contextManagement, ... }`
|
|
1695
|
+
* — see apps/anyone/api/src/routes/sandbox-feature-flags.ts). Returns
|
|
1695
1696
|
* null when indeterminate (no api url, or the request failed) so the shared
|
|
1696
1697
|
* poller keeps the last-known values rather than flipping on a transient error.
|
|
1697
1698
|
* This is the single fetch behind `feature-flags-poll.ts`; extensions read the
|
|
@@ -1709,11 +1710,7 @@ async function fetchHarnessFlags() {
|
|
|
1709
1710
|
}, "feature-flags fetch failed");
|
|
1710
1711
|
return null;
|
|
1711
1712
|
}
|
|
1712
|
-
|
|
1713
|
-
return {
|
|
1714
|
-
contextManagement: body.contextManagement ?? null,
|
|
1715
|
-
subagent: body.subagent ?? null
|
|
1716
|
-
};
|
|
1713
|
+
return { contextManagement: (await res.json()).contextManagement ?? null };
|
|
1717
1714
|
} catch (err) {
|
|
1718
1715
|
log$10.debug({
|
|
1719
1716
|
err,
|
|
@@ -1772,7 +1769,11 @@ async function postSubagentSpawn({ messageId, tasks }) {
|
|
|
1772
1769
|
tasks
|
|
1773
1770
|
} });
|
|
1774
1771
|
if (!res.ok) throw new Error(`subagent-spawn POST failed: ${res.status}`);
|
|
1775
|
-
|
|
1772
|
+
const body = await res.json();
|
|
1773
|
+
return {
|
|
1774
|
+
taskIds: body.taskIds,
|
|
1775
|
+
tasks: body.tasks ?? []
|
|
1776
|
+
};
|
|
1776
1777
|
}
|
|
1777
1778
|
function createHeartbeatThrottle({ messageId }) {
|
|
1778
1779
|
let lastAt = 0;
|
|
@@ -1921,20 +1922,16 @@ function createPlatformExtensions({ sessionId, channelContext }) {
|
|
|
1921
1922
|
* Shared harness feature-flag poll.
|
|
1922
1923
|
*
|
|
1923
1924
|
* The api exposes one `/feature-flags` GET that returns every harness flag in a
|
|
1924
|
-
* single response (`{ contextManagement,
|
|
1925
|
+
* single response (`{ contextManagement, commandFlags }` — see
|
|
1925
1926
|
* apps/anyone/api/src/routes/sandbox-feature-flags.ts). Rather than each
|
|
1926
1927
|
* extension issuing its own GET — and, worse, a *blocking* GET on the
|
|
1927
1928
|
* pre-first-token `session_start` path — a single background poller fetches
|
|
1928
1929
|
* that response once per interval and fans the values out to every subscriber.
|
|
1929
1930
|
*
|
|
1930
|
-
* Why one poller:
|
|
1931
|
-
*
|
|
1932
|
-
*
|
|
1933
|
-
*
|
|
1934
|
-
* every session, flag on or off. Reading the last-polled value instead keeps
|
|
1935
|
-
* the hot path allocation-only. A cold cache reads as `null` (fail-open to
|
|
1936
|
-
* unregistered); a newly-flipped flag takes effect on the next poll, matching
|
|
1937
|
-
* how context-management already treats its flag.
|
|
1931
|
+
* Why one poller: context-management consumes the `contextManagement` flag
|
|
1932
|
+
* without a blocking GET on the pre-first-token `session_start` path. Reading
|
|
1933
|
+
* the last-polled value keeps the hot path allocation-only; a cold cache reads
|
|
1934
|
+
* as `null` and a newly-flipped flag takes effect on the next poll.
|
|
1938
1935
|
*
|
|
1939
1936
|
* The poll is fire-and-forget and self-unref'd — it never keeps the process
|
|
1940
1937
|
* alive and an indeterminate result (no api url / transient failure) leaves the
|
|
@@ -1943,15 +1940,11 @@ function createPlatformExtensions({ sessionId, channelContext }) {
|
|
|
1943
1940
|
const log$9 = logger.child({ module: "feature-flags-poll" });
|
|
1944
1941
|
const FLAG_POLL_INTERVAL_MS = 6e4;
|
|
1945
1942
|
let contextManagement = null;
|
|
1946
|
-
|
|
1947
|
-
const subscribers = {
|
|
1948
|
-
contextManagement: /* @__PURE__ */ new Set(),
|
|
1949
|
-
subagent: /* @__PURE__ */ new Set()
|
|
1950
|
-
};
|
|
1943
|
+
const subscribers = { contextManagement: /* @__PURE__ */ new Set() };
|
|
1951
1944
|
let pollerStarted = false;
|
|
1952
1945
|
let firstPollSettled = false;
|
|
1953
1946
|
let resolveFirstPoll = null;
|
|
1954
|
-
|
|
1947
|
+
new Promise((resolve) => {
|
|
1955
1948
|
resolveFirstPoll = resolve;
|
|
1956
1949
|
});
|
|
1957
1950
|
function markFirstPollSettled() {
|
|
@@ -1960,20 +1953,8 @@ function markFirstPollSettled() {
|
|
|
1960
1953
|
resolveFirstPoll?.();
|
|
1961
1954
|
}
|
|
1962
1955
|
/** Last-polled value of a flag, or `null` if not yet resolved. */
|
|
1963
|
-
function getPolledFlag(
|
|
1964
|
-
return
|
|
1965
|
-
}
|
|
1966
|
-
/**
|
|
1967
|
-
* Await the first poll already kicked by `startFeatureFlagPoller` (never a new
|
|
1968
|
-
* GET). Resolves when that poll settles, immediately if it already has, or
|
|
1969
|
-
* immediately when there's no flag source to poll. Callers on the hot path
|
|
1970
|
-
* should race this against their own short timeout so a slow/failed flag
|
|
1971
|
-
* service cannot delay first-token; a timeout just means the caller reads the
|
|
1972
|
-
* still-cold cache and falls back to its default, exactly as before.
|
|
1973
|
-
*/
|
|
1974
|
-
function awaitFirstFlagPoll() {
|
|
1975
|
-
if (firstPollSettled || !hasFlagSource()) return Promise.resolve();
|
|
1976
|
-
return firstPollPromise;
|
|
1956
|
+
function getPolledFlag(_name) {
|
|
1957
|
+
return contextManagement;
|
|
1977
1958
|
}
|
|
1978
1959
|
/**
|
|
1979
1960
|
* Subscribe to changes of a flag. The callback fires only on a *transition*
|
|
@@ -1986,9 +1967,8 @@ function onFlagChange(name, cb) {
|
|
|
1986
1967
|
}
|
|
1987
1968
|
function apply(name, next) {
|
|
1988
1969
|
if (next === null) return;
|
|
1989
|
-
const prev =
|
|
1990
|
-
|
|
1991
|
-
else subagent = next;
|
|
1970
|
+
const prev = contextManagement;
|
|
1971
|
+
contextManagement = next;
|
|
1992
1972
|
if (next !== prev) for (const cb of subscribers[name]) try {
|
|
1993
1973
|
cb(next);
|
|
1994
1974
|
} catch (err) {
|
|
@@ -2003,7 +1983,6 @@ async function pollOnce() {
|
|
|
2003
1983
|
const flags = await fetchHarnessFlags();
|
|
2004
1984
|
if (!flags) return;
|
|
2005
1985
|
apply("contextManagement", flags.contextManagement ?? null);
|
|
2006
|
-
apply("subagent", flags.subagent ?? null);
|
|
2007
1986
|
} catch (err) {
|
|
2008
1987
|
log$9.debug({ err }, "feature-flag poll threw");
|
|
2009
1988
|
}
|
|
@@ -2762,7 +2741,7 @@ function soulSection(cwd, soul) {
|
|
|
2762
2741
|
|
|
2763
2742
|
**\`soul.md\` is where behavior lives.** Any standing instruction about how you should act — a rule a user wants you to follow going forward, a tone or format preference, a workflow convention, a "from now on, always/never …" — belongs here, not in \`.memory/\`. Memory records *what happened* (facts, events, findings); soul defines *how you behave*. When a user gives you a durable behavioral rule, write it to \`soul.md\`. If you find behavioral rules that ended up in \`.memory/\`, treat that as misfiled and move them here.
|
|
2764
2743
|
|
|
2765
|
-
Keep it current. When you gain a durable new capability — a tool you build, a skill or integration you set up, a service you connect — or a user hands you a lasting behavioral rule, record it in \`soul.md\` so a future conversation knows it's part of you rather than rediscovering it from scratch.
|
|
2744
|
+
Keep it current. When you gain a durable new capability — a tool you build, a skill or integration you set up, a service you connect, a secret or auth credential you wire in — or a user hands you a lasting behavioral rule, record it in \`soul.md\` so a future conversation knows it's part of you rather than rediscovering it from scratch. Do this the moment you gain the capability, and for a credential that means the moment it verifies with a real call, not after a human points out that you forgot. Connecting a capability is itself a durable change worth recording, not merely a step toward the task in front of you. Edit \`soul.md\` (then \`git add soul.md && git commit && git push\`) to redefine yourself; picked up on the next message.
|
|
2766
2745
|
|
|
2767
2746
|
${soul ? soul : "_(empty — write to `soul.md` to define your persona)_"}`;
|
|
2768
2747
|
}
|
|
@@ -2781,7 +2760,6 @@ const soulExtension = (pi) => {
|
|
|
2781
2760
|
//#endregion
|
|
2782
2761
|
//#region src/extensions/subagent/index.ts
|
|
2783
2762
|
const log$2 = logger.child({ module: "subagent-ext" });
|
|
2784
|
-
const COLD_START_FLAG_WAIT_MS = 750;
|
|
2785
2763
|
const MAX_TASKS = 8;
|
|
2786
2764
|
const TaskItem = Type.Object({
|
|
2787
2765
|
task: Type.String({ description: "The task to delegate to a subagent run." }),
|
|
@@ -2790,8 +2768,14 @@ const TaskItem = Type.Object({
|
|
|
2790
2768
|
maxLength: 120
|
|
2791
2769
|
}),
|
|
2792
2770
|
persona: Type.Optional(Type.String({ description: "Optional extra system prompt / role for this task, applied ON TOP of the child run's own default persona (your full identity and soul are still there underneath). Omit to run with just your default persona." })),
|
|
2793
|
-
model: Type.Optional(Type.String({ description: "Optional model id to run this subagent on
|
|
2771
|
+
model: Type.Optional(Type.String({ description: "Optional model id to run this subagent on. PREFER A LOWER-COST, FASTER MODEL when the task is well-scoped and does not need your full reasoning depth — most delegated subtasks (searching, summarizing, mechanical edits, gathering or reformatting data, running a check) run just as well on a lighter model and cost far less. Reserve a top-tier model for subtasks that genuinely need deep reasoning or careful judgment. Must be a real catalogued model id. Omit to inherit your own model. If you are locked to a Google-compliant model, only compliant models are accepted." })),
|
|
2772
|
+
timeoutMinutes: Type.Optional(Type.Integer({
|
|
2773
|
+
description: "Optional wall-clock timeout for this subagent, in minutes. If the run is still going after this long it is ended and you are rewoken with a timeout result, so a hung subagent can never strand you. Omit for the default (30 minutes). Raise it for genuinely long work (a big migration, a large audit); lower it for a quick lookup. Range 1-360.",
|
|
2774
|
+
minimum: 1,
|
|
2775
|
+
maximum: 360
|
|
2776
|
+
}))
|
|
2794
2777
|
});
|
|
2778
|
+
const DEFAULT_SUBAGENT_TIMEOUT_MS = 30 * 6e4;
|
|
2795
2779
|
const SubagentParams = Type.Object({ tasks: Type.Array(TaskItem, {
|
|
2796
2780
|
description: "One or more tasks to delegate. Each spawns an isolated subagent run linked to this conversation; they run in parallel and each rewakes you with its result when it finishes.",
|
|
2797
2781
|
minItems: 1,
|
|
@@ -2806,7 +2790,8 @@ function buildTool(messageId) {
|
|
|
2806
2790
|
"Delegate one or more tasks to subagent runs — fresh isolated copies of yourself, each with its own context window, linked to this conversation.",
|
|
2807
2791
|
"Use it to parallelize independent work, to keep a large or noisy subtask out of your own context, or to run a task under a specialized persona.",
|
|
2808
2792
|
"Fire-and-forget: this returns immediately after queueing. It does NOT wait for results. Each subagent runs on its own and, when it finishes, sends you its result on this thread — so queue the work, then keep going or end your turn. To chain, re-delegate after a result lands.",
|
|
2809
|
-
"Pass tasks: [{ task, title, persona?, model? }]. title is a short 3-6 word name for the task — it is shown to the person in the chat as that subagent's row, so name the work rather than restating the prompt. persona is an optional extra system prompt layered ON TOP of your default persona for that task (it adds to, it does not replace, your identity); omit it to run with just your default persona. model is an optional model id for that task; omit it to
|
|
2793
|
+
"Pass tasks: [{ task, title, persona?, model? }]. title is a short 3-6 word name for the task — it is shown to the person in the chat as that subagent's row, so name the work rather than restating the prompt. persona is an optional extra system prompt layered ON TOP of your default persona for that task (it adds to, it does not replace, your identity); omit it to run with just your default persona. model is an optional model id for that task — prefer a lower-cost, faster model for well-scoped subtasks that don't need deep reasoning, and reserve a top-tier model for the ones that do; omit it to inherit your own model.",
|
|
2794
|
+
"Peering: each queued task comes back with its own conversation id. A subagent is a real linked conversation, so to see what one is doing RIGHT NOW while it runs — its reasoning, the tools it has called and their results, its progress — read that conversation with `platform conversations show <conversationId>` (you are already authorized; it is your own delegated run). Check in that way instead of waiting blind for the final result. The read reflects the child's persisted state, which lags a few seconds behind live (tool results land as they complete; in-progress reasoning can be up to ~5s stale), so peek between checkpoints rather than polling in a tight loop."
|
|
2810
2795
|
].join(" "),
|
|
2811
2796
|
promptSnippet: "subagent — delegate tasks to isolated subagent runs; each rewakes you with its result when done",
|
|
2812
2797
|
parameters: SubagentParams,
|
|
@@ -2824,24 +2809,35 @@ function buildTool(messageId) {
|
|
|
2824
2809
|
task: t.task,
|
|
2825
2810
|
title: t.title ?? null,
|
|
2826
2811
|
persona: t.persona ?? null,
|
|
2827
|
-
model: t.model ?? null
|
|
2812
|
+
model: t.model ?? null,
|
|
2813
|
+
timeoutMs: t.timeoutMinutes != null ? t.timeoutMinutes * 6e4 : DEFAULT_SUBAGENT_TIMEOUT_MS
|
|
2828
2814
|
}));
|
|
2829
2815
|
try {
|
|
2830
|
-
const
|
|
2816
|
+
const spawned = await postSubagentSpawn({
|
|
2831
2817
|
messageId,
|
|
2832
2818
|
tasks: spawnTasks
|
|
2833
2819
|
});
|
|
2820
|
+
const { taskIds } = spawned;
|
|
2834
2821
|
log$2.info({
|
|
2835
2822
|
event: "subagent_spawned",
|
|
2836
2823
|
count: taskIds.length
|
|
2837
2824
|
}, "subagent tasks queued");
|
|
2838
|
-
const
|
|
2825
|
+
const convByTask = new Map(spawned.tasks.map((t) => [t.taskId, t.conversationId]));
|
|
2826
|
+
const lines = taskIds.map((id, i) => {
|
|
2827
|
+
const label = spawnTasks[i]?.title ?? spawnTasks[i]?.task ?? "";
|
|
2828
|
+
const conv = convByTask.get(id);
|
|
2829
|
+
return `- ${id}: ${label}${conv ? ` — peek: conversations show ${conv}` : ""}`;
|
|
2830
|
+
}).join("\n");
|
|
2831
|
+
const peerHint = spawned.tasks.length ? "\nTo see what a subagent is doing while it runs, read its conversation with `platform conversations show <conversationId>` (shown per task above)." : "";
|
|
2839
2832
|
return {
|
|
2840
2833
|
content: [{
|
|
2841
2834
|
type: "text",
|
|
2842
|
-
text: `Queued ${taskIds.length} subagent ${taskIds.length === 1 ? "run" : "runs"}. Each runs on its own and will send you its result on this thread when it finishes — keep working or end your turn meanwhile.\n${lines}`
|
|
2835
|
+
text: `Queued ${taskIds.length} subagent ${taskIds.length === 1 ? "run" : "runs"}. Each runs on its own and will send you its result on this thread when it finishes — keep working or end your turn meanwhile.\n${lines}${peerHint}`
|
|
2843
2836
|
}],
|
|
2844
|
-
details: {
|
|
2837
|
+
details: {
|
|
2838
|
+
taskIds,
|
|
2839
|
+
tasks: spawned.tasks
|
|
2840
|
+
}
|
|
2845
2841
|
};
|
|
2846
2842
|
} catch (err) {
|
|
2847
2843
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -2862,16 +2858,15 @@ function buildTool(messageId) {
|
|
|
2862
2858
|
};
|
|
2863
2859
|
}
|
|
2864
2860
|
/**
|
|
2865
|
-
* Gated on `harness-subagent-enabled`, read from the shared feature-flag poll.
|
|
2866
2861
|
* The factory takes the session's channel context to resolve the originating
|
|
2867
2862
|
* messageId — the api links each spawned run to the conversation that message
|
|
2868
2863
|
* belongs to and rewakes it on completion (nothing about the parent is piped
|
|
2869
|
-
* from the sandbox beyond that id).
|
|
2864
|
+
* from the sandbox beyond that id). The tool is registered unconditionally at
|
|
2865
|
+
* session_start.
|
|
2870
2866
|
*/
|
|
2871
2867
|
function createSubagentExtension({ channelContext }) {
|
|
2872
2868
|
return (pi) => {
|
|
2873
2869
|
const messageId = extractMessageId(channelContext);
|
|
2874
|
-
startFeatureFlagPoller();
|
|
2875
2870
|
let registered = false;
|
|
2876
2871
|
const registerOnce = () => {
|
|
2877
2872
|
if (registered) return;
|
|
@@ -2879,22 +2874,21 @@ function createSubagentExtension({ channelContext }) {
|
|
|
2879
2874
|
pi.registerTool(buildTool(messageId));
|
|
2880
2875
|
log$2.info({ event: "subagent_enabled" }, "subagent tool registered");
|
|
2881
2876
|
};
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
});
|
|
2885
|
-
pi.on("session_start", async () => {
|
|
2886
|
-
if (getPolledFlag("subagent") === null) await Promise.race([awaitFirstFlagPoll(), new Promise((resolve) => setTimeout(resolve, COLD_START_FLAG_WAIT_MS).unref?.())]);
|
|
2887
|
-
if (getPolledFlag("subagent") === true) registerOnce();
|
|
2877
|
+
pi.on("session_start", () => {
|
|
2878
|
+
registerOnce();
|
|
2888
2879
|
});
|
|
2889
2880
|
};
|
|
2890
2881
|
}
|
|
2891
2882
|
//#endregion
|
|
2892
2883
|
//#region src/extensions/tool-call-env.ts
|
|
2893
2884
|
const TOOL_CALL_ID_VAR = "TOOL_CALL_ID";
|
|
2885
|
+
function shellQuoteValue(value) {
|
|
2886
|
+
return quote([value]);
|
|
2887
|
+
}
|
|
2894
2888
|
function withToolCallId({ command, toolCallId }) {
|
|
2895
|
-
return `export ${TOOL_CALL_ID_VAR}=${toolCallId}; ${command}`;
|
|
2889
|
+
return `export ${TOOL_CALL_ID_VAR}=${shellQuoteValue(toolCallId)}; ${command}`;
|
|
2896
2890
|
}
|
|
2897
|
-
const PLATFORM_EXPORT = new RegExp(`^\\s*export\\s+(?:${TOOL_CALL_ID_VAR}|ANYONE_\\w+|SKYDIVE_\\w+)=(?:"(?:\\\\.|[^"])*"|'[^']*'|[^;\\s]*)\\s*;\\s*`);
|
|
2891
|
+
const PLATFORM_EXPORT = new RegExp(`^\\s*export\\s+(?:${TOOL_CALL_ID_VAR}|ANYONE_\\w+|SKYDIVE_\\w+)=(?:"(?:\\\\.|[^"])*"|'(?:'\\\\''|[^'])*'|[^;\\s]*)\\s*;\\s*`);
|
|
2898
2892
|
function stripPlatformExportsForDisplay(command) {
|
|
2899
2893
|
let c = command;
|
|
2900
2894
|
let m;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skydiveai/pi-extensions",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.751",
|
|
4
4
|
"homepage": "https://skydive.com",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Create, Inc.",
|
|
@@ -17,12 +17,6 @@
|
|
|
17
17
|
},
|
|
18
18
|
"publishConfig": {
|
|
19
19
|
"access": "public",
|
|
20
|
-
"exports": {
|
|
21
|
-
".": {
|
|
22
|
-
"types": "./dist/index.d.mts",
|
|
23
|
-
"default": "./dist/index.mjs"
|
|
24
|
-
}
|
|
25
|
-
},
|
|
26
20
|
"registry": "https://registry.npmjs.org"
|
|
27
21
|
},
|
|
28
22
|
"scripts": {
|
|
@@ -45,6 +39,7 @@
|
|
|
45
39
|
"@skydiveai/pi-server": "^0.1.0",
|
|
46
40
|
"hono": "^4.6.14",
|
|
47
41
|
"pino": "^9.6.0",
|
|
42
|
+
"shell-quote": "^1.8.4",
|
|
48
43
|
"typebox": "^1.1.34",
|
|
49
44
|
"yaml": "^2.8.3",
|
|
50
45
|
"zod": "^3.25.0"
|