@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.
Files changed (2) hide show
  1. package/dist/index.mjs +58 -14
  2. 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
- const flags = await fetchHarnessFlags();
1981
- if (!flags) return;
1982
- apply("contextManagement", flags.contextManagement ?? null);
1983
- apply("subagent", flags.subagent ?? null);
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. Edit it (then \`git add soul.md && git commit && git push\`) to redefine yourself; picked up on the next message.
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") === true) {
2845
- pi.registerTool(buildTool(messageId));
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.14",
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"