@skydiveai/pi-extensions 0.1.252-beta.1 → 0.1.252-beta.11

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.d.mts CHANGED
@@ -1,9 +1,27 @@
1
+ /// <reference types="node" />
1
2
  import { A2AOptions, Middleware, ProtocolsOptions } from "@skydiveai/pi-server";
2
3
  import { ExtensionFactory } from "@earendil-works/pi-coding-agent";
3
4
  import { Type } from "typebox";
4
5
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
6
+ import * as _$qs from "qs";
7
+ import * as _$express from "express";
5
8
  import { Logger } from "pino";
6
9
 
10
+ //#region ../../node_modules/@types/express-serve-static-core/index.d.ts
11
+ declare global {
12
+ namespace Express {
13
+ // These open interfaces may be extended in an application-specific manner via declaration merging.
14
+ // See for example method-override.d.ts (https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/method-override/index.d.ts)
15
+ interface Request {}
16
+ interface Response {}
17
+ interface Locals {}
18
+ interface Application {}
19
+ }
20
+ }
21
+ interface ParamsDictionary {
22
+ [key: string]: string;
23
+ }
24
+ //#endregion
7
25
  //#region src/harness.d.ts
8
26
  type CreateHarnessOptions = ProtocolsOptions & {
9
27
  cwd: string;
@@ -26,7 +44,7 @@ declare function createHarness(options: CreateHarnessOptions): {
26
44
  };
27
45
  protocols: {
28
46
  handlers: {
29
- /** Express-style; mount at /a2a. */a2a: RequestHandler; /** GET /.well-known/agent-card.json. */
47
+ /** Express-style; mount at /a2a. */a2a: _$express.RequestHandler<ParamsDictionary, any, any, _$qs.ParsedQs, Record<string, any>>; /** GET /.well-known/agent-card.json. */
30
48
  agentCard: Middleware; /** Mirrors each vendor's API shape. */
31
49
  openai: {
32
50
  v1: {
package/dist/index.mjs CHANGED
@@ -161,7 +161,7 @@ async function readPackageVersion(pkg) {
161
161
  let dir;
162
162
  try {
163
163
  dir = dirname(require.resolve(pkg));
164
- } catch {
164
+ } catch (_error) {
165
165
  return null;
166
166
  }
167
167
  let current = dir;
@@ -169,7 +169,7 @@ async function readPackageVersion(pkg) {
169
169
  try {
170
170
  const manifest = JSON.parse(await readFile(resolve(current, "package.json"), "utf8"));
171
171
  if (manifest.name === pkg && typeof manifest.version === "string") return manifest.version;
172
- } catch {}
172
+ } catch (_error) {}
173
173
  const parent = dirname(current);
174
174
  if (parent === current) return null;
175
175
  current = parent;
@@ -210,7 +210,7 @@ function createPlatformEnvMiddleware() {
210
210
  return async (req, _res, next) => {
211
211
  try {
212
212
  await loadPlatformConfig(new Request(requestUrl(req), { headers: requestHeaders(req) }));
213
- } catch {}
213
+ } catch (_error) {}
214
214
  next();
215
215
  };
216
216
  }
@@ -662,7 +662,7 @@ async function listToolFiles(dir) {
662
662
  file,
663
663
  mtimeMs: s.mtimeMs
664
664
  });
665
- } catch {}
665
+ } catch (_error) {}
666
666
  }
667
667
  return out;
668
668
  }
@@ -946,7 +946,7 @@ function probeAlive(pid) {
946
946
  try {
947
947
  process.kill(pid, 0);
948
948
  return true;
949
- } catch {
949
+ } catch (_error) {
950
950
  return false;
951
951
  }
952
952
  }
@@ -1041,7 +1041,7 @@ async function connectClient(id, config, opts = {}) {
1041
1041
  }
1042
1042
  try {
1043
1043
  await client.close();
1044
- } catch {}
1044
+ } catch (_error) {}
1045
1045
  return {
1046
1046
  status: "failed",
1047
1047
  error: formatError(result),
@@ -1163,7 +1163,7 @@ function estimateToolTokens(tool) {
1163
1163
  let chars = tool.name.length + (tool.description?.length ?? 0);
1164
1164
  if (tool.inputSchema !== void 0 && tool.inputSchema !== null) try {
1165
1165
  chars += JSON.stringify(tool.inputSchema).length;
1166
- } catch {
1166
+ } catch (_error) {
1167
1167
  chars += 256;
1168
1168
  }
1169
1169
  return Math.ceil(chars / 4);
@@ -2163,7 +2163,7 @@ function extractMessageId(channelContextJson) {
2163
2163
  try {
2164
2164
  const parsed = channelContextRefSchema.safeParse(JSON.parse(channelContextJson));
2165
2165
  return parsed.success ? parsed.data.messageId : null;
2166
- } catch {
2166
+ } catch (_error) {
2167
2167
  return null;
2168
2168
  }
2169
2169
  }
@@ -2410,7 +2410,7 @@ async function postSubagentSpawn({ messageId, tasks }) {
2410
2410
  try {
2411
2411
  const errBody = await res.json();
2412
2412
  if (errBody && typeof errBody.error === "string") detail = `: ${errBody.error}`;
2413
- } catch {}
2413
+ } catch (_error) {}
2414
2414
  throw new Error(`subagent-spawn POST failed (${res.status})${detail}`);
2415
2415
  }
2416
2416
  const body = await res.json();
@@ -2434,7 +2434,7 @@ async function postSubagentRearm({ messageId, taskId, timeoutMinutes }) {
2434
2434
  try {
2435
2435
  const body = await res.json();
2436
2436
  if (typeof body.error === "string") detail = `: ${body.error}`;
2437
- } catch {}
2437
+ } catch (_error) {}
2438
2438
  throw new Error(`subagent re-arm POST failed (${res.status})${detail}`);
2439
2439
  }
2440
2440
  const body = await res.json();
@@ -3028,6 +3028,13 @@ const bashDefaultTimeoutExtension = (pi) => {
3028
3028
  * idle sandbox's resource usage is not the agent's problem and there is no
3029
3029
  * open turn to deliver into anyway.
3030
3030
  *
3031
+ * Delivery is further gated to turns that are ACTIVELY in tool-call work
3032
+ * (see WarningDeliveryGate): a warning that lands once the model has moved
3033
+ * on to its final reply triggers a continuation turn after the answer was
3034
+ * already produced, which breaks external-channel delivery (Slack steer-
3035
+ * after-reply). So the warning only interrupts between/around tool calls
3036
+ * and never chases a turn that is already wrapping up.
3037
+ *
3031
3038
  * Best-effort throughout: any read failure (cgroup absent, controller not
3032
3039
  * delegated, non-cgroup-v2 host, df missing) reads as "no signal" for that
3033
3040
  * stat and the extension warns on what it can see — it must never break a
@@ -3151,20 +3158,77 @@ function resourcePressureWarningText(trigger, { mem, cpuPct, diskPct }) {
3151
3158
  const closer = trigger === "memory" ? "If the workload genuinely needs more memory or CPU, request a bigger sandbox with `platform compute request`." : "A bigger compute tier will not add disk. Free space on the current computer instead, and do not request a compute upgrade for disk pressure.";
3152
3159
  return `<system_notification>${lead} Current usage: ${stats.join(", ")}. Act now: ${remedy} ${closer} Do not mention this constraint as you work unless the user asks. This warning does not change whether the user is owed a reply. Do not call \`platform channel suppress-reply\` because of this warning. Complete any reply already owed after checkpointing. This is an automated resource warning, not a message from the user; continue the task, adjusted.</system_notification>`;
3153
3160
  }
3161
+ /**
3162
+ * Whether a pressure warning may be delivered into the open turn right now.
3163
+ *
3164
+ * Only interrupts turns that are actively in tool-call work. The failure it
3165
+ * guards against: the poll lands after the model has stopped calling tools
3166
+ * and is streaming (or has finished) its final reply. `sendMessage` with
3167
+ * `triggerTurn: true` then starts a continuation turn BEHIND an answer the
3168
+ * user is already owed, which wedges external-channel delivery (Slack).
3169
+ *
3170
+ * Deliverable means ALL of:
3171
+ * - agent active (between agent_start and agent_end),
3172
+ * - the turn has issued at least one tool call,
3173
+ * - no assistant message is currently streaming (while one streams we cannot
3174
+ * know whether it will end the loop, so we hold and re-poll),
3175
+ * - the loop is not closed: no assistant message without tool calls has
3176
+ * completed this turn (that message IS the final reply; anything after it
3177
+ * is post-answer and must not be steered).
3178
+ *
3179
+ * Pure and exported so the gating is regression-testable without fake timers.
3180
+ */
3181
+ var WarningDeliveryGate = class {
3182
+ agentActive = false;
3183
+ sawToolCall = false;
3184
+ toolsInFlight = 0;
3185
+ messageStreaming = false;
3186
+ loopClosed = false;
3187
+ agentStart() {
3188
+ this.agentActive = true;
3189
+ this.sawToolCall = false;
3190
+ this.toolsInFlight = 0;
3191
+ this.messageStreaming = false;
3192
+ this.loopClosed = false;
3193
+ }
3194
+ agentEnd() {
3195
+ this.agentActive = false;
3196
+ this.messageStreaming = false;
3197
+ }
3198
+ toolExecutionStart() {
3199
+ this.sawToolCall = true;
3200
+ this.toolsInFlight += 1;
3201
+ }
3202
+ toolExecutionEnd() {
3203
+ this.toolsInFlight = Math.max(0, this.toolsInFlight - 1);
3204
+ }
3205
+ messageStart(message) {
3206
+ if (message.role !== "assistant") return;
3207
+ this.messageStreaming = true;
3208
+ }
3209
+ messageEnd(message) {
3210
+ if (message.role !== "assistant") return;
3211
+ this.messageStreaming = false;
3212
+ if (!(Array.isArray(message.content) && message.content.some((part) => part.type === "toolCall"))) this.loopClosed = true;
3213
+ }
3214
+ canDeliver() {
3215
+ return this.agentActive && this.sawToolCall && !this.loopClosed && !this.messageStreaming;
3216
+ }
3217
+ };
3154
3218
  const resourcePressureWarningExtension = (pi) => {
3155
- let agentActive = false;
3219
+ const gate = new WarningDeliveryGate();
3156
3220
  let warnedMemThisTurn = false;
3157
3221
  let warnedDiskThisTurn = false;
3158
3222
  let timer = null;
3159
3223
  const sampleCpu = createCpuSampler();
3160
3224
  async function checkOnce() {
3161
- if (!agentActive || warnedMemThisTurn && warnedDiskThisTurn) return;
3225
+ if (!gate.canDeliver() || warnedMemThisTurn && warnedDiskThisTurn) return;
3162
3226
  const [mem, cpuPct, diskPct] = await Promise.all([
3163
3227
  readMemUsePct(),
3164
3228
  sampleCpu(),
3165
3229
  readDiskUsePct()
3166
3230
  ]);
3167
- if (!agentActive) return;
3231
+ if (!gate.canDeliver()) return;
3168
3232
  let trigger = null;
3169
3233
  if (!warnedMemThisTurn && mem !== null && mem.pct >= 80) {
3170
3234
  trigger = "memory";
@@ -3193,8 +3257,20 @@ const resourcePressureWarningExtension = (pi) => {
3193
3257
  deliverAs: "followUp"
3194
3258
  });
3195
3259
  }
3260
+ pi.on("tool_execution_start", () => {
3261
+ gate.toolExecutionStart();
3262
+ });
3263
+ pi.on("tool_execution_end", () => {
3264
+ gate.toolExecutionEnd();
3265
+ });
3266
+ pi.on("message_start", (event) => {
3267
+ gate.messageStart(event.message);
3268
+ });
3269
+ pi.on("message_end", (event) => {
3270
+ gate.messageEnd(event.message);
3271
+ });
3196
3272
  pi.on("agent_start", async () => {
3197
- agentActive = true;
3273
+ gate.agentStart();
3198
3274
  warnedMemThisTurn = false;
3199
3275
  warnedDiskThisTurn = false;
3200
3276
  if (!timer) {
@@ -3207,7 +3283,7 @@ const resourcePressureWarningExtension = (pi) => {
3207
3283
  }
3208
3284
  });
3209
3285
  pi.on("agent_end", async () => {
3210
- agentActive = false;
3286
+ gate.agentEnd();
3211
3287
  if (timer) {
3212
3288
  clearInterval(timer);
3213
3289
  timer = null;
@@ -3708,7 +3784,7 @@ async function readMemoryIndexFile({ cwd }) {
3708
3784
  try {
3709
3785
  const trimmed = (await readFile(path, "utf-8")).trim();
3710
3786
  return trimmed.length > 0 ? trimmed : null;
3711
- } catch {
3787
+ } catch (_error) {
3712
3788
  return null;
3713
3789
  }
3714
3790
  }
@@ -3748,7 +3824,7 @@ async function maxMtimeAcrossDir(dir) {
3748
3824
  let dirStat;
3749
3825
  try {
3750
3826
  dirStat = await stat(dir);
3751
- } catch {
3827
+ } catch (_error) {
3752
3828
  return null;
3753
3829
  }
3754
3830
  if (!dirStat.isDirectory()) return null;
@@ -3763,7 +3839,7 @@ async function walkMdFiles(dir, out) {
3763
3839
  let entries;
3764
3840
  try {
3765
3841
  entries = await readdir(dir, { withFileTypes: true });
3766
- } catch {
3842
+ } catch (_error) {
3767
3843
  return;
3768
3844
  }
3769
3845
  await Promise.all(entries.map(async (entry) => {
@@ -3776,7 +3852,7 @@ async function listMdFilesShallow(dir) {
3776
3852
  let entries;
3777
3853
  try {
3778
3854
  entries = await readdir(dir, { withFileTypes: true });
3779
- } catch {
3855
+ } catch (_error) {
3780
3856
  return [];
3781
3857
  }
3782
3858
  return entries.filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => join(dir, e.name));
@@ -3785,7 +3861,7 @@ async function listSubdirs(dir) {
3785
3861
  let entries;
3786
3862
  try {
3787
3863
  entries = await readdir(dir, { withFileTypes: true });
3788
- } catch {
3864
+ } catch (_error) {
3789
3865
  return [];
3790
3866
  }
3791
3867
  return entries.filter((e) => e.isDirectory()).map((e) => join(dir, e.name));
@@ -3811,14 +3887,14 @@ async function readFrontmatterOnly(filePath) {
3811
3887
  let fh;
3812
3888
  try {
3813
3889
  fh = await open(filePath, "r");
3814
- } catch {
3890
+ } catch (_error) {
3815
3891
  return null;
3816
3892
  }
3817
3893
  try {
3818
3894
  const buf = Buffer.alloc(FRONTMATTER_READ_BYTES);
3819
3895
  const { bytesRead } = await fh.read(buf, 0, FRONTMATTER_READ_BYTES, 0);
3820
3896
  return parseFrontmatter(buf.toString("utf-8", 0, bytesRead));
3821
- } catch {
3897
+ } catch (_error) {
3822
3898
  return null;
3823
3899
  } finally {
3824
3900
  await fh.close().catch(() => {});
@@ -3830,7 +3906,7 @@ function parseFrontmatter(text) {
3830
3906
  let parsed;
3831
3907
  try {
3832
3908
  parsed = parse(match[1]);
3833
- } catch {
3909
+ } catch (_error) {
3834
3910
  return null;
3835
3911
  }
3836
3912
  const result = FrontmatterSchema.safeParse(parsed);
@@ -4826,7 +4902,7 @@ async function readLogChunk(id, maxBytes, anchor) {
4826
4902
  text: buffer.toString("utf8", start),
4827
4903
  size
4828
4904
  };
4829
- } catch {
4905
+ } catch (_error) {
4830
4906
  return {
4831
4907
  text: "",
4832
4908
  size: 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skydiveai/pi-extensions",
3
- "version": "0.1.252-beta.1",
3
+ "version": "0.1.252-beta.11",
4
4
  "homepage": "https://skydive.com",
5
5
  "license": "MIT",
6
6
  "author": "Create, Inc.",