@tt-a1i/openpi 0.3.0 → 0.3.1

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.
@@ -17,7 +17,10 @@ import {
17
17
  resetOpenPiToolSurface,
18
18
  } from "../shared/tool-surface.ts";
19
19
 
20
- const CapabilitySchema = Type.Enum(OPENPI_CAPABILITY_NAMES);
20
+ const CapabilitySchema = Type.Unsafe<OpenPiCapability>({
21
+ type: "string",
22
+ enum: OPENPI_CAPABILITY_NAMES,
23
+ });
21
24
 
22
25
  const OpenPiLoadToolsParameters = Type.Object({
23
26
  groups: Type.Optional(
@@ -95,8 +95,7 @@ import {
95
95
  SUBAGENT_WAIT_PARAMETER_DESCRIPTIONS,
96
96
  SUBAGENT_WAIT_TOOL_DESCRIPTION,
97
97
  } from "./src/prompt.ts";
98
- import { createDeferredResultDelivery } from "./src/result-delivery.ts";
99
- import { resultDeliveryOptions } from "../background-terminals/src/result-delivery.ts";
98
+ import { createSubagentResultDelivery } from "./src/result-delivery.ts";
100
99
  import {
101
100
  effectiveChildToolAllowlist,
102
101
  resolveStandaloneChildProjectTrust,
@@ -210,7 +209,7 @@ export function createSubagentResultDispatcher(
210
209
  pi: ExtensionAPI,
211
210
  outputFor: (snap: SubagentSnapshot) => string = truncatedOutput,
212
211
  ) {
213
- return (snaps: readonly SubagentSnapshot[], wake: boolean) => {
212
+ return (snaps: readonly SubagentSnapshot[]) => {
214
213
  if (snaps.length === 0) return;
215
214
  const content = snaps
216
215
  .map((snap) =>
@@ -249,7 +248,7 @@ export function createSubagentResultDispatcher(
249
248
  display: false,
250
249
  details,
251
250
  },
252
- resultDeliveryOptions(wake),
251
+ { deliverAs: "followUp", triggerTurn: true },
253
252
  );
254
253
  };
255
254
  }
@@ -322,8 +321,14 @@ export default function (pi: ExtensionAPI) {
322
321
  let requestWidgetRender: (() => void) | undefined;
323
322
  let navigationLayerRegistered = false;
324
323
  let dashboardOpen = false;
325
- const resultDelivery = createDeferredResultDelivery<SubagentSnapshot>();
326
324
  const dispatchResults = createSubagentResultDispatcher(pi);
325
+ const resultDelivery = createSubagentResultDelivery<SubagentSnapshot>({
326
+ isIdle: () => sessionContext?.isIdle() === true,
327
+ // Every unconsumed fire-and-forget result must reach the parent. The
328
+ // delivery coordinator batches results that settled while it was busy.
329
+ deliver: dispatchResults,
330
+ });
331
+ pi.on("agent_settled", () => resultDelivery.parentSettled());
327
332
  const hideLifecycleTools = () =>
328
333
  patchOwnedTools(pi, "subagents", {
329
334
  disable: OPENPI_TOOL_SURFACE.subagents.deferred,
@@ -436,25 +441,6 @@ export default function (pi: ExtensionAPI) {
436
441
  navigationLayerRegistered = true;
437
442
  };
438
443
 
439
- /**
440
- * `wake` decides whether this costs the model a turn. A subagent that
441
- * settled while the model sits idle is the result it is waiting on. A
442
- * backlog that piled up while it worked is not: waking once per stale
443
- * subagent forces a turn each, and the model can only answer "that one
444
- * already finished". `nextTurn` still enters context with the user's next
445
- * message, without demanding a reply.
446
- */
447
- const deliverResults = (
448
- snaps: readonly SubagentSnapshot[],
449
- wake: boolean,
450
- ) => {
451
- dispatchResults(snaps, wake);
452
- };
453
-
454
- const flushResults = (wake: boolean) => {
455
- deliverResults(resultDelivery.drain(), wake);
456
- };
457
-
458
444
  const deliverBtwResult = (snap: SubagentSnapshot) => {
459
445
  // appendEntry is a synchronous SessionManager operation and emits an
460
446
  // entry_appended event, so it is safe while the parent is streaming and
@@ -500,10 +486,10 @@ export default function (pi: ExtensionAPI) {
500
486
  // subagent_wait can consume it before agent_settled flushes follow-ups.
501
487
  // Defer a copy: the live snapshot keeps mutating if the subagent is
502
488
  // restarted before the deferred result flushes.
489
+ // The delivery coordinator closes both sides of the wake-up race: it
490
+ // flushes now if the parent is already idle, otherwise the parent's next
491
+ // agent_settled edge rechecks this same pending Map.
503
492
  resultDelivery.defer({ ...snap, meta: { ...snap.meta } });
504
- // Settled while the model sits idle: it has nothing else in flight, so
505
- // this is the result it is waiting on — wake it.
506
- if (sessionContext?.isIdle()) flushResults(true);
507
493
  };
508
494
 
509
495
  pi.on("session_start", (_event, ctx) => {
@@ -530,10 +516,6 @@ export default function (pi: ExtensionAPI) {
530
516
  managerPromise?.then(updateStatus).catch(() => undefined);
531
517
  });
532
518
 
533
- // These settled while the model was working on something else, so they go
534
- // into context without forcing a turn per stale subagent.
535
- pi.on("agent_settled", () => flushResults(false));
536
-
537
519
  pi.on("session_shutdown", async () => {
538
520
  if (navigationLayerRegistered) {
539
521
  removeEditorLayer(pi, "subagents");
@@ -1,17 +1,62 @@
1
- export function createDeferredResultDelivery<T extends { id: string }>() {
1
+ export interface SubagentResultDeliveryOptions<T> {
2
+ /** True only when the parent has no run or queued continuation in flight. */
3
+ readonly isIdle: () => boolean;
4
+ /** Deliver one drained batch and wake the parent. */
5
+ readonly deliver: (results: readonly T[]) => void;
6
+ }
7
+
8
+ /**
9
+ * One-shot result delivery for fire-and-forget subagents.
10
+ *
11
+ * The tool contract promises that a settled child re-invokes the parent. A
12
+ * child that settles while the parent is busy therefore remains retractable
13
+ * until the parent's `agent_settled` event, but it must never be downgraded to
14
+ * a `nextTurn` message that needs another user prompt. There are two symmetric
15
+ * wake-up edges so no ordering can lose the notification:
16
+ *
17
+ * 1. child settles after the parent became idle -> `defer` flushes now;
18
+ * 2. parent settles after the child -> `parentSettled` flushes the batch.
19
+ *
20
+ * The parent boundary wakes even if an earlier extension handler has already
21
+ * started another turn: Pi queues the follow-up into that active run.
22
+ *
23
+ * The Map is the one-shot gate: `subagent_wait` may consume a result before it
24
+ * is delivered, and whichever path drains first prevents duplicate delivery.
25
+ */
26
+ export function createSubagentResultDelivery<T extends { id: string }>(
27
+ options: SubagentResultDeliveryOptions<T>,
28
+ ) {
2
29
  const pending = new Map<string, T>();
3
30
 
31
+ const flush = () => {
32
+ if (pending.size === 0) return;
33
+ const results = [...pending.values()];
34
+ pending.clear();
35
+ try {
36
+ options.deliver(results);
37
+ } catch (error) {
38
+ // A synchronous session teardown may reject append/send. Preserve the
39
+ // original batch ahead of anything deferred re-entrantly while delivery
40
+ // ran, so a later boundary can retry without loss or reordering.
41
+ const current = [...pending.values()];
42
+ pending.clear();
43
+ for (const result of results) pending.set(result.id, result);
44
+ for (const result of current) pending.set(result.id, result);
45
+ throw error;
46
+ }
47
+ };
48
+
4
49
  return {
5
50
  defer(result: T) {
6
51
  pending.set(result.id, result);
52
+ if (options.isIdle()) flush();
7
53
  },
8
54
  consume(ids: Iterable<string>) {
9
55
  for (const id of ids) pending.delete(id);
10
56
  },
11
- drain() {
12
- const results = [...pending.values()];
13
- pending.clear();
14
- return results;
57
+ /** Flush at the authoritative parent boundary. */
58
+ parentSettled() {
59
+ flush();
15
60
  },
16
61
  clear() {
17
62
  pending.clear();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tt-a1i/openpi",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "OpenPI — a Pi-native multi-agent workbench with background execution, isolated subagents, replay-safe workflows, goals, tasks, and observable TUI",
5
5
  "license": "MIT",
6
6
  "author": "tt-a1i",