@skydiveai/pi-extensions 0.1.0-beta.14 → 0.1.0-beta.142
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 +58 -14
- 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";
|
|
@@ -1949,11 +1950,33 @@ const subscribers = {
|
|
|
1949
1950
|
subagent: /* @__PURE__ */ new Set()
|
|
1950
1951
|
};
|
|
1951
1952
|
let pollerStarted = false;
|
|
1953
|
+
let firstPollSettled = false;
|
|
1954
|
+
let resolveFirstPoll = null;
|
|
1955
|
+
const firstPollPromise = new Promise((resolve) => {
|
|
1956
|
+
resolveFirstPoll = resolve;
|
|
1957
|
+
});
|
|
1958
|
+
function markFirstPollSettled() {
|
|
1959
|
+
if (firstPollSettled) return;
|
|
1960
|
+
firstPollSettled = true;
|
|
1961
|
+
resolveFirstPoll?.();
|
|
1962
|
+
}
|
|
1952
1963
|
/** Last-polled value of a flag, or `null` if not yet resolved. */
|
|
1953
1964
|
function getPolledFlag(name) {
|
|
1954
1965
|
return name === "contextManagement" ? contextManagement : subagent;
|
|
1955
1966
|
}
|
|
1956
1967
|
/**
|
|
1968
|
+
* Await the first poll already kicked by `startFeatureFlagPoller` (never a new
|
|
1969
|
+
* GET). Resolves when that poll settles, immediately if it already has, or
|
|
1970
|
+
* immediately when there's no flag source to poll. Callers on the hot path
|
|
1971
|
+
* should race this against their own short timeout so a slow/failed flag
|
|
1972
|
+
* service cannot delay first-token; a timeout just means the caller reads the
|
|
1973
|
+
* still-cold cache and falls back to its default, exactly as before.
|
|
1974
|
+
*/
|
|
1975
|
+
function awaitFirstFlagPoll() {
|
|
1976
|
+
if (firstPollSettled || !hasFlagSource()) return Promise.resolve();
|
|
1977
|
+
return firstPollPromise;
|
|
1978
|
+
}
|
|
1979
|
+
/**
|
|
1957
1980
|
* Subscribe to changes of a flag. The callback fires only on a *transition*
|
|
1958
1981
|
* (skipped while the value is unchanged), so a subscriber registered before the
|
|
1959
1982
|
* first poll still learns the initial value. Returns an unsubscribe fn.
|
|
@@ -1977,10 +2000,14 @@ function apply(name, next) {
|
|
|
1977
2000
|
}
|
|
1978
2001
|
}
|
|
1979
2002
|
async function pollOnce() {
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
2003
|
+
try {
|
|
2004
|
+
const flags = await fetchHarnessFlags();
|
|
2005
|
+
if (!flags) return;
|
|
2006
|
+
apply("contextManagement", flags.contextManagement ?? null);
|
|
2007
|
+
apply("subagent", flags.subagent ?? null);
|
|
2008
|
+
} catch (err) {
|
|
2009
|
+
log$9.debug({ err }, "feature-flag poll threw");
|
|
2010
|
+
}
|
|
1984
2011
|
}
|
|
1985
2012
|
/**
|
|
1986
2013
|
* Start the shared background poll (idempotent). No-op when there's no
|
|
@@ -1991,7 +2018,7 @@ async function pollOnce() {
|
|
|
1991
2018
|
function startFeatureFlagPoller() {
|
|
1992
2019
|
if (pollerStarted || !hasFlagSource()) return;
|
|
1993
2020
|
pollerStarted = true;
|
|
1994
|
-
pollOnce();
|
|
2021
|
+
pollOnce().finally(markFirstPollSettled);
|
|
1995
2022
|
setInterval(() => void pollOnce(), FLAG_POLL_INTERVAL_MS).unref?.();
|
|
1996
2023
|
}
|
|
1997
2024
|
//#endregion
|
|
@@ -2736,7 +2763,7 @@ function soulSection(cwd, soul) {
|
|
|
2736
2763
|
|
|
2737
2764
|
**\`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
2765
|
|
|
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.
|
|
2766
|
+
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
2767
|
|
|
2741
2768
|
${soul ? soul : "_(empty — write to `soul.md` to define your persona)_"}`;
|
|
2742
2769
|
}
|
|
@@ -2755,9 +2782,14 @@ const soulExtension = (pi) => {
|
|
|
2755
2782
|
//#endregion
|
|
2756
2783
|
//#region src/extensions/subagent/index.ts
|
|
2757
2784
|
const log$2 = logger.child({ module: "subagent-ext" });
|
|
2785
|
+
const COLD_START_FLAG_WAIT_MS = 750;
|
|
2758
2786
|
const MAX_TASKS = 8;
|
|
2759
2787
|
const TaskItem = Type.Object({
|
|
2760
2788
|
task: Type.String({ description: "The task to delegate to a subagent run." }),
|
|
2789
|
+
title: Type.String({
|
|
2790
|
+
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…\".",
|
|
2791
|
+
maxLength: 120
|
|
2792
|
+
}),
|
|
2761
2793
|
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
2794
|
model: Type.Optional(Type.String({ description: "Optional model id to run this subagent on (e.g. \"anthropic/claude-opus-4-8\"). Must be a real catalogued model. Omit to run on your own model. If you are locked to a Google-compliant model, only compliant models are accepted." }))
|
|
2763
2795
|
});
|
|
@@ -2775,7 +2807,7 @@ function buildTool(messageId) {
|
|
|
2775
2807
|
"Delegate one or more tasks to subagent runs — fresh isolated copies of yourself, each with its own context window, linked to this conversation.",
|
|
2776
2808
|
"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
2809
|
"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 run on your own model."
|
|
2810
|
+
"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 run on your own model."
|
|
2779
2811
|
].join(" "),
|
|
2780
2812
|
promptSnippet: "subagent — delegate tasks to isolated subagent runs; each rewakes you with its result when done",
|
|
2781
2813
|
parameters: SubagentParams,
|
|
@@ -2791,6 +2823,7 @@ function buildTool(messageId) {
|
|
|
2791
2823
|
};
|
|
2792
2824
|
const spawnTasks = tasks.map((t) => ({
|
|
2793
2825
|
task: t.task,
|
|
2826
|
+
title: t.title ?? null,
|
|
2794
2827
|
persona: t.persona ?? null,
|
|
2795
2828
|
model: t.model ?? null
|
|
2796
2829
|
}));
|
|
@@ -2803,7 +2836,7 @@ function buildTool(messageId) {
|
|
|
2803
2836
|
event: "subagent_spawned",
|
|
2804
2837
|
count: taskIds.length
|
|
2805
2838
|
}, "subagent tasks queued");
|
|
2806
|
-
const lines = taskIds.map((id, i) => `- ${id}: ${spawnTasks[i]?.task ?? ""}`).join("\n");
|
|
2839
|
+
const lines = taskIds.map((id, i) => `- ${id}: ${spawnTasks[i]?.title ?? spawnTasks[i]?.task ?? ""}`).join("\n");
|
|
2807
2840
|
return {
|
|
2808
2841
|
content: [{
|
|
2809
2842
|
type: "text",
|
|
@@ -2840,21 +2873,32 @@ function createSubagentExtension({ channelContext }) {
|
|
|
2840
2873
|
return (pi) => {
|
|
2841
2874
|
const messageId = extractMessageId(channelContext);
|
|
2842
2875
|
startFeatureFlagPoller();
|
|
2876
|
+
let registered = false;
|
|
2877
|
+
const registerOnce = () => {
|
|
2878
|
+
if (registered) return;
|
|
2879
|
+
registered = true;
|
|
2880
|
+
pi.registerTool(buildTool(messageId));
|
|
2881
|
+
log$2.info({ event: "subagent_enabled" }, "subagent tool registered");
|
|
2882
|
+
};
|
|
2883
|
+
onFlagChange("subagent", (enabled) => {
|
|
2884
|
+
if (enabled) registerOnce();
|
|
2885
|
+
});
|
|
2843
2886
|
pi.on("session_start", async () => {
|
|
2844
|
-
if (getPolledFlag("subagent") ===
|
|
2845
|
-
|
|
2846
|
-
log$2.info({ event: "subagent_enabled" }, "subagent tool registered");
|
|
2847
|
-
}
|
|
2887
|
+
if (getPolledFlag("subagent") === null) await Promise.race([awaitFirstFlagPoll(), new Promise((resolve) => setTimeout(resolve, COLD_START_FLAG_WAIT_MS).unref?.())]);
|
|
2888
|
+
if (getPolledFlag("subagent") === true) registerOnce();
|
|
2848
2889
|
});
|
|
2849
2890
|
};
|
|
2850
2891
|
}
|
|
2851
2892
|
//#endregion
|
|
2852
2893
|
//#region src/extensions/tool-call-env.ts
|
|
2853
2894
|
const TOOL_CALL_ID_VAR = "TOOL_CALL_ID";
|
|
2895
|
+
function shellQuoteValue(value) {
|
|
2896
|
+
return quote([value]);
|
|
2897
|
+
}
|
|
2854
2898
|
function withToolCallId({ command, toolCallId }) {
|
|
2855
|
-
return `export ${TOOL_CALL_ID_VAR}=${toolCallId}; ${command}`;
|
|
2899
|
+
return `export ${TOOL_CALL_ID_VAR}=${shellQuoteValue(toolCallId)}; ${command}`;
|
|
2856
2900
|
}
|
|
2857
|
-
const PLATFORM_EXPORT = new RegExp(`^\\s*export\\s+(?:${TOOL_CALL_ID_VAR}|ANYONE_\\w+|SKYDIVE_\\w+)=(?:"(?:\\\\.|[^"])*"|'[^']*'|[^;\\s]*)\\s*;\\s*`);
|
|
2901
|
+
const PLATFORM_EXPORT = new RegExp(`^\\s*export\\s+(?:${TOOL_CALL_ID_VAR}|ANYONE_\\w+|SKYDIVE_\\w+)=(?:"(?:\\\\.|[^"])*"|'(?:'\\\\''|[^'])*'|[^;\\s]*)\\s*;\\s*`);
|
|
2858
2902
|
function stripPlatformExportsForDisplay(command) {
|
|
2859
2903
|
let c = command;
|
|
2860
2904
|
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.142",
|
|
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"
|