@tt-a1i/openpi 0.6.0 → 0.7.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 (60) hide show
  1. package/README.md +13 -11
  2. package/SETUP.md +2 -2
  3. package/THIRD_PARTY_NOTICES.md +242 -0
  4. package/extensions/ai-providers/README.md +12 -6
  5. package/extensions/ai-providers/cursor/connect-frame-reader.ts +76 -0
  6. package/extensions/ai-providers/cursor/input-images.ts +2 -3
  7. package/extensions/ai-providers/cursor/proto.ts +218 -11
  8. package/extensions/ai-providers/cursor/protobuf.ts +12 -2
  9. package/extensions/ai-providers/cursor/provider.ts +280 -37
  10. package/extensions/ai-providers/cursor/tool-bridge.ts +68 -0
  11. package/extensions/ai-providers/index.ts +3 -3
  12. package/extensions/file-mutation-display/index.ts +13 -9
  13. package/extensions/shared/agent-transcript.ts +3 -2
  14. package/extensions/shared/child-session.ts +14 -0
  15. package/extensions/subagents/index.ts +20 -3
  16. package/extensions/subagents/src/agent-types.ts +5 -17
  17. package/extensions/subagents/src/backends/pi.ts +70 -59
  18. package/extensions/subagents/src/backends/tool-preview.ts +29 -0
  19. package/extensions/subagents/src/manager.ts +2 -71
  20. package/extensions/subagents/src/prompt.ts +2 -2
  21. package/extensions/subagents/src/runtime.ts +10 -3
  22. package/extensions/web/index.ts +69 -5
  23. package/extensions/workflows/artifacts.ts +362 -23
  24. package/extensions/workflows/dashboard.ts +141 -21
  25. package/extensions/workflows/index.ts +62 -20
  26. package/extensions/workflows/progress-projection.ts +7 -1
  27. package/extensions/workflows/runner.ts +5 -162
  28. package/extensions/workflows/sandbox.ts +4 -0
  29. package/package.json +28 -4
  30. package/skills/subagents/REFERENCE.md +6 -7
  31. package/skills/subagents/SKILL.md +1 -1
  32. package/skills/workflows/REFERENCE.md +3 -1
  33. package/web/dist/app.js +87 -0
  34. package/web/dist/favicon.svg +9 -0
  35. package/web/dist/index.html +15 -0
  36. package/web/dist/styles.css +3 -0
  37. package/web/host/web-host.ts +6 -9
  38. package/web/ui/index.html +3 -131
  39. package/web/ui/public/favicon.svg +9 -0
  40. package/web/ui/src/app/App.tsx +134 -0
  41. package/web/ui/src/app/providers.tsx +38 -0
  42. package/web/ui/src/components/Markdown.tsx +58 -0
  43. package/web/ui/src/components/OpenPiLogo.tsx +41 -0
  44. package/web/ui/src/features/activity/ActivityBar.tsx +120 -0
  45. package/web/ui/src/features/composer/Composer.tsx +237 -0
  46. package/web/ui/src/features/sessions/SessionSidebar.tsx +418 -0
  47. package/web/ui/src/features/transcript/Transcript.tsx +860 -0
  48. package/web/ui/src/i18n.ts +159 -0
  49. package/web/ui/src/lib/format.ts +57 -0
  50. package/web/ui/src/main.tsx +16 -0
  51. package/web/ui/src/protocol/client.ts +199 -0
  52. package/web/ui/src/protocol/event-stream.ts +88 -0
  53. package/web/ui/src/store/web-store.ts +926 -0
  54. package/web/ui/src/styles.css +420 -0
  55. package/web/ui/tsconfig.json +12 -0
  56. package/web/ui/vite-env.d.ts +1 -0
  57. package/web/vite.config.mjs +21 -1
  58. package/web/host/static-assets.ts +0 -4
  59. package/web/ui/app.js +0 -1700
  60. package/web/ui/styles.css +0 -680
@@ -53,7 +53,11 @@ import {
53
53
  formatActivityStatus,
54
54
  } from "../shared/activity-status.ts";
55
55
  import { fitNavigationSides } from "../shared/below-editor-navigation.ts";
56
- import { waitBounded } from "../shared/child-session.ts";
56
+ import {
57
+ inheritedChildToolAllowlist,
58
+ resolveStandaloneChildProjectTrust,
59
+ waitBounded,
60
+ } from "../shared/child-session.ts";
57
61
  import { contextPercent } from "../shared/context-utilization.ts";
58
62
  import { completionOwnerFor } from "../shared/completion-inbox.ts";
59
63
  import {
@@ -544,6 +548,7 @@ interface ScriptAgentResult {
544
548
  }
545
549
 
546
550
  interface AgentCallOptions {
551
+ working_dir?: unknown;
547
552
  agent_type?: unknown;
548
553
  label?: unknown;
549
554
  phase?: unknown;
@@ -1339,11 +1344,12 @@ export default function workflows(
1339
1344
  structured: boolean,
1340
1345
  cwd: string,
1341
1346
  agentTypePrompt?: string,
1347
+ childProjectTrusted = projectTrusted,
1342
1348
  ) =>
1343
1349
  createWorkflowResources(
1344
1350
  cwd,
1345
1351
  structured ? "structured" : "plain",
1346
- projectTrusted,
1352
+ childProjectTrusted,
1347
1353
  agentTypePrompt,
1348
1354
  );
1349
1355
 
@@ -1638,6 +1644,36 @@ export default function workflows(
1638
1644
  );
1639
1645
  }
1640
1646
 
1647
+ const childTools = inheritedChildToolAllowlist(
1648
+ pi.getActiveTools(),
1649
+ agentType?.tools,
1650
+ );
1651
+ if (
1652
+ opts.working_dir !== undefined &&
1653
+ (typeof opts.working_dir !== "string" || !opts.working_dir.trim())
1654
+ ) {
1655
+ return fail(
1656
+ `agent "${label}": working_dir must be a non-empty string`,
1657
+ );
1658
+ }
1659
+ const requestedCwd = path.resolve(
1660
+ ctx.cwd,
1661
+ typeof opts.working_dir === "string" ? opts.working_dir : ".",
1662
+ );
1663
+ try {
1664
+ if (!fs.statSync(requestedCwd).isDirectory())
1665
+ throw new Error("not a directory");
1666
+ } catch {
1667
+ return fail(
1668
+ `agent "${label}": working_dir is not a directory: ${requestedCwd}`,
1669
+ );
1670
+ }
1671
+ const childProjectTrusted = resolveStandaloneChildProjectTrust({
1672
+ parentCwd: ctx.cwd,
1673
+ childCwd: requestedCwd,
1674
+ parentTrusted: projectTrusted,
1675
+ });
1676
+
1641
1677
  const explicitModel =
1642
1678
  typeof opts.model === "string" && opts.model.trim()
1643
1679
  ? opts.model.trim()
@@ -1708,11 +1744,14 @@ export default function workflows(
1708
1744
  const operatorFingerprint = operatorKey
1709
1745
  ? agentCallKey("workflow-operator", {
1710
1746
  execution: {
1747
+ cwd: requestedCwd,
1748
+ projectTrusted: childProjectTrusted,
1749
+ tools: childTools,
1711
1750
  agentType: agentType
1712
1751
  ? {
1713
1752
  name: agentType.name,
1714
1753
  body: agentType.body,
1715
- tools: agentType.tools,
1754
+ tools: childTools,
1716
1755
  }
1717
1756
  : undefined,
1718
1757
  model: model ? `${model.provider}/${model.id}` : undefined,
@@ -1732,7 +1771,7 @@ export default function workflows(
1732
1771
  const replaySafe =
1733
1772
  operatorKey === undefined &&
1734
1773
  isReplaySafeAgentCall({
1735
- tools: agentType?.tools,
1774
+ tools: agentType?.tools === undefined ? undefined : childTools,
1736
1775
  isolation: opts.isolation,
1737
1776
  });
1738
1777
  const replayLease = beginProcessReplayWorkspaceLease(replaySafe);
@@ -1744,13 +1783,14 @@ export default function workflows(
1744
1783
  try {
1745
1784
  replayResources = await getResources(
1746
1785
  effectiveSchema !== undefined,
1747
- ctx.cwd,
1786
+ requestedCwd,
1748
1787
  agentType?.body,
1788
+ childProjectTrusted,
1749
1789
  );
1750
1790
  replayIdentity = createReplayIdentity(
1751
- ctx.cwd,
1791
+ requestedCwd,
1752
1792
  replayResources.loader,
1753
- projectTrusted,
1793
+ childProjectTrusted,
1754
1794
  );
1755
1795
  } catch {
1756
1796
  // Fingerprinting is an optimization boundary. If resources cannot
@@ -1768,7 +1808,7 @@ export default function workflows(
1768
1808
  ? {
1769
1809
  name: agentType.name,
1770
1810
  body: agentType.body,
1771
- tools: agentType.tools,
1811
+ tools: childTools,
1772
1812
  }
1773
1813
  : undefined,
1774
1814
  model: model ? `${model.provider}/${model.id}` : undefined,
@@ -1925,7 +1965,7 @@ export default function workflows(
1925
1965
  );
1926
1966
  }
1927
1967
  const created = await createWorktree({
1928
- cwd: ctx.cwd,
1968
+ cwd: requestedCwd,
1929
1969
  label,
1930
1970
  id: `${details.runId}-${record.index}`,
1931
1971
  });
@@ -1937,18 +1977,19 @@ export default function workflows(
1937
1977
  worktree = created.worktree;
1938
1978
  if (!runSettled) record.worktreeBranch = worktree.branch;
1939
1979
  }
1940
- if (runSignal.aborted || runSettled) {
1941
- throw runSignal.reason instanceof Error
1942
- ? runSignal.reason
1943
- : new Error("Workflow was aborted");
1944
- }
1945
- const agentCwd = worktree?.path ?? ctx.cwd;
1980
+ const agentCwd = worktree?.path ?? requestedCwd;
1946
1981
 
1947
1982
  // Inside the try, not before it: building resources can throw
1948
1983
  // (bad settings, an unreadable skills dir), and a throw out here
1949
1984
  // would skip the finally and leak the worktree permanently —
1950
1985
  // nothing sweeps `.git/pi-worktrees/` afterwards.
1951
1986
  try {
1987
+ if (runSignal.aborted || runSettled) {
1988
+ throw runSignal.reason instanceof Error
1989
+ ? runSignal.reason
1990
+ : new Error("Workflow was aborted");
1991
+ }
1992
+
1952
1993
  let rejectResourceLoad: (() => void) | undefined;
1953
1994
  const resourceAbort = new Promise<never>((_resolve, reject) => {
1954
1995
  rejectResourceLoad = () =>
@@ -1968,6 +2009,7 @@ export default function workflows(
1968
2009
  effectiveSchema !== undefined,
1969
2010
  agentCwd,
1970
2011
  agentType?.body,
2012
+ childProjectTrusted,
1971
2013
  ),
1972
2014
  resourceAbort,
1973
2015
  ]).finally(() => {
@@ -1992,7 +2034,7 @@ export default function workflows(
1992
2034
  settingsManager: resources.settingsManager,
1993
2035
  ...(sessionManager ? { sessionManager } : {}),
1994
2036
  modelRegistry: ctx.modelRegistry,
1995
- ...(agentType?.tools ? { tools: agentType.tools } : {}),
2037
+ tools: childTools,
1996
2038
  ...(testAgentSessionFactory
1997
2039
  ? { sessionFactory: testAgentSessionFactory }
1998
2040
  : {}),
@@ -2108,9 +2150,9 @@ export default function workflows(
2108
2150
  // unfingerprintable calls always run for real.
2109
2151
  const completedIdentity = callKey
2110
2152
  ? createReplayIdentity(
2111
- ctx.cwd,
2153
+ requestedCwd,
2112
2154
  resources.loader,
2113
- projectTrusted,
2155
+ childProjectTrusted,
2114
2156
  )
2115
2157
  : undefined;
2116
2158
  const completedKey = completedIdentity
@@ -2157,7 +2199,7 @@ export default function workflows(
2157
2199
  runId: details.runId,
2158
2200
  agentIndex: record.index,
2159
2201
  agentLabel: record.label,
2160
- repoCwd: ctx.cwd,
2202
+ repoCwd: requestedCwd,
2161
2203
  worktree,
2162
2204
  });
2163
2205
  let cleanup: WorktreeCleanup;
@@ -2174,7 +2216,7 @@ export default function workflows(
2174
2216
  const reclaimer =
2175
2217
  workflowLifecycleTestHooks?.reclaimWorktree ??
2176
2218
  reclaimWorktree;
2177
- cleanup = await reclaimer(ctx.cwd, worktree).catch(
2219
+ cleanup = await reclaimer(requestedCwd, worktree).catch(
2178
2220
  (error): WorktreeCleanup => ({
2179
2221
  removed: false,
2180
2222
  branchDeleted: false,
@@ -250,8 +250,11 @@ export class AgentProgressProjection {
250
250
  snapshot(
251
251
  toolTimings: ReadonlyMap<string, ProgressToolTiming> = new Map(),
252
252
  ): AgentProgressProjectionSnapshot {
253
+ // Reserve the initial task, then spend the remaining byte budget on the
254
+ // newest evidence. Forward selection would silently discard final errors
255
+ // after enough large tool results, even below the entry-count limit.
253
256
  const selected = this.firstEntry
254
- ? [this.firstEntry, ...this.tailEntries]
257
+ ? [this.firstEntry, ...this.tailEntries.slice().reverse()]
255
258
  : [];
256
259
  const transcript: TranscriptEntry[] = [];
257
260
  let totalBytes = 0;
@@ -277,6 +280,9 @@ export class AgentProgressProjection {
277
280
  : { timestamp: entry.timestamp }),
278
281
  });
279
282
  }
283
+ // Budgeting order is not display order: keep the retained tail chronological.
284
+ const newestFirstTail = transcript.splice(1);
285
+ transcript.push(...newestFirstTail.reverse());
280
286
  if (transcript.length < this.totalEntries) {
281
287
  transcript.push({
282
288
  role: "toolResult",
@@ -50,7 +50,6 @@ import { truncateUtf8 } from "./serialization.ts";
50
50
  import { bindWorkflowToolRenderer } from "./tool-renderer.ts";
51
51
 
52
52
  const AGENT_OUTPUT_MAX_BYTES = 64 * 1024;
53
- export const MODEL_PROGRESS_TIMEOUT_MS = 45_000;
54
53
 
55
54
  export type WorkflowModel = NonNullable<ExtensionContext["model"]>;
56
55
  export type ThinkingLevel = ReturnType<ExtensionAPI["getThinkingLevel"]>;
@@ -111,8 +110,6 @@ export interface RunAgentOptions {
111
110
  replayFilesystemBoundary?: ReplayFilesystemBoundaryOptions;
112
111
  /** Test-only override for the per-tool execution timeout. */
113
112
  toolCallTimeoutMs?: number;
114
- /** Test-only override for the per-provider-turn model-progress timeout. */
115
- modelProgressTimeoutMs?: number;
116
113
  /** Test-only override for the end-to-end abort/shutdown deadline. */
117
114
  shutdownTimeoutMs?: number;
118
115
  /** Test seam for lifecycle races; production always uses createAgentSession. */
@@ -245,119 +242,6 @@ function errorText(error: unknown): string {
245
242
  );
246
243
  }
247
244
 
248
- function formatTimeout(timeoutMs: number) {
249
- return timeoutMs % 1_000 === 0
250
- ? `${timeoutMs / 1_000} seconds`
251
- : `${timeoutMs} ms`;
252
- }
253
-
254
- export function resolveModelProgressTimeoutMs(
255
- settingsManager: SettingsManager,
256
- override?: number,
257
- ) {
258
- if (override !== undefined) return override;
259
- const configured =
260
- settingsManager.getProjectSettings().httpIdleTimeoutMs ??
261
- settingsManager.getGlobalSettings().httpIdleTimeoutMs;
262
- return typeof configured === "number" && Number.isFinite(configured)
263
- ? Math.max(MODEL_PROGRESS_TIMEOUT_MS, Math.floor(configured))
264
- : MODEL_PROGRESS_TIMEOUT_MS;
265
- }
266
-
267
- /** Abort any provider turn that stops producing model-visible progress. */
268
- export function createModelProgressWatchdog(
269
- onTimeout: (error: Error) => Promise<unknown>,
270
- options: { timeoutMs?: number; model?: string } = {},
271
- ) {
272
- const timeoutMs = options.timeoutMs ?? MODEL_PROGRESS_TIMEOUT_MS;
273
- let timer: ReturnType<typeof setTimeout> | undefined;
274
- let activeTurn = false;
275
- let closed = false;
276
- let rejectTimeout!: (error: Error) => void;
277
- const timeout = new Promise<never>((_resolve, reject) => {
278
- rejectTimeout = reject;
279
- });
280
-
281
- const clear = () => {
282
- if (timer) clearTimeout(timer);
283
- timer = undefined;
284
- };
285
- const schedule = () => {
286
- clear();
287
- if (!activeTurn || closed) return;
288
- // This timer owns the awaited watchdog outcome. Keep it referenced so a
289
- // short-lived Node 22 process cannot exit with the promise still pending.
290
- timer = setTimeout(() => {
291
- timer = undefined;
292
- activeTurn = false;
293
- closed = true;
294
- const model = options.model ? ` for ${options.model}` : "";
295
- const error = new Error(
296
- `Agent provider turn${model} produced no model-visible progress for ${formatTimeout(timeoutMs)}; the provider request may be stalled. Retry the workflow.`,
297
- );
298
- rejectTimeout(error);
299
- try {
300
- void onTimeout(error).catch(() => {});
301
- } catch {
302
- // The timeout result remains authoritative even if abort throws before
303
- // returning its promise; bounded shutdown below gets another chance.
304
- }
305
- }, timeoutMs);
306
- };
307
- const armTurn = () => {
308
- if (closed) return;
309
- activeTurn = true;
310
- schedule();
311
- };
312
- const markProgress = () => {
313
- if (!activeTurn || closed) return;
314
- schedule();
315
- };
316
- const completeTurn = () => {
317
- activeTurn = false;
318
- clear();
319
- };
320
- const cancel = () => {
321
- closed = true;
322
- activeTurn = false;
323
- clear();
324
- };
325
-
326
- return {
327
- armTurn,
328
- markProgress,
329
- completeTurn,
330
- cancel,
331
- async waitFor<T>(operation: Promise<T>) {
332
- try {
333
- return await Promise.race([operation, timeout]);
334
- } finally {
335
- cancel();
336
- }
337
- },
338
- };
339
- }
340
-
341
- function isModelVisibleProgress(event: AgentSessionEvent) {
342
- if (event.type !== "message_update" || event.message.role !== "assistant") {
343
- return false;
344
- }
345
- // Raw transport heartbeats never become AgentSession events. Empty stream,
346
- // text, and thinking starts likewise cannot keep a provider turn alive.
347
- const update = event.assistantMessageEvent;
348
- if (
349
- update.type === "text_delta" ||
350
- update.type === "thinking_delta" ||
351
- update.type === "toolcall_delta"
352
- ) {
353
- return update.delta.length > 0;
354
- }
355
- if (update.type === "text_end" || update.type === "thinking_end") {
356
- return update.content.length > 0;
357
- }
358
- return update.type === "toolcall_start" || update.type === "toolcall_end";
359
- }
360
-
361
245
  export async function runAgent(
362
246
  options: RunAgentOptions,
363
247
  ): Promise<AgentOutcome> {
@@ -367,8 +251,6 @@ export async function runAgent(
367
251
  let session: AgentSession | undefined;
368
252
  let unsubscribeToolGuards: (() => void) | undefined;
369
253
  let aborted = false;
370
- let terminalCause: "abort" | "model-progress-timeout" | undefined;
371
- let modelProgressTimeoutMessage: string | undefined;
372
254
  let abortOperation: Promise<unknown> | undefined;
373
255
  let rejectForAbort: ((error: Error) => void) | undefined;
374
256
  let rejectForProjectionFailure: ((error: Error) => void) | undefined;
@@ -389,7 +271,6 @@ export async function runAgent(
389
271
  const onAbort = () => {
390
272
  if (aborted) return;
391
273
  aborted = true;
392
- terminalCause ??= "abort";
393
274
  if (session) {
394
275
  try {
395
276
  abortOperation ??= session.abort();
@@ -594,10 +475,6 @@ export async function runAgent(
594
475
  });
595
476
  };
596
477
 
597
- let armModelProgress = () => {};
598
- let markModelProgress = () => {};
599
- let completeModelTurn = () => {};
600
- let cancelModelProgressWatchdog = () => {};
601
478
  let compactionReconcileQueued = false;
602
479
  const queueCompactionReconcile = () => {
603
480
  if (compactionReconcileQueued) return;
@@ -625,7 +502,6 @@ export async function runAgent(
625
502
  };
626
503
  const unsubscribe = childSession.subscribe((event) => {
627
504
  if (settled) return;
628
- if (event.type === "turn_start") armModelProgress();
629
505
  if (event.type === "tool_execution_start") {
630
506
  toolRenderer.start(
631
507
  event.toolCallId,
@@ -648,10 +524,6 @@ export async function runAgent(
648
524
  event.isError,
649
525
  );
650
526
  }
651
- if (isModelVisibleProgress(event)) markModelProgress();
652
- if (event.type === "message_end" && event.message.role === "assistant") {
653
- completeModelTurn();
654
- }
655
527
  if (event.type === "message_end") {
656
528
  assistantSettlement = observeAssistantSettlement(
657
529
  assistantSettlement,
@@ -686,32 +558,10 @@ export async function runAgent(
686
558
  captureToolRenderData(childSession.messages);
687
559
  snapshotProjection();
688
560
  if (!aborted) {
689
- const watchdog = createModelProgressWatchdog(
690
- (error) => {
691
- terminalCause ??= "model-progress-timeout";
692
- if (terminalCause === "model-progress-timeout") {
693
- modelProgressTimeoutMessage ??= error.message;
694
- }
695
- abortOperation ??= childSession.abort();
696
- void abortOperation.catch(() => {});
697
- return abortOperation;
698
- },
699
- {
700
- timeoutMs: resolveModelProgressTimeoutMs(
701
- options.settingsManager,
702
- options.modelProgressTimeoutMs,
703
- ),
704
- model: modelId,
705
- },
706
- );
707
- armModelProgress = watchdog.armTurn;
708
- markModelProgress = watchdog.markProgress;
709
- completeModelTurn = watchdog.completeTurn;
710
- cancelModelProgressWatchdog = watchdog.cancel;
561
+ // Pi owns transport liveness and retries. Quiet model output is not
562
+ // evidence of a stalled request (thinking and retry backoff can be silent).
711
563
  await Promise.race([
712
- watchdog.waitFor(
713
- childSession.prompt(buildWorkflowAgentPrompt(options.prompt)),
714
- ),
564
+ childSession.prompt(buildWorkflowAgentPrompt(options.prompt)),
715
565
  abortRace,
716
566
  projectionFailureRace,
717
567
  ]);
@@ -719,7 +569,6 @@ export async function runAgent(
719
569
  } catch (error) {
720
570
  promptErrorMessage ??= errorText(error);
721
571
  } finally {
722
- cancelModelProgressWatchdog();
723
572
  options.signal?.removeEventListener("abort", onAbort);
724
573
  settled = true;
725
574
  unsubscribe();
@@ -758,11 +607,7 @@ export async function runAgent(
758
607
  ? `Cleanup failed: ${cleanupErrors.join("; ")}`
759
608
  : undefined;
760
609
 
761
- if (
762
- terminalCause === "abort" ||
763
- (terminalCause === undefined &&
764
- assistantSettlement?.stopReason === "aborted")
765
- ) {
610
+ if (aborted || assistantSettlement?.stopReason === "aborted") {
766
611
  return {
767
612
  ok: false,
768
613
  output,
@@ -779,9 +624,7 @@ export async function runAgent(
779
624
  }
780
625
 
781
626
  const failureMessage =
782
- (terminalCause === "model-progress-timeout"
783
- ? modelProgressTimeoutMessage
784
- : agentFailureMessage(assistantSettlement, promptErrorMessage)) ??
627
+ agentFailureMessage(assistantSettlement, promptErrorMessage) ??
785
628
  cleanupError;
786
629
  if (failureMessage !== undefined) {
787
630
  return {
@@ -35,6 +35,7 @@ export interface SandboxAgentOptions {
35
35
  provider?: unknown;
36
36
  effort?: unknown;
37
37
  isolation?: unknown;
38
+ working_dir?: unknown;
38
39
  operator?: unknown;
39
40
  inputs?: unknown;
40
41
  }
@@ -110,6 +111,9 @@ function sanitizeAgentOptions(value: unknown): SandboxAgentOptions {
110
111
  ...(value.provider !== undefined ? { provider: value.provider } : {}),
111
112
  ...(value.effort !== undefined ? { effort: value.effort } : {}),
112
113
  ...(value.isolation !== undefined ? { isolation: value.isolation } : {}),
114
+ ...(value.working_dir !== undefined
115
+ ? { working_dir: value.working_dir }
116
+ : {}),
113
117
  ...(value.operator !== undefined ? { operator: value.operator } : {}),
114
118
  ...(value.inputs !== undefined ? { inputs: value.inputs } : {}),
115
119
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tt-a1i/openpi",
3
- "version": "0.6.0",
3
+ "version": "0.7.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
5
  "license": "MIT",
6
6
  "author": "tt-a1i",
@@ -58,21 +58,43 @@
58
58
  "acorn": "^8.17.0",
59
59
  "effect": "^4.0.0-beta.99",
60
60
  "jiti": "2.7.0",
61
- "marked": "^18.0.5",
62
61
  "undici": "8.9.0"
63
62
  },
64
63
  "devDependencies": {
64
+ "@astryxdesign/core": "0.5.2",
65
+ "@astryxdesign/theme-neutral": "0.5.2",
66
+ "@axe-core/playwright": "4.13.0",
65
67
  "@biomejs/biome": "2.5.8",
66
68
  "@earendil-works/pi-ai": "^0.85.1",
67
69
  "@earendil-works/pi-coding-agent": "^0.85.1",
68
70
  "@earendil-works/pi-tui": "^0.85.1",
69
71
  "@effect/tsgo": "^0.24.2",
70
72
  "@effect/vitest": "^4.0.0-beta.99",
73
+ "@playwright/test": "1.62.1",
74
+ "@stylexjs/stylex": "0.19.0",
75
+ "@tailwindcss/vite": "4.3.3",
76
+ "@testing-library/react": "16.3.3",
71
77
  "@types/node": "^26.1.1",
78
+ "@types/react": "19.2.18",
79
+ "@types/react-dom": "19.2.5",
80
+ "@vitejs/plugin-react": "6.1.1",
81
+ "eventsource-parser": "4.1.0",
82
+ "i18next": "26.4.1",
83
+ "jsdom": "30.0.1",
84
+ "lucide-react": "1.39.0",
85
+ "react": "19.2.8",
86
+ "react-dom": "19.2.8",
87
+ "react-i18next": "17.0.13",
88
+ "react-markdown": "10.1.0",
89
+ "rehype-sanitize": "6.0.0",
90
+ "remark-breaks": "4.0.0",
91
+ "remark-gfm": "4.0.1",
92
+ "tailwindcss": "4.3.3",
72
93
  "typebox": "^1.3.6",
73
94
  "typescript": "^7.0.2",
74
95
  "vite": "^8.2.0",
75
- "vitest": "4.1.10"
96
+ "vitest": "4.1.10",
97
+ "zustand": "5.0.15"
76
98
  },
77
99
  "peerDependencies": {
78
100
  "@earendil-works/pi-ai": ">=0.85.1",
@@ -98,7 +120,9 @@
98
120
  "dev:web": "node --experimental-strip-types scripts/dev-web.mjs",
99
121
  "dev:web:ui": "vite --config web/vite.config.mjs --host 127.0.0.1",
100
122
  "dev:web:backend": "node scripts/dev-web-backend.mjs",
101
- "check:web": "node --check scripts/dev-web.mjs && node --check web/vite.config.mjs && node --check web/ui/app.js",
123
+ "build:web": "vite build --config web/vite.config.mjs",
124
+ "check:web": "node --check scripts/dev-web.mjs && node --check web/vite.config.mjs && tsc --noEmit --project web/ui/tsconfig.json && bun run build:web",
125
+ "test:web:e2e": "bun run build:web && playwright test --config tests/web/playwright.config.ts",
102
126
  "benchmark:workflow-child-startup": "node --experimental-strip-types scripts/benchmark-workflow-child-startup.mjs",
103
127
  "test": "node scripts/run-tests.mjs",
104
128
  "provenance": "node scripts/provenance.mjs"
@@ -32,7 +32,7 @@ file:line references. You cannot modify files — do not attempt to.
32
32
  | ------------------ | -------- | ------------------------------------------------------------------------------------ |
33
33
  | `name` | yes | `[a-z0-9-]`, ≤64 chars, and must equal the filename stem. |
34
34
  | `description` | yes | Shown to the parent model when it picks a type. ≤1024 chars. |
35
- | `tools` | no | Tool allowlist. **Omit to inherit the normal tool set.** |
35
+ | `tools` | no | Tool allowlist. **Omit to inherit active parent child-eligible tools.** |
36
36
  | `model` | no | `provider/model-id`, or a bare id resolved against the current provider; ≤256 chars. |
37
37
  | `reasoning_effort` | no | `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. |
38
38
  | body | no | Appended to the child's system prompt. ≤16384 chars. |
@@ -50,16 +50,15 @@ file with the same name.
50
50
 
51
51
  | Role | Tools | Relative effort guidance | Purpose |
52
52
  | ------------- | ------------------------------------------------------------------- | ------------------------ | ------------------------------------------------------ |
53
- | `explorer` | `read grep find ls fd rg git_show git_diff git_log` | Moderate | Read-only codebase tracing; increase for harder tasks. |
54
- | `implementer` | `read bash edit write grep find ls fd rg git_show git_diff git_log` | Medium-high | Focused implementation; adjust for scope and risk. |
55
- | `reviewer` | `read grep find ls fd rg git_show git_diff git_log` | High | Read-only correctness, safety, and regression review. |
56
- | `advisor` | `read grep find ls fd rg git_show git_diff git_log` | High | Deep read-only analysis and technical advice. |
53
+ | `explorer` | Active parent child-eligible tools | Moderate | Read-only codebase tracing; increase for harder tasks. |
54
+ | `implementer` | Active parent child-eligible tools | Medium-high | Focused implementation; adjust for scope and risk. |
55
+ | `reviewer` | Active parent child-eligible tools | High | Read-only correctness, safety, and regression review. |
56
+ | `advisor` | Active parent child-eligible tools | High | Deep read-only analysis and technical advice. |
57
57
 
58
58
  These are relative selection hints, not fixed Pi thinking levels. Built-ins set
59
59
  no model or reasoning-effort default. An explicit user requirement takes
60
60
  priority; otherwise the parent model chooses from levels supported by the
61
- resolved child model according to the role and task difficulty. Their tool
62
- allowlists still intersect with plan mode and the child denylist.
61
+ resolved child model according to the role and task difficulty. Custom role allowlists intersect with active parent tools, Plan Mode, and the child denylist. Built-in read-only task guidance is a role instruction, not a filesystem sandbox. Existing custom role files are never widened automatically.
63
62
 
64
63
  ## Discovery
65
64
 
@@ -10,7 +10,7 @@ The tool definitions are canonical for parameters, limits, model syntax, isolati
10
10
  - Delegate substantial independent work, not a lookup or edit the parent can do directly.
11
11
  - Give the child a standalone prompt with paths, constraints, relevant context, and the expected report; it cannot see the parent conversation or ask the user.
12
12
  - Inherit the parent model by default. When choosing the child's reasoning effort, honor an explicit user requirement first; otherwise use the selected role's relative guidance and the task's difficulty, choosing from levels supported by the resolved child model.
13
- - Prefer a matching agent type when one exists; its tool restriction is enforced. Model precedence is explicit spawn override, selected type-file model, configured built-in role model, then parent model. Reasoning precedence is explicit spawn override, selected type default, then parent effort. Types live in `~/.pi/agent/agents/*.md` and, for trusted projects, `.pi/agents/*.md`; see [Agent types](REFERENCE.md).
13
+ - Prefer a matching agent type when one exists; built-ins inherit active parent tools, while an explicit custom tool list is enforced as a narrowing restriction. Model precedence is explicit spawn override, selected type-file model, configured built-in role model, then parent model. Reasoning precedence is explicit spawn override, selected type default, then parent effort. Types live in `~/.pi/agent/agents/*.md` and, for trusted projects, `.pi/agents/*.md`; see [Agent types](REFERENCE.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. In an interactive session, if none remains, tell the user the child is still running and end the turn; automatic result delivery will re-invoke the parent when it settles. Do not block merely because the next step depends on the result or because there is nothing else to do. Use `subagent_wait` only when the user explicitly asks to keep the current response open for the result, or when non-interactive automation must return it in the same invocation.
16
16
  - Use optional `output_schema` when downstream work needs a machine-validated result rather than prose. The child then receives one terminating `structured_output` tool, and the run fails if it finishes without submitting a matching value. Keep schemas small and task-specific; the validated JSON is delivered to the parent and preserved in a private content-addressed artifact. Omit the option for ordinary text reports.
@@ -14,8 +14,10 @@ The `workflow` script is an async JavaScript function body executed in a restric
14
14
 
15
15
  `await agent(prompt, options)` runs one child and always resolves to `{ ok, output, structured?, ref?, acceptance?, acceptanceWarning?, error? }`. Check `ok` before reading output. Children receive normal trust-aware resources but cannot recursively orchestrate or ask the user.
16
16
 
17
- Useful options include `agent_type`, `label`, `phase`, `schema`, `model`, `provider`, `effort`, `isolation`, `operator`, and `inputs`. The legacy `acceptance` option remains readable only during the 0.x migration window described below.
17
+ Useful options include `agent_type`, `label`, `phase`, `schema`, `model`, `provider`, `effort`, `working_dir`, `isolation`, `operator`, and `inputs`. The legacy `acceptance` option remains readable only during the 0.x migration window described below.
18
18
 
19
+ - Set `working_dir` when tools must run in another repository. Relative paths resolve against the parent cwd; prompt text alone does not change it. The directory must exist. Project resource trust is checked for the target independently.
20
+ - Built-in roles inherit currently active parent child-eligible tools, including shell/network when available. Explicit custom tool lists only narrow this surface. Built-ins with inherited tools execute for real on resume; custom bounded read-only calls retain the replay filesystem boundary below.
19
21
  - 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
22
  - `schema` validates structured output. Use it whenever later workflow logic branches on fields.
21
23
  - `acceptance` is deprecated since OpenPI 0.5 and scheduled for removal in 1.0. Compatibility calls still return the child-authored ledger with `authority: "model-self-attestation"` and a migration warning, but it never determines `ok`. Use ordinary `schema` for findings, then let the parent evaluate them alongside runtime-observed exit codes, test receipts, file fingerprints, and tool results. Old DSL, journals, and artifacts remain readable during 0.x.