@evandu/super-pi 0.1.141 → 0.1.143

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 (50) hide show
  1. package/agents/super-pi-task/core.ts +55 -27
  2. package/agents/super-pi-task/index.ts +76 -4
  3. package/dist/.super-pi-check-inputs.json +2 -2
  4. package/dist/client/.super-pi-client-sources.json +7 -8
  5. package/dist/client/assets/{CodeMirrorEditor-Dm5IDKy9.js → CodeMirrorEditor-CXDQ0ikN.js} +2 -2
  6. package/dist/client/assets/{dist-BGaKp7BG.js → dist-BK_KWsQr.js} +1 -1
  7. package/dist/client/assets/{dist-CN1j5LO2.js → dist-BKsr7wc2.js} +1 -1
  8. package/dist/client/assets/{dist-C7UQGfmz.js → dist-BXQWaRci.js} +1 -1
  9. package/dist/client/assets/{dist-BvCNiV_z.js → dist-B_uWAP2-.js} +1 -1
  10. package/dist/client/assets/{dist-CNgQgdyS.js → dist-BwwacXyS.js} +1 -1
  11. package/dist/client/assets/{dist-GAPsnCM8.js → dist-CWd7iD1S.js} +1 -1
  12. package/dist/client/assets/{dist-DWj0wg8M.js → dist-CbjfwVs-.js} +1 -1
  13. package/dist/client/assets/{dist-PQZBepmy.js → dist-CoEavaUe.js} +1 -1
  14. package/dist/client/assets/{dist-BqXU6S8h.js → dist-D60BDUMe.js} +1 -1
  15. package/dist/client/assets/{dist-LCDxbTTI.js → dist-DBbQHcwG.js} +1 -1
  16. package/dist/client/assets/{dist-DtO-7WEZ.js → dist-DVEBhaUf.js} +1 -1
  17. package/dist/client/assets/{dist-D0eIQjoI.js → dist-Db6KH9Qv.js} +1 -1
  18. package/dist/client/assets/{dist-BMbwbGH9.js → dist-Dsj0Q8gO.js} +1 -1
  19. package/dist/client/assets/{dist-4baaMdqn.js → dist-H0dUhAgy.js} +1 -1
  20. package/dist/client/assets/{dist-D0-9TI_k.js → dist-MZSBP_1m.js} +1 -1
  21. package/dist/client/assets/{dist-B-edBMDa.js → dist-PNt22XLU.js} +1 -1
  22. package/dist/client/assets/{dist-M3BFsjal.js → dist-S5UwGLvb.js} +1 -1
  23. package/dist/client/assets/{dist-F7KOq8Xm.js → dist-_jioFcsD.js} +1 -1
  24. package/dist/client/assets/{dist-CSDLiQKt.js → dist-hmP8KP8-.js} +1 -1
  25. package/dist/client/assets/{dist-D7YZUqfA.js → dist-jV9tqAoC.js} +1 -1
  26. package/dist/client/assets/{dist-CMDW84Eh.js → dist-oqETQw0G.js} +1 -1
  27. package/dist/client/assets/{dist-DQWPdnen.js → dist-oz2WOWPd.js} +1 -1
  28. package/dist/client/assets/{index-BJIfLtDs.css → index-6h_u-X47.css} +1 -1
  29. package/dist/client/assets/{index-DHsjaI7m.js → index-iSnWwgJ-.js} +26 -26
  30. package/dist/client/index.html +2 -2
  31. package/dist/server/application/conversation-service.d.ts +2 -1
  32. package/dist/server/application/conversation-service.js +25 -7
  33. package/dist/server/application/run-service.d.ts +15 -0
  34. package/dist/server/application/run-service.js +59 -1
  35. package/dist/server/application/session-orchestrator-service.d.ts +9 -1
  36. package/dist/server/application/session-orchestrator-service.js +79 -10
  37. package/dist/server/http-server.js +18 -11
  38. package/dist/server/runtime.js +7 -6
  39. package/dist/server/store.d.ts +12 -1
  40. package/dist/server/store.js +77 -4
  41. package/dist/server/task-tool.d.ts +4 -2
  42. package/dist/server/task-tool.js +6 -3
  43. package/dist/server/time-zone.d.ts +20 -0
  44. package/dist/server/time-zone.js +32 -0
  45. package/dist/server/types.d.ts +12 -0
  46. package/dist/server/workflow-engine.d.ts +11 -95
  47. package/dist/server/workflow-engine.js +122 -134
  48. package/dist/server/workflow.d.ts +73 -31
  49. package/dist/server/workflow.js +112 -30
  50. package/package.json +17 -2
@@ -71,6 +71,8 @@ export declare const TASK_EXEC_ENDPOINT_ENV = "SUPER_PI_EXEC_ENDPOINT";
71
71
  export declare const TASK_ROSTER_ENV = "SUPER_PI_TASK_ROSTER";
72
72
  /** The conversation the dispatch belongs to. The server resolves the workspace from this itself. */
73
73
  export declare const TASK_CONVERSATION_ENV = "SUPER_PI_TASK_CONVERSATION";
74
+ /** The conversation that spawned this one. Empty means a top-level conversation. */
75
+ export declare const PARENT_CONVERSATION_ENV = "SUPER_PI_PARENT_CONVERSATION";
74
76
  /** How many task hops deep this process already is. A primary is `0`. See `DEFAULT_SUBAGENT_DEPTH`. */
75
77
  export declare const TASK_DEPTH_ENV = "SUPER_PI_TASK_DEPTH";
76
78
  /**
@@ -255,9 +257,9 @@ export declare const SESSION_STATUS_TOOL_NAME = "session_status";
255
257
  export declare const SESSION_DISPATCH_TOOL_NAME = "session_dispatch";
256
258
  export declare const SESSION_STOP_TOOL_NAME = "session_stop";
257
259
  /** Model-facing description for session_spawn. */
258
- export declare const SESSION_SPAWN_DESCRIPTION = "Create a new sibling conversation in the current workspace and immediately dispatch its initial task.\n\nDIFFERENCE FROM THE 'task' TOOL:\n- The 'task' tool delegates to a transient, one-shot subagent process (scout, critic, patcher) that runs once, returns its final text, and terminates. It has no conversation history and does not appear in the workspace sidebar.\n- 'session_spawn' creates a real, persistent sibling CONVERSATION in Super Pi. It appears in the sidebar, retains multi-turn message history, can receive follow-up messages via session_dispatch, can be inspected via session_status, and can be stopped via session_stop. The user can also switch to it in the Web UI.\n\nWHEN TO USE THIS:\n- Long-running, multi-phase tracks of work that require ongoing context, multiple turns of dialogue, or independent user visibility.\n- Parallel work streams (e.g., Track A: backend refactor, Track B: frontend component update) where both need full conversational capabilities.\n- When the user asks you to act as an orchestrator/manager who delegates to multiple sessions.\n\nWHEN NOT TO USE THIS:\n- For one-off, focused delegation with a single answer: use 'task' (with scout/critic/patcher). It is faster, cheaper, and returns directly to your transcript.\n- For running shell commands or build watchers: use 'run_async'.\n- For a linear multi-step subagent pipeline: use 'workflow'.\n\nUsage notes:\n1. Spawning is ASYNCHRONOUS by default: it creates the conversation, enqueues the initial task, and returns the new conversationId immediately. The sibling session runs in the background.\n2. The initial task should provide clear context and goals since the new session starts with a fresh conversation history.\n3. Use session_status with the returned conversationId to check progress, or session_dispatch to send further messages.";
260
+ export declare const SESSION_SPAWN_DESCRIPTION = "Create a new sibling conversation in the current workspace and immediately dispatch its initial task.\n\nDIFFERENCE FROM THE 'task' TOOL:\n- The 'task' tool delegates to a transient, one-shot subagent process (scout, critic, patcher) that runs once, returns its final text, and terminates. It has no conversation history and does not appear in the workspace sidebar.\n- 'session_spawn' creates a real, persistent sibling CONVERSATION in Super Pi. It appears in the sidebar, retains multi-turn message history, can receive follow-up messages via session_dispatch, can be inspected via session_status, and can be stopped via session_stop. The user can also switch to it in the Web UI.\n\nWHEN TO USE THIS:\n- Long-running, multi-phase tracks of work that require ongoing context, multiple turns of dialogue, or independent user visibility.\n- Parallel work streams (e.g., Track A: backend refactor, Track B: frontend component update) where both need full conversational capabilities.\n- When the user asks you to act as an orchestrator/manager who delegates to multiple sessions.\n\nWHEN NOT TO USE THIS:\n- For one-off, focused delegation with a single answer: use 'task' (with scout/critic/patcher). It is faster, cheaper, and returns directly to your transcript.\n- For running shell commands or build watchers: use 'run_async'.\n- For a linear multi-step subagent pipeline: use 'workflow'.\n\nUsage notes:\n1. Spawning is ASYNCHRONOUS by default: it creates the conversation, enqueues the initial task, and returns the new conversationId immediately. The sibling session runs in the background.\n2. The initial task should provide clear context and goals since the new session starts with a fresh conversation history.\n3. Use session_status with the returned conversationId to check progress, or session_dispatch to send further messages.\n4. The new session reports completion to its DIRECT parent automatically. 'parentConversationId' optionally selects that parent; by default it is the calling session.";
259
261
  /** Model-facing description for session_status. */
260
- export declare const SESSION_STATUS_DESCRIPTION = "Inspect the progress, run state, and latest activity of sibling conversations created by the orchestrator.\n\nWHEN TO USE THIS:\n- To check whether a spawned sibling conversation is still running, has finished (idle), is stopped, or encountered an error.\n- To read the latest assistant progress snippet without bloating your context with the session's entire raw message transcript.\n- To list all sibling conversations under your coordination when conversationId is omitted.\n\nUsage notes:\n1. When 'conversationId' is provided: returns a compact summary for that specific session, including its run status ('idle', 'running', 'waiting', 'stopped', 'failed'), messageCount, turnCount, lastAssistantSnippet, lastUserSnippet, activeTool, and error (if any).\n2. When 'conversationId' is omitted: returns a summary list of ALL sibling conversations spawned by this orchestrator session.\n3. This tool never returns full transcripts; it returns concise summaries and snippets to keep your context window lean.";
262
+ export declare const SESSION_STATUS_DESCRIPTION = "Inspect the progress, run state, and latest activity of sibling conversations created by the orchestrator.\n\nWHEN TO USE THIS:\n- To check whether a spawned sibling conversation is still running, has finished (idle), is stopped, or encountered an error.\n- To read the latest assistant progress snippet without bloating your context with the session's entire raw message transcript.\n- To list all sibling conversations under your coordination when conversationId is omitted.\n\nUsage notes:\n1. When 'conversationId' is provided: returns a compact summary for that specific session, including its run status ('idle', 'running', 'waiting', 'stopped', 'failed'), messageCount, turnCount, lastAssistantSnippet, lastUserSnippet, activeTool, and error (if any).\n2. When 'conversationId' is omitted: returns a summary list of sibling conversations whose direct parent is 'parentConversationId' (the calling session by default).\n3. By default it never returns full transcripts; concise summaries keep your context window lean. Set 'includeOutput' with a conversationId to read the final output from its result file, capped to the last 64Ki characters with an explicit truncation notice.";
261
263
  /** Model-facing description for session_dispatch. */
262
264
  export declare const SESSION_DISPATCH_DESCRIPTION = "Send a message, follow-up instructions, or steering guidance to an existing sibling conversation.\n\nWHEN TO USE THIS:\n- To follow up with a sibling session after reading its progress with session_status.\n- To give feedback, provide additional requirements, or answer a question from a sibling session.\n- To kick off a new turn on an idle or stopped sibling session.\n\nUsage notes:\n1. 'conversationId' is required and must identify an existing sibling conversation.\n2. 'content' is the text instruction sent to that session, just as if typed by a user in that session's chat.\n3. Dispatch is ASYNCHRONOUS by default: it enqueues the message into the session's queue and returns immediately.\n4. If the sibling session is currently busy running a turn, the dispatched message is placed in its waiting queue and processed when the active turn completes.";
263
265
  /** Model-facing description for session_stop. */
@@ -70,6 +70,8 @@ export const TASK_EXEC_ENDPOINT_ENV = "SUPER_PI_EXEC_ENDPOINT";
70
70
  export const TASK_ROSTER_ENV = "SUPER_PI_TASK_ROSTER";
71
71
  /** The conversation the dispatch belongs to. The server resolves the workspace from this itself. */
72
72
  export const TASK_CONVERSATION_ENV = "SUPER_PI_TASK_CONVERSATION";
73
+ /** The conversation that spawned this one. Empty means a top-level conversation. */
74
+ export const PARENT_CONVERSATION_ENV = "SUPER_PI_PARENT_CONVERSATION";
73
75
  /** How many task hops deep this process already is. A primary is `0`. See `DEFAULT_SUBAGENT_DEPTH`. */
74
76
  export const TASK_DEPTH_ENV = "SUPER_PI_TASK_DEPTH";
75
77
  /**
@@ -379,7 +381,8 @@ WHEN NOT TO USE THIS:
379
381
  Usage notes:
380
382
  1. Spawning is ASYNCHRONOUS by default: it creates the conversation, enqueues the initial task, and returns the new conversationId immediately. The sibling session runs in the background.
381
383
  2. The initial task should provide clear context and goals since the new session starts with a fresh conversation history.
382
- 3. Use session_status with the returned conversationId to check progress, or session_dispatch to send further messages.`;
384
+ 3. Use session_status with the returned conversationId to check progress, or session_dispatch to send further messages.
385
+ 4. The new session reports completion to its DIRECT parent automatically. 'parentConversationId' optionally selects that parent; by default it is the calling session.`;
383
386
  /** Model-facing description for session_status. */
384
387
  export const SESSION_STATUS_DESCRIPTION = `Inspect the progress, run state, and latest activity of sibling conversations created by the orchestrator.
385
388
 
@@ -390,8 +393,8 @@ WHEN TO USE THIS:
390
393
 
391
394
  Usage notes:
392
395
  1. When 'conversationId' is provided: returns a compact summary for that specific session, including its run status ('idle', 'running', 'waiting', 'stopped', 'failed'), messageCount, turnCount, lastAssistantSnippet, lastUserSnippet, activeTool, and error (if any).
393
- 2. When 'conversationId' is omitted: returns a summary list of ALL sibling conversations spawned by this orchestrator session.
394
- 3. This tool never returns full transcripts; it returns concise summaries and snippets to keep your context window lean.`;
396
+ 2. When 'conversationId' is omitted: returns a summary list of sibling conversations whose direct parent is 'parentConversationId' (the calling session by default).
397
+ 3. By default it never returns full transcripts; concise summaries keep your context window lean. Set 'includeOutput' with a conversationId to read the final output from its result file, capped to the last 64Ki characters with an explicit truncation notice.`;
395
398
  /** Model-facing description for session_dispatch. */
396
399
  export const SESSION_DISPATCH_DESCRIPTION = `Send a message, follow-up instructions, or steering guidance to an existing sibling conversation.
397
400
 
@@ -0,0 +1,20 @@
1
+ /**
2
+ * THE CLOCK THIS PROCESS SCHEDULES BY.
3
+ *
4
+ * A scheduled task's 「每天 09:00」 is a WALL CLOCK, and the wall clock it means is the one this
5
+ * process runs on - the machine holding the server. The browser that reads the result may be on a
6
+ * different clock entirely, so a moment computed here and rendered there says one thing in one place
7
+ * and another thing in the other.
8
+ *
9
+ * The name is the IANA zone rather than an offset, because an offset is ambiguous twice a year: it
10
+ * does not say whether 「09:00」 is the summer or the winter offset, and a task that drifts by an
11
+ * hour twice a year is the kind of bug nobody reports.
12
+ */
13
+ export declare function serverTimeZone(): string;
14
+ /**
15
+ * The zone's short name for the moment, the way a person would say it: 「GMT+8」, 「UTC」.
16
+ *
17
+ * Used in the panel next to a time, so the reader can tell WHICH clock they are looking at without
18
+ * having to know what 「Asia/Shanghai」 means.
19
+ */
20
+ export declare function timeZoneLabel(at?: number): string;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * THE CLOCK THIS PROCESS SCHEDULES BY.
3
+ *
4
+ * A scheduled task's 「每天 09:00」 is a WALL CLOCK, and the wall clock it means is the one this
5
+ * process runs on - the machine holding the server. The browser that reads the result may be on a
6
+ * different clock entirely, so a moment computed here and rendered there says one thing in one place
7
+ * and another thing in the other.
8
+ *
9
+ * The name is the IANA zone rather than an offset, because an offset is ambiguous twice a year: it
10
+ * does not say whether 「09:00」 is the summer or the winter offset, and a task that drifts by an
11
+ * hour twice a year is the kind of bug nobody reports.
12
+ */
13
+ export function serverTimeZone() {
14
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
15
+ }
16
+ /**
17
+ * The zone's short name for the moment, the way a person would say it: 「GMT+8」, 「UTC」.
18
+ *
19
+ * Used in the panel next to a time, so the reader can tell WHICH clock they are looking at without
20
+ * having to know what 「Asia/Shanghai」 means.
21
+ */
22
+ export function timeZoneLabel(at = Date.now()) {
23
+ const zone = serverTimeZone();
24
+ try {
25
+ const parts = new Intl.DateTimeFormat("en-US", { timeZone: zone, timeZoneName: "shortOffset" }).formatToParts(at);
26
+ const name = parts.find((part) => part.type === "timeZoneName")?.value;
27
+ return name ?? zone;
28
+ }
29
+ catch {
30
+ return zone;
31
+ }
32
+ }
@@ -503,6 +503,9 @@ export type Conversation = {
503
503
  * not what is true right now.
504
504
  */
505
505
  modelNotice?: string;
506
+ /** Why this conversation started on Pi's default agent instead of the workspace's deleted default.
507
+ * Written once at birth, like modelNotice, and rendered verbatim. */
508
+ agentNotice?: string;
506
509
  /** Absent means "Pi's own default", which is why a conversation created before the picker existed still works. */
507
510
  model?: ModelSelection;
508
511
  thinkingLevel?: string;
@@ -885,6 +888,12 @@ export type AppState = {
885
888
  * "nothing collapsed", which is the correct reading of a preference that did not exist yet.
886
889
  */
887
890
  collapsedConversationIds?: string[];
891
+ /**
892
+ * THE CLOCK THIS SERVER SCHEDULES BY. A scheduled task's 「每天 09:00」 is the SERVER's wall clock,
893
+ * and the panel has to say so, because the browser rendering 「下次」 may be on a different clock
894
+ * entirely. Without this the two lines in one row quietly mean two different things.
895
+ */
896
+ serverTimeZone?: string;
888
897
  };
889
898
  export type TodoItem = {
890
899
  id?: string;
@@ -914,6 +923,9 @@ export type OrchestratorSessionSummary = {
914
923
  updatedAt: string;
915
924
  createdAt?: number;
916
925
  lastProgress?: string;
926
+ output?: string;
927
+ outputPath?: string;
928
+ outputTruncated?: boolean;
917
929
  };
918
930
  /**
919
931
  * What Pi reports about ITS OWN queue, forwarded to the browser as it arrives.
@@ -3,129 +3,45 @@ import type { Store } from "./store.js";
3
3
  import type { ModelSelection, ServerEvent } from "./types.js";
4
4
  import { type WorkflowPlan, type WorkflowRun } from "./workflow.js";
5
5
  /**
6
- * Walking an orchestrated plan: the half of the feature that DECIDES anything.
7
- *
8
- * WHY THIS FILE HOLDS NO SPAWNING, NO STREAMING AND NO PROCESS TEARDOWN. Every one of those already
9
- * exists and is already proved - `subagents.ts` spawns and drains, `subagent-runs.ts` records and
10
- * publishes. The engine's whole job is to call `dispatch` in the right ORDER and decide what to do when
11
- * a step does not succeed, and `SubagentRunRecorder.dispatch` is the seam that makes that possible: it
12
- * ALWAYS RESOLVES with a settled record, including for a dispatch that never started (`subagent-runs.ts:159`).
13
- * So an engine step is `await` one call, read `status` off the record, and move on. There is no error
14
- * parsing here, no exception handling around a spawn, and no way for a child to be left running because
15
- * the thing above it threw - which is the entire reason this feature is small.
16
- *
17
- * WHAT THE ENGINE IS NOT ALLOWED TO DO, stated as the two rules it is built around:
18
- *
19
- * 1. IT DOES NOT DECIDE WHAT A STEP MEANS. `role` is written through to the run record and read by
20
- * nobody in this process. A step that says "verify" and a step that says "look at it" execute the same
21
- * way. The rules about who does what live in `agents/*.md` and in the model that wrote the plan, not
22
- * in a dispatcher holding a table of role names.
23
- * 2. IT DOES NOT JUDGE RESULTS. A `verifier` step that completes with 结论:失败 has SUCCEEDED, as far as
24
- * this loop is concerned, and the plan advances. The verdict lives in the text, and the component that
25
- * can read the text - the primary agent - is what decides whether to run another round. A server that
26
- * parsed 通过/失败 would be guessing at a convention `verifier.md` states in prose and `scout.md` and
27
- * `critic.md` do not have at all, and would have no correct answer when the text said neither.
6
+ * Recursive descent over the primary's plan. This engine owns execution order and the mechanical `until`
7
+ * comparison; it does not infer tasks, discover foreach items, or ask a child whether another round should
8
+ * run. Spawning, streaming and teardown remain behind `SubagentRunRecorder.dispatch`, which always resolves
9
+ * with a settled record.
28
10
  */
29
- /** What the engine needs from the server around it. Injected, so this file holds no globals. */
30
11
  export type WorkflowEngineHooks = {
31
12
  store: Store;
32
- /** Publishes one event to whoever is listening. A plain function, so a test can pass an array. */
33
13
  broadcast: (event: ServerEvent) => void;
34
14
  };
35
- /** Everything one plan needs to run. The plan itself arrives already validated and normalized. */
36
15
  export type WorkflowRunInput = {
37
16
  conversationId: string;
38
- /** The dispatching conversation's workspace, resolved by the caller. Never defaulted to the server's own directory. */
39
17
  cwd: string;
40
18
  plan: WorkflowPlan;
41
19
  model?: ModelSelection;
42
- /** Tools the primary agent denied, inherited by every step, exactly as a single dispatch inherits them. */
43
20
  denyTools?: readonly string[];
44
- /** Cancels the whole plan. Each step is dispatched under it, so one signal reaches the loop and the child. */
45
21
  signal?: AbortSignal;
46
22
  };
47
- /** The outcome of a plan, as the endpoint reports it. Always produced, never thrown. */
48
23
  export type WorkflowOutcome = {
49
24
  workflow: WorkflowRun;
50
25
  };
51
26
  export declare class WorkflowEngine {
52
27
  private readonly recorder;
53
28
  private readonly hooks;
54
- /**
55
- * One controller per in-flight plan.
56
- *
57
- * NEEDED SEPARATELY FROM THE RECORDER'S, and the difference is what stops a plan between steps. The
58
- * recorder's map only holds runs that are executing right now, so a plan halted by a signal while no
59
- * child is out would have nothing to abort and would happily start the next one. This map holds the
60
- * PLAN, which exists for its whole length, so `stop` reaches it in every phase.
61
- */
62
29
  private readonly controllers;
30
+ private readonly owners;
63
31
  constructor(recorder: SubagentRunRecorder, hooks: WorkflowEngineHooks);
64
- /**
65
- * Stops ONE plan, and says whether there was anything to stop.
66
- *
67
- * `false` for an unknown or already-finished id is deliberate and matches `SubagentRunRecorder.stop`:
68
- * the click that produces it raced the plan's own terminal event, and an error reply would put a
69
- * failure banner on a timeline that says it finished normally.
70
- *
71
- * The abort reaches the child too, because every step is dispatched under this controller's signal and
72
- * `dispatch` turns it into the same abort-then-killTree the recorder uses. Stopping a plan really does
73
- * stop the process it is on.
74
- */
75
32
  stop(workflowId: string): boolean;
76
- /** Stops every plan in flight for one conversation, and returns how many were signalled. */
77
33
  stopConversation(conversationId: string): number;
78
- /** Aborts every plan in flight. Called on shutdown, so no child outlives the server that owns it. */
79
34
  abortAll(): void;
80
- /** Waits until aborted plans have committed their terminal state. */
81
35
  waitForIdle(): Promise<void>;
82
- /** The conversation a plan belongs to, which only this map knows while the plan is running. */
83
36
  private conversationOf;
84
- /**
85
- * Which conversation each in-flight plan belongs to.
86
- *
87
- * Held beside the controllers rather than read back from the store, because the store is the right
88
- * place for what a person may read later and the wrong place for a synchronous lookup in a stop
89
- * handler - it clones the entire state, and this runs on a click.
90
- */
91
- private readonly owners;
92
- /**
93
- * Executes one plan to a terminal state, and returns the record as it ended.
94
- *
95
- * ALWAYS RESOLVES, like `dispatch` does and for the same reason: by the time the first await runs, the
96
- * whole plan is already on disk, so the honest account of a plan that cannot proceed is a record that
97
- * says why, rather than a rejection that leaves it claiming to be running until the next restart.
98
- */
99
37
  run(input: WorkflowRunInput): Promise<WorkflowOutcome>;
100
- /**
101
- * Runs one step until it succeeds, is skipped, or stops the plan. Returns whether the PLAN should halt.
102
- *
103
- * The retry is a loop rather than a recursive call so the bound in the policy is the loop's own
104
- * condition and cannot be exceeded by a bug in the call structure - the one property a retry policy has
105
- * to have absolutely.
106
- */
38
+ /** Allocates execution rows once per statically known occurrence; loop occurrences are allocated per round. */
39
+ private materializeNodes;
40
+ private runNodes;
41
+ private runLoop;
42
+ private runForeach;
107
43
  private runStep;
108
- /**
109
- * Marks every step from `from` onward as stopped, because none of them will run.
110
- *
111
- * LEAVING THEM `pending` WOULD BE A LIE IN THE SPECIFIC DIRECTION THAT MATTERS: a browser renders
112
- * "未开始" for a pending step, so a plan that died after step one of three would show three steps with
113
- * two still waiting, and a person would reasonably expect the rest to happen. They will not.
114
- */
115
44
  private haltRemaining;
116
- /**
117
- * Persists a plan, THEN announces it.
118
- *
119
- * PERSIST FIRST, ANNOUNCE SECOND, and the order is load-bearing here for exactly the reason it is in
120
- * `SubagentRunRecorder.settle`: a browser that is told a step finished while `state.json` still says it
121
- * is running will show a timeline a reload immediately contradicts, and the startup reconciliation that
122
- * would fix it runs on a SERVER restart, not a page refresh.
123
- *
124
- * Unlike the recorder, this AWAITS both halves rather than writing fire-and-forget, and that is not an
125
- * inconsistency. The recorder cannot await: it publishes on every text delta, and a drain loop that
126
- * flushed to disk per token would throttle the child it is streaming. This publishes twice per step -
127
- * at most sixteen times for a whole plan - so awaiting costs nothing measurable and buys the strong
128
- * version of the guarantee.
129
- */
45
+ /** Persist first, announce second, so a reload never contradicts an event already shown. */
130
46
  private commit;
131
47
  }