@tt-a1i/openpi 0.1.0 → 0.2.0

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 (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +295 -389
  3. package/SETUP.md +24 -22
  4. package/THIRD_PARTY_NOTICES.md +3 -4
  5. package/assets/readme-hero-mobile.svg +2 -2
  6. package/assets/readme-hero.svg +10 -10
  7. package/extensions/ask-user/handoff.ts +5 -1
  8. package/extensions/ask-user/index.ts +44 -0
  9. package/extensions/background-terminals/index.ts +118 -29
  10. package/extensions/background-terminals/src/domain.ts +5 -1
  11. package/extensions/background-terminals/src/manager.ts +2 -1
  12. package/extensions/background-terminals/src/prompt.ts +35 -0
  13. package/extensions/background-terminals/src/result-delivery.ts +76 -3
  14. package/extensions/background-terminals/src/ui/tool-result.ts +52 -1
  15. package/extensions/capabilities/index.ts +198 -0
  16. package/extensions/context-pivot/index.ts +21 -0
  17. package/extensions/cron/index.ts +42 -15
  18. package/extensions/execution-convergence/active-evidence.ts +129 -0
  19. package/extensions/execution-convergence/index.ts +442 -0
  20. package/extensions/execution-convergence/workspace-provenance.ts +338 -0
  21. package/extensions/file-search/index.ts +8 -1
  22. package/extensions/file-search/src/binaries.ts +2 -1
  23. package/extensions/git-info/src/runtime.ts +1 -1
  24. package/extensions/goal/controller.ts +2 -1
  25. package/extensions/goal/index.ts +20 -1
  26. package/extensions/plan-mode/index.ts +12 -0
  27. package/extensions/setup/index.ts +241 -45
  28. package/extensions/setup/intercom-fs-helper.cjs +130 -0
  29. package/extensions/setup/intercom.ts +603 -0
  30. package/extensions/shared/child-session.ts +42 -5
  31. package/extensions/shared/setup-config.ts +27 -1
  32. package/extensions/shared/setup-episode-state.ts +7 -0
  33. package/extensions/shared/tool-surface.ts +435 -0
  34. package/extensions/subagents/index.ts +16 -1
  35. package/extensions/subagents/src/manager.ts +13 -11
  36. package/extensions/subagents/src/prompt.ts +1 -1
  37. package/extensions/tasks/index.ts +39 -12
  38. package/extensions/ui-customization/footer.ts +6 -1
  39. package/extensions/workflows/artifacts.ts +6 -1
  40. package/extensions/workflows/dashboard.ts +138 -27
  41. package/extensions/workflows/graph-projection.ts +240 -0
  42. package/extensions/workflows/handoff.ts +194 -0
  43. package/extensions/workflows/index.ts +258 -56
  44. package/extensions/workflows/invocation-ledger.ts +368 -0
  45. package/extensions/workflows/model.ts +57 -1
  46. package/extensions/workflows/operator.ts +131 -0
  47. package/extensions/workflows/prompt.ts +10 -38
  48. package/extensions/workflows/replay-safety.ts +9 -8
  49. package/extensions/workflows/runner.ts +10 -2
  50. package/extensions/workflows/sandbox.ts +5 -0
  51. package/package.json +15 -15
  52. package/skills/subagents/SKILL.md +6 -0
  53. package/skills/workflows/EXAMPLES.md +58 -0
  54. package/skills/workflows/REFERENCE.md +44 -0
  55. package/skills/workflows/SKILL.md +39 -0
@@ -379,14 +379,15 @@ export function beginProcessReplayWorkspaceLease(replaySafe: boolean) {
379
379
  return processReplayWorkspaceGuard.begin(replaySafe);
380
380
  }
381
381
 
382
- interface ReplayResourceLoader extends Pick<
383
- DefaultResourceLoader,
384
- | "getAgentsFiles"
385
- | "getAppendSystemPrompt"
386
- | "getExtensions"
387
- | "getSkills"
388
- | "getSystemPrompt"
389
- > {}
382
+ interface ReplayResourceLoader
383
+ extends Pick<
384
+ DefaultResourceLoader,
385
+ | "getAgentsFiles"
386
+ | "getAppendSystemPrompt"
387
+ | "getExtensions"
388
+ | "getSkills"
389
+ | "getSystemPrompt"
390
+ > {}
390
391
 
391
392
  function digest(value: string | Buffer) {
392
393
  return createHash("sha256").update(value).digest("hex");
@@ -97,6 +97,8 @@ export interface RunAgentOptions {
97
97
  cwd: string;
98
98
  loader: DefaultResourceLoader;
99
99
  settingsManager: SettingsManager;
100
+ /** Optional per-run manager reused by one logical workflow operator. */
101
+ sessionManager?: SessionManager;
100
102
  modelRegistry: ExtensionContext["modelRegistry"];
101
103
  /** Agent Type allowlist; childToolPolicy can only narrow capabilities. */
102
104
  tools?: readonly string[];
@@ -465,6 +467,8 @@ export function createFirstResponseWatchdog(
465
467
  const timeoutMs = options.timeoutMs ?? FIRST_RESPONSE_TIMEOUT_MS;
466
468
  let timer: ReturnType<typeof setTimeout> | undefined;
467
469
  const timeout = new Promise<never>((_resolve, reject) => {
470
+ // This timer owns the awaited watchdog outcome. Keep it referenced so a
471
+ // short-lived Node 22 process cannot exit with the promise still pending.
468
472
  timer = setTimeout(() => {
469
473
  timer = undefined;
470
474
  const model = options.model ? ` for ${options.model}` : "";
@@ -475,7 +479,6 @@ export function createFirstResponseWatchdog(
475
479
  );
476
480
  void onTimeout().catch(() => {});
477
481
  }, timeoutMs);
478
- timer.unref?.();
479
482
  });
480
483
 
481
484
  const cancel = () => {
@@ -485,6 +488,7 @@ export function createFirstResponseWatchdog(
485
488
 
486
489
  return {
487
490
  markResponse: cancel,
491
+ cancel,
488
492
  async waitFor<T>(operation: Promise<T>) {
489
493
  try {
490
494
  return await Promise.race([operation, timeout]);
@@ -533,7 +537,8 @@ export async function runAgent(
533
537
  : {}),
534
538
  resourceLoader: options.loader,
535
539
  settingsManager: options.settingsManager,
536
- sessionManager: SessionManager.inMemory(options.cwd),
540
+ sessionManager:
541
+ options.sessionManager ?? SessionManager.inMemory(options.cwd),
537
542
  ...(customTools ? { customTools } : {}),
538
543
  ...childToolPolicy(childTools),
539
544
  }));
@@ -619,6 +624,7 @@ export async function runAgent(
619
624
  };
620
625
 
621
626
  let markFirstResponse = () => {};
627
+ let cancelFirstResponseWatchdog = () => {};
622
628
  const unsubscribe = childSession.subscribe((event) => {
623
629
  if (settled) return;
624
630
  if (isAssistantResponseEvent(event)) markFirstResponse();
@@ -693,6 +699,7 @@ export async function runAgent(
693
699
  },
694
700
  );
695
701
  markFirstResponse = watchdog.markResponse;
702
+ cancelFirstResponseWatchdog = watchdog.cancel;
696
703
  await Promise.race([
697
704
  watchdog.waitFor(
698
705
  childSession.prompt(buildWorkflowAgentPrompt(options.prompt)),
@@ -703,6 +710,7 @@ export async function runAgent(
703
710
  } catch (error) {
704
711
  promptErrorMessage = errorText(error);
705
712
  } finally {
713
+ cancelFirstResponseWatchdog();
706
714
  options.signal?.removeEventListener("abort", onAbort);
707
715
  settled = true;
708
716
  unsubscribe();
@@ -30,12 +30,15 @@ export interface SandboxAgentOptions {
30
30
  provider?: unknown;
31
31
  effort?: unknown;
32
32
  isolation?: unknown;
33
+ operator?: unknown;
34
+ inputs?: unknown;
33
35
  }
34
36
 
35
37
  export interface SandboxAgentResult {
36
38
  ok: boolean;
37
39
  output: string;
38
40
  structured?: unknown;
41
+ ref?: string;
39
42
  error?: string;
40
43
  }
41
44
 
@@ -102,6 +105,8 @@ function sanitizeAgentOptions(value: unknown): SandboxAgentOptions {
102
105
  ...(value.provider !== undefined ? { provider: value.provider } : {}),
103
106
  ...(value.effort !== undefined ? { effort: value.effort } : {}),
104
107
  ...(value.isolation !== undefined ? { isolation: value.isolation } : {}),
108
+ ...(value.operator !== undefined ? { operator: value.operator } : {}),
109
+ ...(value.inputs !== undefined ? { inputs: value.inputs } : {}),
105
110
  };
106
111
  }
107
112
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@tt-a1i/openpi",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "OpenPI — a Pi-native multi-agent workbench with background execution, isolated subagents, replay-safe workflows, goals, tasks, and observable TUI",
5
- "license": "UNLICENSED",
5
+ "license": "MIT",
6
6
  "author": "tt-a1i",
7
7
  "keywords": [
8
8
  "pi-package",
@@ -14,11 +14,11 @@
14
14
  ],
15
15
  "repository": {
16
16
  "type": "git",
17
- "url": "git+https://github.com/tt-a1i/my-pi-setup.git"
17
+ "url": "git+https://github.com/tt-a1i/openpi.git"
18
18
  },
19
- "homepage": "https://github.com/tt-a1i/my-pi-setup#readme",
19
+ "homepage": "https://github.com/tt-a1i/openpi#readme",
20
20
  "bugs": {
21
- "url": "https://github.com/tt-a1i/my-pi-setup/issues"
21
+ "url": "https://github.com/tt-a1i/openpi/issues"
22
22
  },
23
23
  "files": [
24
24
  "extensions",
@@ -41,13 +41,10 @@
41
41
  "extensions": [
42
42
  "./extensions"
43
43
  ],
44
- "skills": [
45
- "./skills"
46
- ],
47
44
  "themes": [
48
45
  "./themes"
49
46
  ],
50
- "image": "https://raw.githubusercontent.com/tt-a1i/my-pi-setup/main/assets/openpi-package.png"
47
+ "image": "https://raw.githubusercontent.com/tt-a1i/openpi/main/assets/openpi-package.png"
51
48
  },
52
49
  "dependencies": {
53
50
  "@effect/platform-node": "^4.0.0-beta.99",
@@ -55,13 +52,13 @@
55
52
  "effect": "^4.0.0-beta.99"
56
53
  },
57
54
  "devDependencies": {
55
+ "@biomejs/biome": "2.5.8",
58
56
  "@earendil-works/pi-ai": "^0.84.1",
59
57
  "@earendil-works/pi-coding-agent": "^0.84.1",
60
58
  "@earendil-works/pi-tui": "^0.84.1",
61
59
  "@effect/tsgo": "^0.24.2",
62
60
  "@effect/vitest": "^4.0.0-beta.99",
63
61
  "@types/node": "^26.1.1",
64
- "prettier": "^3.9.5",
65
62
  "typebox": "^1.3.6",
66
63
  "typescript": "^7.0.2",
67
64
  "vitest": "4.1.10"
@@ -77,11 +74,14 @@
77
74
  "node": ">=22.19.0"
78
75
  },
79
76
  "scripts": {
80
- "prepublishOnly": "npm run format:check && npm run check && npm test",
81
- "check": "tsc --noEmit",
77
+ "prepublishOnly": "bun run check && bun run test",
78
+ "check": "bun run format:check && bun run lint && bun run typecheck",
82
79
  "prepare": "node scripts/prepare-effect-tsgo.mjs",
83
- "format": "prettier --write \"extensions/**/*.{ts,cjs}\" \"scripts/**/*.{js,mjs,cjs,ts}\" \".github/**/*.{yml,yaml}\" \"*.json\"",
84
- "format:check": "prettier --check \"extensions/**/*.{ts,cjs}\" \"scripts/**/*.{js,mjs,cjs,ts}\" \".github/**/*.{yml,yaml}\" \"*.json\"",
80
+ "format": "biome format --write .",
81
+ "format:check": "biome format .",
82
+ "lint": "biome lint . --error-on-warnings",
83
+ "typecheck": "tsc --noEmit",
85
84
  "test": "node --test --experimental-strip-types extensions/*/*.test.ts && vitest run extensions/file-search/index.spec.ts"
86
- }
85
+ },
86
+ "packageManager": "bun@1.3.14"
87
87
  }
@@ -13,3 +13,9 @@ The tool definitions are canonical for parameters, limits, model syntax, isolati
13
13
  - Prefer a matching agent type when one exists; its tool restriction is enforced. An explicit spawn model or reasoning effort wins, otherwise use the type default then inherit the parent. Types live in `~/.pi/agent/agents/*.md` and, for trusted projects, `.pi/agents/*.md`; see `extensions/subagents/docs/agent-types.md`.
14
14
  - Isolate concurrent writers in worktrees according to the `subagent_spawn` schema so they cannot overwrite one checkout or git index. While Plan Mode is active, use only read-only exploration types (or no type); worktree isolation and types narrowed by Plan Mode are rejected.
15
15
  - After spawning, continue useful parent work. Let automatic result delivery drive the next turn; block only when the immediate next step truly depends on that result.
16
+
17
+ ## Worktree isolation
18
+
19
+ Concurrent writers without isolation share the same checkout and Git index, so edits and `git add` operations can overwrite each other. Set `isolation: "worktree"` and tell every writing child to commit.
20
+
21
+ The worktree is branched from `HEAD`, requires a Git repository, and starts clean without gitignored files such as build output or `.env`. A successful committed child leaves a branch for the parent to merge. An empty branch can be deleted; dirty, untracked, ignored, detached, timed-out, or uninspectable work is preserved instead of being destroyed. Direct subagents retain their checkout for later `subagent_send` review and only reclaim it on Session retirement after a bounded empty-work proof.
@@ -0,0 +1,58 @@
1
+ # Workflow examples
2
+
3
+ ## Independent scan and verification pipeline
4
+
5
+ Each file advances to verification as soon as its own scan completes:
6
+
7
+ ```js
8
+ export const meta = {
9
+ name: "reliability-review",
10
+ description: "Review modules for reliability risks, then report",
11
+ phases: [{ title: "Scan" }, { title: "Verify" }, { title: "Report" }],
12
+ }
13
+ const FINDINGS = {
14
+ type: "object",
15
+ properties: {
16
+ issues: { type: "array", items: { type: "string" } },
17
+ ok: { type: "boolean" },
18
+ },
19
+ required: ["issues", "ok"],
20
+ }
21
+ phase("Scan")
22
+ const checked = await pipeline(
23
+ args.files,
24
+ (file) => agent(`Trace ${file} for reliability risks with file:line evidence.`, {
25
+ agent_type: "explorer", label: `scan:${file}`, phase: "Scan", schema: FINDINGS,
26
+ }),
27
+ (scan, file) => scan.ok
28
+ ? agent(`Verify the candidate issues in ${file}.`, {
29
+ agent_type: "reviewer", label: `verify:${file}`, phase: "Verify", inputs: [scan.ref],
30
+ })
31
+ : null,
32
+ )
33
+ const verified = checked.filter((result) => result && result.ok)
34
+ const dropped = checked.length - verified.length
35
+ if (dropped) log(`${dropped}/${checked.length} file(s) dropped before verification`)
36
+ phase("Report")
37
+ const report = await agent("Synthesize recommendations from the verified findings.", {
38
+ agent_type: "advisor", label: "report", phase: "Report",
39
+ inputs: verified.map((result) => result.ref),
40
+ })
41
+ log(`done — ${verified.length} verified, ${usage().total} tokens`)
42
+ return { verified: verified.length, dropped, report: report.ok ? report.output : report.error }
43
+ ```
44
+
45
+ ## When a barrier is correct
46
+
47
+ Use `parallel()` when one synthesis prompt must compare every independent result:
48
+
49
+ ```js
50
+ const findings = await parallel(files.map((file) => () =>
51
+ agent(`Inspect ${file}.`, { agent_type: "explorer", label: file })
52
+ ))
53
+ const usable = findings.filter((result) => result && result.ok)
54
+ return agent("Deduplicate and rank all findings.", {
55
+ agent_type: "advisor",
56
+ inputs: usable.map((result) => result.ref),
57
+ })
58
+ ```
@@ -0,0 +1,44 @@
1
+ # Workflow DSL reference
2
+
3
+ The `workflow` script is an async JavaScript function body executed in a restricted, killable sandbox. It has no imports, eval, timers, filesystem, network, or process APIs. Normal JavaScript control flow, array methods, `await`, and template strings are available. Return a JSON-serializable value.
4
+
5
+ ## Metadata and narration
6
+
7
+ - `export const meta = { name?, description?, phases: [{ title, detail? }] }` declares progress metadata. Declare phases up front.
8
+ - `phase(title)` selects a declared phase.
9
+ - `log(message)` emits one terminal-safe progress line. The latest 100 lines are retained and dropped-line counts are reported.
10
+ - `usage()` returns cumulative `{ input, output, cacheRead, cacheWrite, total, cost, agents }`. It refreshes after agents settle. Compaction can make it a lower bound; it is a reading, not a limit.
11
+ - `args` is the parsed `args` tool parameter, or the original string when it is not valid JSON.
12
+
13
+ ## Agent calls
14
+
15
+ `await agent(prompt, options)` runs one child and always resolves to `{ ok, output, structured?, ref?, acceptance?, error? }`. Check `ok` before reading output. Children receive normal trust-aware resources but cannot recursively orchestrate or ask the user.
16
+
17
+ Useful options include `agent_type`, `label`, `phase`, `schema`, `acceptance`, `model`, `provider`, `effort`, `isolation`, `operator`, and `inputs`.
18
+
19
+ - Prefer a matching `agent_type`. Model precedence is explicit model/provider, type file, configured built-in role, then parent. Effort precedence is explicit effort, type default, then parent.
20
+ - `schema` validates structured output. Use it whenever later workflow logic branches on fields.
21
+ - `acceptance: { criteria: [{ id, description, requiredEvidence? }] }` requires the same child to return an evidence ledger. Missing, malformed, or rejected criteria make `ok:false` while preserving output and evidence.
22
+ - `operator: "name"` reuses one in-memory child Session for serialized follow-ups inside the same run. Its model, role/tools, effort, structured mode, and cwd are frozen by the first activation. Operators cannot use per-call worktrees or replay, and do not survive restarts.
23
+ - `inputs: [ref, ...]` accepts successful opaque refs from the same workflow run only. Each conclusion is bounded to 16 KiB and total injected input to 48 KiB. Inputs are marked as untrusted data; the resulting graph is observability, not scheduling authority.
24
+ - `isolation: "worktree"` gives a writing child its own branch and checkout. Concurrent writers without isolation share one checkout and Git index and can overwrite each other. Tell isolated writers to commit. Empty worktrees are reclaimed; commits keep the branch; dirty work may keep the directory.
25
+
26
+ ## Fan-out
27
+
28
+ `await pipeline(items, stage1, stage2, ...)` advances each item independently. A stage receives `(previousResult, originalItem, index)`. A throwing stage drops that item to null and skips its remaining stages. Results preserve input order.
29
+
30
+ `await parallel([() => agent(...), ...], { concurrency? })` is a barrier: later code starts after every thunk settles. A throwing thunk becomes null without discarding siblings. Use it when the next step needs the whole set for comparison, deduplication, merging, or an early aggregate decision.
31
+
32
+ Prefer `pipeline()` for ordinary multi-stage fan-out. Mapping, filtering, or flattening between stages is not by itself a reason for a barrier.
33
+
34
+ ## Limits and failures
35
+
36
+ Workflow concurrency defaults to the configured package value and has a hard maximum of 64. Agent calls default to the configured package limit and have a hard maximum of 1024. There is no whole-run deadline. A child must produce its first assistant event within 45 seconds; individual child tool calls time out independently after 3 minutes and return an error result the child can recover from.
37
+
38
+ Each call persists intent, admission, and execution state. Interrupted nonterminal calls become `uncertain`, never guessed failed. Artifacts contain results, bounded transcripts, and a read-only graph projection for explicit result refs.
39
+
40
+ ## Background and replay
41
+
42
+ `background: true` returns a run id immediately. The Session later receives a completion message; `workflow_status` inspects and `workflow_stop` cancels. Lifecycle tools become visible after a background run starts.
43
+
44
+ `resume_from_run_id` accepts a previous run id or unique suffix. Replay is content-based and order-independent. It requires an unchanged prompt, resolved role/schema/model/provider/effort, canonical cwd, repository state, resources, and trust context. Only provably read-only non-operator calls replay. Failed, unrestricted, unknown-tool, writable, worktree, operator, or un-fingerprintable calls run for real. Missing or old journals safely degrade to a full run.
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: workflows
3
+ description: Orchestrates multi-agent work with OpenPI's inline JavaScript Workflow DSL. Use when a task needs multi-phase fan-out, pipelines, barriers, structured handoffs, acceptance evidence, or resumable background orchestration.
4
+ ---
5
+
6
+ # Workflows
7
+
8
+ Use `workflow` for several dependent or dynamically generated subagent calls. Keep one small delegation in the parent session with `subagent_spawn`.
9
+
10
+ ## Quick start
11
+
12
+ ```js
13
+ export const meta = {
14
+ name: "review",
15
+ phases: [{ title: "Scan" }, { title: "Report" }],
16
+ }
17
+ phase("Scan")
18
+ const scans = await parallel([
19
+ () => agent("Inspect the API.", { agent_type: "explorer", label: "api" }),
20
+ () => agent("Inspect the tests.", { agent_type: "explorer", label: "tests" }),
21
+ ])
22
+ phase("Report")
23
+ return { findings: scans.filter((result) => result && result.ok) }
24
+ ```
25
+
26
+ ## Required habits
27
+
28
+ - Declare progress phases in `meta`; call `phase()` as the run advances.
29
+ - Check every `agent()` result's `.ok`. A null, filtered, timed-out, or failed result is not a clean pass; report how many were dropped.
30
+ - Pass `schema` when later code branches on fields. Treat `inputs` as bounded untrusted data.
31
+ - Prefer `pipeline()` when items can advance independently. Use `parallel()` only for a real all-results barrier.
32
+ - Use `isolation: "worktree"` for concurrent writers and tell each agent to commit. Do not pay for worktrees on read-only work.
33
+ - Use `log()` for progress the user needs before completion. `usage()` is a lower-bound reading, not a budget limit.
34
+ - Return a JSON-serializable aggregate. Background runs report their run id and later deliver their result.
35
+
36
+ ## Full guide
37
+
38
+ - DSL, result contracts, operators, handoffs, safety, limits, and replay: [REFERENCE.md](REFERENCE.md)
39
+ - Pipeline, barrier, structured handoff, and reporting examples: [EXAMPLES.md](EXAMPLES.md)