@skydiveai/pi-extensions 0.1.0-beta.39 → 0.1.0-beta.390
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 +57 -45
- package/package.json +2 -1
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,
|
|
@@ -1921,20 +1918,16 @@ function createPlatformExtensions({ sessionId, channelContext }) {
|
|
|
1921
1918
|
* Shared harness feature-flag poll.
|
|
1922
1919
|
*
|
|
1923
1920
|
* The api exposes one `/feature-flags` GET that returns every harness flag in a
|
|
1924
|
-
* single response (`{ contextManagement,
|
|
1921
|
+
* single response (`{ contextManagement, commandFlags }` — see
|
|
1925
1922
|
* apps/anyone/api/src/routes/sandbox-feature-flags.ts). Rather than each
|
|
1926
1923
|
* extension issuing its own GET — and, worse, a *blocking* GET on the
|
|
1927
1924
|
* pre-first-token `session_start` path — a single background poller fetches
|
|
1928
1925
|
* that response once per interval and fans the values out to every subscriber.
|
|
1929
1926
|
*
|
|
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.
|
|
1927
|
+
* Why one poller: context-management consumes the `contextManagement` flag
|
|
1928
|
+
* without a blocking GET on the pre-first-token `session_start` path. Reading
|
|
1929
|
+
* the last-polled value keeps the hot path allocation-only; a cold cache reads
|
|
1930
|
+
* as `null` and a newly-flipped flag takes effect on the next poll.
|
|
1938
1931
|
*
|
|
1939
1932
|
* The poll is fire-and-forget and self-unref'd — it never keeps the process
|
|
1940
1933
|
* alive and an indeterminate result (no api url / transient failure) leaves the
|
|
@@ -1943,15 +1936,21 @@ function createPlatformExtensions({ sessionId, channelContext }) {
|
|
|
1943
1936
|
const log$9 = logger.child({ module: "feature-flags-poll" });
|
|
1944
1937
|
const FLAG_POLL_INTERVAL_MS = 6e4;
|
|
1945
1938
|
let contextManagement = null;
|
|
1946
|
-
|
|
1947
|
-
const subscribers = {
|
|
1948
|
-
contextManagement: /* @__PURE__ */ new Set(),
|
|
1949
|
-
subagent: /* @__PURE__ */ new Set()
|
|
1950
|
-
};
|
|
1939
|
+
const subscribers = { contextManagement: /* @__PURE__ */ new Set() };
|
|
1951
1940
|
let pollerStarted = false;
|
|
1941
|
+
let firstPollSettled = false;
|
|
1942
|
+
let resolveFirstPoll = null;
|
|
1943
|
+
new Promise((resolve) => {
|
|
1944
|
+
resolveFirstPoll = resolve;
|
|
1945
|
+
});
|
|
1946
|
+
function markFirstPollSettled() {
|
|
1947
|
+
if (firstPollSettled) return;
|
|
1948
|
+
firstPollSettled = true;
|
|
1949
|
+
resolveFirstPoll?.();
|
|
1950
|
+
}
|
|
1952
1951
|
/** Last-polled value of a flag, or `null` if not yet resolved. */
|
|
1953
|
-
function getPolledFlag(
|
|
1954
|
-
return
|
|
1952
|
+
function getPolledFlag(_name) {
|
|
1953
|
+
return contextManagement;
|
|
1955
1954
|
}
|
|
1956
1955
|
/**
|
|
1957
1956
|
* Subscribe to changes of a flag. The callback fires only on a *transition*
|
|
@@ -1964,9 +1963,8 @@ function onFlagChange(name, cb) {
|
|
|
1964
1963
|
}
|
|
1965
1964
|
function apply(name, next) {
|
|
1966
1965
|
if (next === null) return;
|
|
1967
|
-
const prev =
|
|
1968
|
-
|
|
1969
|
-
else subagent = next;
|
|
1966
|
+
const prev = contextManagement;
|
|
1967
|
+
contextManagement = next;
|
|
1970
1968
|
if (next !== prev) for (const cb of subscribers[name]) try {
|
|
1971
1969
|
cb(next);
|
|
1972
1970
|
} catch (err) {
|
|
@@ -1977,10 +1975,13 @@ function apply(name, next) {
|
|
|
1977
1975
|
}
|
|
1978
1976
|
}
|
|
1979
1977
|
async function pollOnce() {
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1978
|
+
try {
|
|
1979
|
+
const flags = await fetchHarnessFlags();
|
|
1980
|
+
if (!flags) return;
|
|
1981
|
+
apply("contextManagement", flags.contextManagement ?? null);
|
|
1982
|
+
} catch (err) {
|
|
1983
|
+
log$9.debug({ err }, "feature-flag poll threw");
|
|
1984
|
+
}
|
|
1984
1985
|
}
|
|
1985
1986
|
/**
|
|
1986
1987
|
* Start the shared background poll (idempotent). No-op when there's no
|
|
@@ -1991,7 +1992,7 @@ async function pollOnce() {
|
|
|
1991
1992
|
function startFeatureFlagPoller() {
|
|
1992
1993
|
if (pollerStarted || !hasFlagSource()) return;
|
|
1993
1994
|
pollerStarted = true;
|
|
1994
|
-
pollOnce();
|
|
1995
|
+
pollOnce().finally(markFirstPollSettled);
|
|
1995
1996
|
setInterval(() => void pollOnce(), FLAG_POLL_INTERVAL_MS).unref?.();
|
|
1996
1997
|
}
|
|
1997
1998
|
//#endregion
|
|
@@ -2736,7 +2737,7 @@ function soulSection(cwd, soul) {
|
|
|
2736
2737
|
|
|
2737
2738
|
**\`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.
|
|
2738
2739
|
|
|
2739
|
-
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.
|
|
2740
|
+
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.
|
|
2740
2741
|
|
|
2741
2742
|
${soul ? soul : "_(empty — write to `soul.md` to define your persona)_"}`;
|
|
2742
2743
|
}
|
|
@@ -2758,8 +2759,12 @@ const log$2 = logger.child({ module: "subagent-ext" });
|
|
|
2758
2759
|
const MAX_TASKS = 8;
|
|
2759
2760
|
const TaskItem = Type.Object({
|
|
2760
2761
|
task: Type.String({ description: "The task to delegate to a subagent run." }),
|
|
2762
|
+
title: Type.String({
|
|
2763
|
+
description: "A SHORT name for this task — 3-6 words, sentence case, no trailing period. This is what the person in the chat sees as the row for this subagent, so name the work, do not restate the prompt. Good: \"Audit the billing gate\", \"Compare competitor pricing\", \"Draft the migration\". Bad: \"You are looking at apps/anyone/web and should check every component…\".",
|
|
2764
|
+
maxLength: 120
|
|
2765
|
+
}),
|
|
2761
2766
|
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." })),
|
|
2762
|
-
model: Type.Optional(Type.String({ description: "Optional model id to run this subagent on
|
|
2767
|
+
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." }))
|
|
2763
2768
|
});
|
|
2764
2769
|
const SubagentParams = Type.Object({ tasks: Type.Array(TaskItem, {
|
|
2765
2770
|
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.",
|
|
@@ -2775,7 +2780,7 @@ function buildTool(messageId) {
|
|
|
2775
2780
|
"Delegate one or more tasks to subagent runs — fresh isolated copies of yourself, each with its own context window, linked to this conversation.",
|
|
2776
2781
|
"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.",
|
|
2777
2782
|
"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.",
|
|
2778
|
-
"Pass tasks: [{ task, persona?, model? }]. 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
|
|
2783
|
+
"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."
|
|
2779
2784
|
].join(" "),
|
|
2780
2785
|
promptSnippet: "subagent — delegate tasks to isolated subagent runs; each rewakes you with its result when done",
|
|
2781
2786
|
parameters: SubagentParams,
|
|
@@ -2791,6 +2796,7 @@ function buildTool(messageId) {
|
|
|
2791
2796
|
};
|
|
2792
2797
|
const spawnTasks = tasks.map((t) => ({
|
|
2793
2798
|
task: t.task,
|
|
2799
|
+
title: t.title ?? null,
|
|
2794
2800
|
persona: t.persona ?? null,
|
|
2795
2801
|
model: t.model ?? null
|
|
2796
2802
|
}));
|
|
@@ -2803,7 +2809,7 @@ function buildTool(messageId) {
|
|
|
2803
2809
|
event: "subagent_spawned",
|
|
2804
2810
|
count: taskIds.length
|
|
2805
2811
|
}, "subagent tasks queued");
|
|
2806
|
-
const lines = taskIds.map((id, i) => `- ${id}: ${spawnTasks[i]?.task ?? ""}`).join("\n");
|
|
2812
|
+
const lines = taskIds.map((id, i) => `- ${id}: ${spawnTasks[i]?.title ?? spawnTasks[i]?.task ?? ""}`).join("\n");
|
|
2807
2813
|
return {
|
|
2808
2814
|
content: [{
|
|
2809
2815
|
type: "text",
|
|
@@ -2830,31 +2836,37 @@ function buildTool(messageId) {
|
|
|
2830
2836
|
};
|
|
2831
2837
|
}
|
|
2832
2838
|
/**
|
|
2833
|
-
* Gated on `harness-subagent-enabled`, read from the shared feature-flag poll.
|
|
2834
2839
|
* The factory takes the session's channel context to resolve the originating
|
|
2835
2840
|
* messageId — the api links each spawned run to the conversation that message
|
|
2836
2841
|
* belongs to and rewakes it on completion (nothing about the parent is piped
|
|
2837
|
-
* from the sandbox beyond that id).
|
|
2842
|
+
* from the sandbox beyond that id). The tool is registered unconditionally at
|
|
2843
|
+
* session_start.
|
|
2838
2844
|
*/
|
|
2839
2845
|
function createSubagentExtension({ channelContext }) {
|
|
2840
2846
|
return (pi) => {
|
|
2841
2847
|
const messageId = extractMessageId(channelContext);
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
if (
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
}
|
|
2848
|
+
let registered = false;
|
|
2849
|
+
const registerOnce = () => {
|
|
2850
|
+
if (registered) return;
|
|
2851
|
+
registered = true;
|
|
2852
|
+
pi.registerTool(buildTool(messageId));
|
|
2853
|
+
log$2.info({ event: "subagent_enabled" }, "subagent tool registered");
|
|
2854
|
+
};
|
|
2855
|
+
pi.on("session_start", () => {
|
|
2856
|
+
registerOnce();
|
|
2848
2857
|
});
|
|
2849
2858
|
};
|
|
2850
2859
|
}
|
|
2851
2860
|
//#endregion
|
|
2852
2861
|
//#region src/extensions/tool-call-env.ts
|
|
2853
2862
|
const TOOL_CALL_ID_VAR = "TOOL_CALL_ID";
|
|
2863
|
+
function shellQuoteValue(value) {
|
|
2864
|
+
return quote([value]);
|
|
2865
|
+
}
|
|
2854
2866
|
function withToolCallId({ command, toolCallId }) {
|
|
2855
|
-
return `export ${TOOL_CALL_ID_VAR}=${toolCallId}; ${command}`;
|
|
2867
|
+
return `export ${TOOL_CALL_ID_VAR}=${shellQuoteValue(toolCallId)}; ${command}`;
|
|
2856
2868
|
}
|
|
2857
|
-
const PLATFORM_EXPORT = new RegExp(`^\\s*export\\s+(?:${TOOL_CALL_ID_VAR}|ANYONE_\\w+|SKYDIVE_\\w+)=(?:"(?:\\\\.|[^"])*"|'[^']*'|[^;\\s]*)\\s*;\\s*`);
|
|
2869
|
+
const PLATFORM_EXPORT = new RegExp(`^\\s*export\\s+(?:${TOOL_CALL_ID_VAR}|ANYONE_\\w+|SKYDIVE_\\w+)=(?:"(?:\\\\.|[^"])*"|'(?:'\\\\''|[^'])*'|[^;\\s]*)\\s*;\\s*`);
|
|
2858
2870
|
function stripPlatformExportsForDisplay(command) {
|
|
2859
2871
|
let c = command;
|
|
2860
2872
|
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.390",
|
|
4
4
|
"homepage": "https://skydive.com",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Create, Inc.",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"@skydiveai/pi-server": "^0.1.0",
|
|
46
46
|
"hono": "^4.6.14",
|
|
47
47
|
"pino": "^9.6.0",
|
|
48
|
+
"shell-quote": "^1.8.4",
|
|
48
49
|
"typebox": "^1.1.34",
|
|
49
50
|
"yaml": "^2.8.3",
|
|
50
51
|
"zod": "^3.25.0"
|