@chengchenccc/oh-my-agent 0.1.1-rc.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.
Files changed (276) hide show
  1. package/README.md +79 -0
  2. package/dist/cli/args.d.ts +24 -0
  3. package/dist/cli/args.d.ts.map +1 -0
  4. package/dist/cli/args.js +120 -0
  5. package/dist/cli/initial-input.d.ts +32 -0
  6. package/dist/cli/initial-input.d.ts.map +1 -0
  7. package/dist/cli/initial-input.js +131 -0
  8. package/dist/cli.d.ts +3 -0
  9. package/dist/cli.d.ts.map +1 -0
  10. package/dist/cli.js +3 -0
  11. package/dist/core/__fixtures__/echo-model.d.ts +6 -0
  12. package/dist/core/__fixtures__/echo-model.d.ts.map +1 -0
  13. package/dist/core/__fixtures__/echo-model.js +10 -0
  14. package/dist/core/__fixtures__/mcp-echo-server.d.ts +2 -0
  15. package/dist/core/__fixtures__/mcp-echo-server.d.ts.map +1 -0
  16. package/dist/core/__fixtures__/mcp-echo-server.js +54 -0
  17. package/dist/core/agent-runtime.d.ts +21 -0
  18. package/dist/core/agent-runtime.d.ts.map +1 -0
  19. package/dist/core/agent-runtime.js +14 -0
  20. package/dist/core/memory/autonomous-memory.d.ts +21 -0
  21. package/dist/core/memory/autonomous-memory.d.ts.map +1 -0
  22. package/dist/core/memory/autonomous-memory.js +202 -0
  23. package/dist/core/persistence/in-memory-session-store.d.ts +3 -0
  24. package/dist/core/persistence/in-memory-session-store.d.ts.map +1 -0
  25. package/dist/core/persistence/in-memory-session-store.js +109 -0
  26. package/dist/core/persistence/session-store.contract.d.ts +5 -0
  27. package/dist/core/persistence/session-store.contract.d.ts.map +1 -0
  28. package/dist/core/persistence/session-store.contract.js +183 -0
  29. package/dist/core/persistence/session-store.d.ts +33 -0
  30. package/dist/core/persistence/session-store.d.ts.map +1 -0
  31. package/dist/core/persistence/session-store.js +22 -0
  32. package/dist/core/persistence/session-tree.d.ts +49 -0
  33. package/dist/core/persistence/session-tree.d.ts.map +1 -0
  34. package/dist/core/persistence/session-tree.js +1 -0
  35. package/dist/core/plugins/plugin-code.d.ts +13 -0
  36. package/dist/core/plugins/plugin-code.d.ts.map +1 -0
  37. package/dist/core/plugins/plugin-code.js +62 -0
  38. package/dist/core/plugins/plugin-marketplace.d.ts +72 -0
  39. package/dist/core/plugins/plugin-marketplace.d.ts.map +1 -0
  40. package/dist/core/plugins/plugin-marketplace.js +340 -0
  41. package/dist/core/plugins/plugin-resolve.d.ts +32 -0
  42. package/dist/core/plugins/plugin-resolve.d.ts.map +1 -0
  43. package/dist/core/plugins/plugin-resolve.js +101 -0
  44. package/dist/core/plugins/plugin-trust.d.ts +24 -0
  45. package/dist/core/plugins/plugin-trust.d.ts.map +1 -0
  46. package/dist/core/plugins/plugin-trust.js +89 -0
  47. package/dist/core/runtime/agent-event.d.ts +98 -0
  48. package/dist/core/runtime/agent-event.d.ts.map +1 -0
  49. package/dist/core/runtime/agent-event.js +1 -0
  50. package/dist/core/runtime/agent-loop.d.ts +142 -0
  51. package/dist/core/runtime/agent-loop.d.ts.map +1 -0
  52. package/dist/core/runtime/agent-loop.js +1008 -0
  53. package/dist/core/runtime/approval.d.ts +23 -0
  54. package/dist/core/runtime/approval.d.ts.map +1 -0
  55. package/dist/core/runtime/approval.js +25 -0
  56. package/dist/core/runtime/compaction.d.ts +22 -0
  57. package/dist/core/runtime/compaction.d.ts.map +1 -0
  58. package/dist/core/runtime/compaction.js +90 -0
  59. package/dist/core/runtime/context-estimate.d.ts +42 -0
  60. package/dist/core/runtime/context-estimate.d.ts.map +1 -0
  61. package/dist/core/runtime/context-estimate.js +34 -0
  62. package/dist/core/runtime/create-runtime.d.ts +81 -0
  63. package/dist/core/runtime/create-runtime.d.ts.map +1 -0
  64. package/dist/core/runtime/create-runtime.js +250 -0
  65. package/dist/core/runtime/fake-provider.d.ts +3 -0
  66. package/dist/core/runtime/fake-provider.d.ts.map +1 -0
  67. package/dist/core/runtime/fake-provider.js +87 -0
  68. package/dist/core/runtime/logger.d.ts +13 -0
  69. package/dist/core/runtime/logger.d.ts.map +1 -0
  70. package/dist/core/runtime/logger.js +38 -0
  71. package/dist/core/runtime/loop-input.d.ts +39 -0
  72. package/dist/core/runtime/loop-input.d.ts.map +1 -0
  73. package/dist/core/runtime/loop-input.js +38 -0
  74. package/dist/core/runtime/message-cache.d.ts +16 -0
  75. package/dist/core/runtime/message-cache.d.ts.map +1 -0
  76. package/dist/core/runtime/message-cache.js +23 -0
  77. package/dist/core/runtime/model-catalog.d.ts +10 -0
  78. package/dist/core/runtime/model-catalog.d.ts.map +1 -0
  79. package/dist/core/runtime/model-catalog.js +14 -0
  80. package/dist/core/runtime/plugin-runtime.d.ts +41 -0
  81. package/dist/core/runtime/plugin-runtime.d.ts.map +1 -0
  82. package/dist/core/runtime/plugin-runtime.js +1 -0
  83. package/dist/core/runtime/plugin.d.ts +86 -0
  84. package/dist/core/runtime/plugin.d.ts.map +1 -0
  85. package/dist/core/runtime/plugin.js +23 -0
  86. package/dist/core/runtime/process-tree.d.ts +12 -0
  87. package/dist/core/runtime/process-tree.d.ts.map +1 -0
  88. package/dist/core/runtime/process-tree.js +40 -0
  89. package/dist/core/runtime/prompt.d.ts +23 -0
  90. package/dist/core/runtime/prompt.d.ts.map +1 -0
  91. package/dist/core/runtime/prompt.js +29 -0
  92. package/dist/core/runtime/prompts.d.ts +16 -0
  93. package/dist/core/runtime/prompts.d.ts.map +1 -0
  94. package/dist/core/runtime/prompts.js +36 -0
  95. package/dist/core/runtime/retry.d.ts +20 -0
  96. package/dist/core/runtime/retry.d.ts.map +1 -0
  97. package/dist/core/runtime/retry.js +60 -0
  98. package/dist/core/runtime/run-runtime.d.ts +93 -0
  99. package/dist/core/runtime/run-runtime.d.ts.map +1 -0
  100. package/dist/core/runtime/run-runtime.js +664 -0
  101. package/dist/core/runtime/runtime-catalog.d.ts +12 -0
  102. package/dist/core/runtime/runtime-catalog.d.ts.map +1 -0
  103. package/dist/core/runtime/runtime-catalog.js +89 -0
  104. package/dist/core/runtime/stream-rules.d.ts +9 -0
  105. package/dist/core/runtime/stream-rules.d.ts.map +1 -0
  106. package/dist/core/runtime/stream-rules.js +44 -0
  107. package/dist/core/runtime/title.d.ts +7 -0
  108. package/dist/core/runtime/title.d.ts.map +1 -0
  109. package/dist/core/runtime/title.js +114 -0
  110. package/dist/core/runtime/todo.d.ts +16 -0
  111. package/dist/core/runtime/todo.d.ts.map +1 -0
  112. package/dist/core/runtime/todo.js +40 -0
  113. package/dist/core/runtime/tool-filter.d.ts +12 -0
  114. package/dist/core/runtime/tool-filter.d.ts.map +1 -0
  115. package/dist/core/runtime/tool-filter.js +23 -0
  116. package/dist/core/runtime/tool-pruning.d.ts +29 -0
  117. package/dist/core/runtime/tool-pruning.d.ts.map +1 -0
  118. package/dist/core/runtime/tool-pruning.js +105 -0
  119. package/dist/core/session/input-history.d.ts +8 -0
  120. package/dist/core/session/input-history.d.ts.map +1 -0
  121. package/dist/core/session/input-history.js +41 -0
  122. package/dist/core/session/session-file.d.ts +84 -0
  123. package/dist/core/session/session-file.d.ts.map +1 -0
  124. package/dist/core/session/session-file.js +392 -0
  125. package/dist/core/session/session-loop.d.ts +33 -0
  126. package/dist/core/session/session-loop.d.ts.map +1 -0
  127. package/dist/core/session/session-loop.js +30 -0
  128. package/dist/core/settings/project-settings.d.ts +46 -0
  129. package/dist/core/settings/project-settings.d.ts.map +1 -0
  130. package/dist/core/settings/project-settings.js +85 -0
  131. package/dist/core/settings/workspace-context.d.ts +10 -0
  132. package/dist/core/settings/workspace-context.d.ts.map +1 -0
  133. package/dist/core/settings/workspace-context.js +48 -0
  134. package/dist/core/tools/agent-fs-like.d.ts +15 -0
  135. package/dist/core/tools/agent-fs-like.d.ts.map +1 -0
  136. package/dist/core/tools/agent-fs-like.js +4 -0
  137. package/dist/core/tools/ask-question.d.ts +5 -0
  138. package/dist/core/tools/ask-question.d.ts.map +1 -0
  139. package/dist/core/tools/ask-question.js +51 -0
  140. package/dist/core/tools/bash.d.ts +5 -0
  141. package/dist/core/tools/bash.d.ts.map +1 -0
  142. package/dist/core/tools/bash.js +109 -0
  143. package/dist/core/tools/eval.d.ts +10 -0
  144. package/dist/core/tools/eval.d.ts.map +1 -0
  145. package/dist/core/tools/eval.js +83 -0
  146. package/dist/core/tools/file-tools.d.ts +18 -0
  147. package/dist/core/tools/file-tools.d.ts.map +1 -0
  148. package/dist/core/tools/file-tools.js +249 -0
  149. package/dist/core/tools/glob.d.ts +5 -0
  150. package/dist/core/tools/glob.d.ts.map +1 -0
  151. package/dist/core/tools/glob.js +54 -0
  152. package/dist/core/tools/grep.d.ts +5 -0
  153. package/dist/core/tools/grep.d.ts.map +1 -0
  154. package/dist/core/tools/grep.js +61 -0
  155. package/dist/core/tools/index.d.ts +14 -0
  156. package/dist/core/tools/index.d.ts.map +1 -0
  157. package/dist/core/tools/index.js +14 -0
  158. package/dist/core/tools/ls-tree.d.ts +10 -0
  159. package/dist/core/tools/ls-tree.d.ts.map +1 -0
  160. package/dist/core/tools/ls-tree.js +177 -0
  161. package/dist/core/tools/mcp-mount.d.ts +63 -0
  162. package/dist/core/tools/mcp-mount.d.ts.map +1 -0
  163. package/dist/core/tools/mcp-mount.js +287 -0
  164. package/dist/core/tools/read-image.d.ts +6 -0
  165. package/dist/core/tools/read-image.d.ts.map +1 -0
  166. package/dist/core/tools/read-image.js +98 -0
  167. package/dist/core/tools/skill.d.ts +11 -0
  168. package/dist/core/tools/skill.d.ts.map +1 -0
  169. package/dist/core/tools/skill.js +65 -0
  170. package/dist/core/tools/skills.d.ts +10 -0
  171. package/dist/core/tools/skills.d.ts.map +1 -0
  172. package/dist/core/tools/skills.js +116 -0
  173. package/dist/core/tools/todo-store.d.ts +8 -0
  174. package/dist/core/tools/todo-store.d.ts.map +1 -0
  175. package/dist/core/tools/todo-store.js +100 -0
  176. package/dist/core/tools/todo.d.ts +8 -0
  177. package/dist/core/tools/todo.d.ts.map +1 -0
  178. package/dist/core/tools/todo.js +91 -0
  179. package/dist/core/tools/url-guard.d.ts +12 -0
  180. package/dist/core/tools/url-guard.d.ts.map +1 -0
  181. package/dist/core/tools/url-guard.js +58 -0
  182. package/dist/core/tools/web-ports-std.d.ts +6 -0
  183. package/dist/core/tools/web-ports-std.d.ts.map +1 -0
  184. package/dist/core/tools/web-ports-std.js +145 -0
  185. package/dist/core/tools/web-ports.d.ts +56 -0
  186. package/dist/core/tools/web-ports.d.ts.map +1 -0
  187. package/dist/core/tools/web-ports.js +62 -0
  188. package/dist/core/tools/workspace-sandbox.d.ts +20 -0
  189. package/dist/core/tools/workspace-sandbox.d.ts.map +1 -0
  190. package/dist/core/tools/workspace-sandbox.js +84 -0
  191. package/dist/core/workflow/subagent-registry.d.ts +40 -0
  192. package/dist/core/workflow/subagent-registry.d.ts.map +1 -0
  193. package/dist/core/workflow/subagent-registry.js +118 -0
  194. package/dist/core/workflow/workflow-evaluator.d.ts +23 -0
  195. package/dist/core/workflow/workflow-evaluator.d.ts.map +1 -0
  196. package/dist/core/workflow/workflow-evaluator.js +21 -0
  197. package/dist/core/workflow/workflow-executor.d.ts +108 -0
  198. package/dist/core/workflow/workflow-executor.d.ts.map +1 -0
  199. package/dist/core/workflow/workflow-executor.js +549 -0
  200. package/dist/core/workflow/workflow-tools.d.ts +50 -0
  201. package/dist/core/workflow/workflow-tools.d.ts.map +1 -0
  202. package/dist/core/workflow/workflow-tools.js +217 -0
  203. package/dist/index.d.ts +14 -0
  204. package/dist/index.d.ts.map +1 -0
  205. package/dist/index.js +13 -0
  206. package/dist/main.d.ts +9 -0
  207. package/dist/main.d.ts.map +1 -0
  208. package/dist/main.js +97 -0
  209. package/dist/modes/json-mode.d.ts +5 -0
  210. package/dist/modes/json-mode.d.ts.map +1 -0
  211. package/dist/modes/json-mode.js +46 -0
  212. package/dist/modes/print-mode.d.ts +20 -0
  213. package/dist/modes/print-mode.d.ts.map +1 -0
  214. package/dist/modes/print-mode.js +82 -0
  215. package/dist/modes/rpc/jsonl.d.ts +23 -0
  216. package/dist/modes/rpc/jsonl.d.ts.map +1 -0
  217. package/dist/modes/rpc/jsonl.js +88 -0
  218. package/dist/modes/rpc/rpc-mode.d.ts +18 -0
  219. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -0
  220. package/dist/modes/rpc/rpc-mode.js +326 -0
  221. package/dist/modes/tui/settings-overlay.d.ts +23 -0
  222. package/dist/modes/tui/settings-overlay.d.ts.map +1 -0
  223. package/dist/modes/tui/settings-overlay.js +136 -0
  224. package/dist/modes/tui/tui-commands-session.d.ts +3 -0
  225. package/dist/modes/tui/tui-commands-session.d.ts.map +1 -0
  226. package/dist/modes/tui/tui-commands-session.js +191 -0
  227. package/dist/modes/tui/tui-commands.d.ts +52 -0
  228. package/dist/modes/tui/tui-commands.d.ts.map +1 -0
  229. package/dist/modes/tui/tui-commands.js +373 -0
  230. package/dist/modes/tui/tui-components.d.ts +81 -0
  231. package/dist/modes/tui/tui-components.d.ts.map +1 -0
  232. package/dist/modes/tui/tui-components.js +248 -0
  233. package/dist/modes/tui/tui-format.d.ts +65 -0
  234. package/dist/modes/tui/tui-format.d.ts.map +1 -0
  235. package/dist/modes/tui/tui-format.js +246 -0
  236. package/dist/modes/tui/tui-frame-provider.d.ts +16 -0
  237. package/dist/modes/tui/tui-frame-provider.d.ts.map +1 -0
  238. package/dist/modes/tui/tui-frame-provider.js +31 -0
  239. package/dist/modes/tui/tui-interactive.d.ts +16 -0
  240. package/dist/modes/tui/tui-interactive.d.ts.map +1 -0
  241. package/dist/modes/tui/tui-interactive.js +123 -0
  242. package/dist/modes/tui/tui-io.d.ts +12 -0
  243. package/dist/modes/tui/tui-io.d.ts.map +1 -0
  244. package/dist/modes/tui/tui-io.js +620 -0
  245. package/dist/modes/tui/tui-mode.d.ts +120 -0
  246. package/dist/modes/tui/tui-mode.d.ts.map +1 -0
  247. package/dist/modes/tui/tui-mode.js +391 -0
  248. package/dist/modes/tui/tui-render.d.ts +54 -0
  249. package/dist/modes/tui/tui-render.d.ts.map +1 -0
  250. package/dist/modes/tui/tui-render.js +399 -0
  251. package/dist/modes/tui/tui-slash.d.ts +12 -0
  252. package/dist/modes/tui/tui-slash.d.ts.map +1 -0
  253. package/dist/modes/tui/tui-slash.js +64 -0
  254. package/dist/modes/tui/tui-tool-render.d.ts +6 -0
  255. package/dist/modes/tui/tui-tool-render.d.ts.map +1 -0
  256. package/dist/modes/tui/tui-tool-render.js +79 -0
  257. package/dist/modes/tui/tui-transcript-reconciler.d.ts +25 -0
  258. package/dist/modes/tui/tui-transcript-reconciler.d.ts.map +1 -0
  259. package/dist/modes/tui/tui-transcript-reconciler.js +97 -0
  260. package/dist/modes/tui/view-state.d.ts +67 -0
  261. package/dist/modes/tui/view-state.d.ts.map +1 -0
  262. package/dist/modes/tui/view-state.js +353 -0
  263. package/dist/prompts/agents/subagent.md +26 -0
  264. package/dist/prompts/system/base.md +20 -0
  265. package/dist/prompts/system/memory.md +23 -0
  266. package/dist/prompts/system/safety.md +12 -0
  267. package/dist/protocol/index.d.ts +5 -0
  268. package/dist/protocol/index.d.ts.map +1 -0
  269. package/dist/protocol/index.js +2 -0
  270. package/dist/protocol/mapping.d.ts +25 -0
  271. package/dist/protocol/mapping.d.ts.map +1 -0
  272. package/dist/protocol/mapping.js +115 -0
  273. package/dist/protocol/transport.d.ts +1396 -0
  274. package/dist/protocol/transport.d.ts.map +1 -0
  275. package/dist/protocol/transport.js +170 -0
  276. package/package.json +49 -0
@@ -0,0 +1,108 @@
1
+ import type { Usage } from "@chengchenccc/agent-contract";
2
+ import type { AIMessageChunk, JsonSchema, Message } from "@chengchenccc/message";
3
+ import { type ContextBudget, type ContextSummarizer, type OmaLoopEvent, type PluginTool } from "../agent-runtime.js";
4
+ export interface WorkflowAgentSpec {
5
+ readonly prompt: string;
6
+ readonly label?: string;
7
+ readonly schema?: Readonly<Record<string, unknown>>;
8
+ /** 3.4: registry-provided role prompt (`.oma/agents/*.md` body or a
9
+ * builtin role); the generic subagent tail is appended by the executor. */
10
+ readonly systemPrompt?: string;
11
+ /** 3.4: tool name allowlist — a subset of the executor's file tools.
12
+ * `explore` uses read/grep/glob/tree/read_image; undefined = all. */
13
+ readonly toolNames?: readonly string[];
14
+ /** 3.4: per-call model override (provider/model id, resolved like the
15
+ * run's own model). Defaults to the run model. */
16
+ readonly modelId?: string;
17
+ /** 3.4 Phase 2: resume an existing subagent handle with a follow-up
18
+ * prompt (its spec snapshot is pinned at first dispatch). */
19
+ readonly resumeHandle?: string;
20
+ /** 3.4 Phase 3: fire-and-forget — returns immediately with the handle in
21
+ * `running` state; the result lands on the handle (subagent_output). */
22
+ readonly background?: boolean;
23
+ }
24
+ export interface WorkflowAgentResult {
25
+ readonly label: string;
26
+ readonly text: string;
27
+ readonly output?: unknown;
28
+ readonly ok: boolean;
29
+ readonly error?: string;
30
+ readonly usage?: Usage;
31
+ /** A3: full text spilled to this workspace-relative path (read it back
32
+ * with the read tool instead of carrying it inline). */
33
+ readonly resultPath?: string;
34
+ /** A4: workspace-relative paths the subagent wrote/edited (write/edit
35
+ * tool calls), so the parent has a visible handoff surface. */
36
+ readonly artifacts?: readonly string[];
37
+ /** 3.4 Phase 2: subagent handle — pass it back via `resumeHandle` to
38
+ * continue the same session with a follow-up prompt. */
39
+ readonly handle?: string;
40
+ /** 3.4 Phase 3: present on background dispatch acknowledgements
41
+ * (`running`) and stored on the handle after completion. */
42
+ readonly status?: "running" | "completed" | "failed" | "stopped";
43
+ }
44
+ export interface WorkflowRunResult {
45
+ readonly items: readonly WorkflowAgentResult[];
46
+ readonly totalTokens: number;
47
+ readonly ok: boolean;
48
+ }
49
+ /** Same shape as the session's modelStream option: the subagent session calls
50
+ * it with its own messages/signal/tools. The `modelId` override comes from
51
+ * the role definition (3.4) and must resolve in the runtime catalog;
52
+ * `responseFormat` (F5) asks the provider for schema-conformant JSON. */
53
+ export type SubagentModelStream = (messages: readonly Message[], signal?: AbortSignal, tools?: readonly PluginTool[], modelId?: string, responseFormat?: JsonSchema) => AsyncIterable<AIMessageChunk>;
54
+ export interface WorkflowExecutorOptions {
55
+ /** Build the subagent model stream (same model + reasoning as the run,
56
+ * unless the role pins a `modelId` override). */
57
+ readonly makeSubagentStream: (sessionId: string, modelId?: string, responseFormat?: JsonSchema) => SubagentModelStream;
58
+ readonly modelId: string;
59
+ readonly summarize: ContextSummarizer;
60
+ readonly contextBudget: ContextBudget;
61
+ /** File tools only (no workflow/product tools - recursion + clobber guards). */
62
+ readonly tools: readonly PluginTool[];
63
+ readonly workspaceRoot: string;
64
+ readonly workspaceAccess: "read_only" | "read_write";
65
+ readonly maxConcurrent: number;
66
+ readonly maxTotal: number;
67
+ readonly emit: (event: OmaLoopEvent) => void;
68
+ /** Optional product budget gate: consulted BEFORE each spawn. */
69
+ readonly budgetGate?: () => {
70
+ allowed: boolean;
71
+ reason?: string;
72
+ };
73
+ /** Optional wall-clock deadline per subagent (B7). Undefined = no
74
+ * deadline (the model call keeps its own per-call timeout). */
75
+ readonly perAgentTimeoutMs?: number;
76
+ }
77
+ export interface WorkflowExecutor {
78
+ runSubagent(input: {
79
+ workflowId: string;
80
+ agentId: string;
81
+ } & WorkflowAgentSpec, signal?: AbortSignal): Promise<WorkflowAgentResult>;
82
+ runWorkflow(input: {
83
+ workflowId: string;
84
+ label: string;
85
+ items: readonly WorkflowAgentSpec[];
86
+ signal?: AbortSignal;
87
+ }): Promise<WorkflowRunResult>;
88
+ /** 3.4 Phase 3 control plane. */
89
+ listSubagents(): Array<{
90
+ handle: string;
91
+ label: string;
92
+ status: string;
93
+ usage?: Usage;
94
+ }>;
95
+ getSubagentOutput(handle: string): {
96
+ handle: string;
97
+ status: string;
98
+ result?: WorkflowAgentResult;
99
+ };
100
+ stopSubagent(handle: string): {
101
+ ok: boolean;
102
+ error?: string;
103
+ };
104
+ /** Stop every live subagent (run teardown cascade). */
105
+ abortAllSubagents(): void;
106
+ }
107
+ export declare function createWorkflowExecutor(opts: WorkflowExecutorOptions): WorkflowExecutor;
108
+ //# sourceMappingURL=workflow-executor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflow-executor.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/workflow-executor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,549 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import subagentPrompt from "../../prompts/agents/subagent.md" with { type: "text" };
4
+ import { createInMemorySessionStore, createOmaSession, } from "../agent-runtime.js";
5
+ const SUBAGENT_SYSTEM_PROMPT = subagentPrompt.trim();
6
+ /** A2: minimal JSON-Schema subset validator for model-supplied output
7
+ * schemas — covers type / properties / required / enum / items (the exact
8
+ * Loop triage shape, loop-step.ts renderTriageWorkflow). Unknown keywords
9
+ * pass through; this is a guard, not a full validator.
10
+ * Upgrade path: replace with provider-native structured output
11
+ * (ChatModel.stream responseFormat) once that lands; the validator then
12
+ * stays as a backstop and the schema-correction retry is kept. */
13
+ function matchesSchemaType(type, value) {
14
+ switch (type) {
15
+ case "string":
16
+ return typeof value === "string";
17
+ case "number":
18
+ return typeof value === "number";
19
+ case "integer":
20
+ return typeof value === "number" && Number.isInteger(value);
21
+ case "boolean":
22
+ return typeof value === "boolean";
23
+ case "null":
24
+ return value === null;
25
+ case "object":
26
+ return typeof value === "object" && value !== null && !Array.isArray(value);
27
+ case "array":
28
+ return Array.isArray(value);
29
+ default:
30
+ return true; // unknown types never reject
31
+ }
32
+ }
33
+ /** Returns a human-readable violation, or undefined when the value
34
+ * conforms to the supported subset. */
35
+ function validateJsonSchema(value, schema) {
36
+ const types = (Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : []);
37
+ if (types.length > 0 && !types.some((t) => matchesSchemaType(t, value))) {
38
+ return `expected ${types.join("|")}, got ${value === null ? "null" : typeof value}`;
39
+ }
40
+ if (Array.isArray(schema.enum) && !schema.enum.includes(value)) {
41
+ return `value not in enum (${JSON.stringify(schema.enum).slice(0, 120)})`;
42
+ }
43
+ if (matchesSchemaType("object", value)) {
44
+ const record = value;
45
+ for (const key of schema.required ?? []) {
46
+ if (!(key in record))
47
+ return `missing required property "${key}"`;
48
+ }
49
+ const props = schema.properties;
50
+ if (props) {
51
+ for (const [key, propSchema] of Object.entries(props)) {
52
+ if (key in record) {
53
+ const err = validateJsonSchema(record[key], propSchema);
54
+ if (err)
55
+ return `${key}: ${err}`;
56
+ }
57
+ }
58
+ }
59
+ return undefined;
60
+ }
61
+ if (Array.isArray(value) && schema.items) {
62
+ for (let i = 0; i < value.length; i++) {
63
+ const err = validateJsonSchema(value[i], schema.items);
64
+ if (err)
65
+ return `[${i}]: ${err}`;
66
+ }
67
+ }
68
+ return undefined;
69
+ }
70
+ /** A3 fan-in size guard: per-item inline ceiling, total inline budget, and
71
+ * the excerpt length kept in the tool result when a text is spilled. */
72
+ const MAX_INLINE_ITEM_CHARS = 2000;
73
+ const MAX_TOTAL_INLINE_CHARS = 16_000;
74
+ const EXCERPT_CHARS = 400;
75
+ /** Parse the loop's final text against the optional schema. Returns the
76
+ * parsed output (validated) plus a violation message when it fails. */
77
+ function parseAndValidate(result, schema) {
78
+ const text = (result.messages?.at(-1)?.text ?? "").trim();
79
+ if (!schema || !text)
80
+ return { text };
81
+ try {
82
+ const parsed = JSON.parse(text);
83
+ const schemaError = validateJsonSchema(parsed, schema);
84
+ if (schemaError)
85
+ return { text, parseError: `schema validation failed: ${schemaError}` };
86
+ return { text, output: parsed };
87
+ }
88
+ catch {
89
+ return { text, parseError: `schema output is not valid JSON: ${text.slice(0, 120)}` };
90
+ }
91
+ }
92
+ export function createWorkflowExecutor(opts) {
93
+ let totalSpawned = 0;
94
+ let current = 0;
95
+ const waiters = [];
96
+ /** 3.4 Phase 2: live subagent handles — session + pinned spec snapshot.
97
+ * Per-run executor instance, so the table dies with the run. */
98
+ const subagentHandles = new Map();
99
+ async function acquire(signal) {
100
+ if (current < opts.maxConcurrent) {
101
+ current++;
102
+ return;
103
+ }
104
+ if (signal?.aborted)
105
+ throw new Error("workflow aborted while queued");
106
+ await new Promise((resolve, reject) => {
107
+ const onAbort = () => {
108
+ const idx = waiters.indexOf(fire);
109
+ if (idx >= 0)
110
+ waiters.splice(idx, 1);
111
+ reject(new Error("workflow aborted while queued"));
112
+ };
113
+ const fire = () => {
114
+ signal?.removeEventListener("abort", onAbort);
115
+ resolve();
116
+ };
117
+ waiters.push(fire);
118
+ signal?.addEventListener("abort", onAbort, { once: true });
119
+ // Cover the abort-between-check-and-listen race.
120
+ if (signal?.aborted)
121
+ onAbort();
122
+ });
123
+ }
124
+ function release() {
125
+ const next = waiters.shift();
126
+ if (next)
127
+ next();
128
+ else
129
+ current--;
130
+ }
131
+ class WorkflowGateError extends Error {
132
+ }
133
+ function gate() {
134
+ if (totalSpawned >= opts.maxTotal) {
135
+ throw new WorkflowGateError(`workflow exceeds the ${opts.maxTotal}-agent cap`);
136
+ }
137
+ if (opts.budgetGate) {
138
+ const decision = opts.budgetGate();
139
+ if (!decision.allowed) {
140
+ throw new WorkflowGateError(decision.reason ?? "workflow budget exhausted");
141
+ }
142
+ }
143
+ totalSpawned++;
144
+ }
145
+ async function runSubagent(input, signal) {
146
+ // Phase 2 resume: reuse a live handle's session + pinned spec snapshot
147
+ // (later registry edits never mutate an existing handle's definition).
148
+ const existing = input.resumeHandle ? subagentHandles.get(input.resumeHandle) : null;
149
+ if (input.resumeHandle && !existing) {
150
+ const active = [...subagentHandles.keys()].join(", ");
151
+ return {
152
+ label: input.label ?? input.resumeHandle,
153
+ text: "",
154
+ ok: false,
155
+ error: `unknown subagent handle "${input.resumeHandle}" (active: ${active || "none"})`,
156
+ };
157
+ }
158
+ const spec = existing?.spec ?? input;
159
+ const workflowId = existing?.workflowId ?? input.workflowId;
160
+ const agentId = existing?.agentId ?? input.agentId;
161
+ const sessionId = existing?.sessionId ?? `wf:${workflowId}:${agentId}`;
162
+ await acquire(signal);
163
+ // gate() may throw (cap/budget): it runs inside the try so the acquired
164
+ // concurrency slot is ALWAYS released - a leak here would deadlock every
165
+ // later acquire once all slots are gone.
166
+ try {
167
+ gate();
168
+ const label = input.label ?? agentId;
169
+ const agentSignal = opts.perAgentTimeoutMs !== undefined
170
+ ? signal
171
+ ? AbortSignal.any([signal, AbortSignal.timeout(opts.perAgentTimeoutMs)])
172
+ : AbortSignal.timeout(opts.perAgentTimeoutMs)
173
+ : signal;
174
+ opts.emit({
175
+ type: "workflow_agent_started",
176
+ workflowId,
177
+ agentId,
178
+ label,
179
+ });
180
+ // Role body + the generic subagent tail (constraints live once).
181
+ const systemPrompt = spec.systemPrompt
182
+ ? `${spec.systemPrompt}\n\n${SUBAGENT_SYSTEM_PROMPT}`
183
+ : SUBAGENT_SYSTEM_PROMPT;
184
+ const modelId = spec.modelId ?? opts.modelId;
185
+ const subagentTools = spec.toolNames
186
+ ? opts.tools.filter((t) => spec.toolNames.includes(t.name))
187
+ : opts.tools;
188
+ const store = existing?.store ?? createInMemorySessionStore();
189
+ if (!existing) {
190
+ // The loop opens the session record on startLoop: seed it first
191
+ // (same contract as the run runtime's create-runtime.ts).
192
+ await store.create({
193
+ sessionId,
194
+ backendKind: "oma",
195
+ workspaceRoot: opts.workspaceRoot,
196
+ leafEntryId: null,
197
+ createdAt: Date.now(),
198
+ updatedAt: Date.now(),
199
+ });
200
+ }
201
+ const session = existing?.session ??
202
+ createOmaSession({
203
+ sessionId,
204
+ store,
205
+ plugins: [{ name: "subagent-tools", tools: subagentTools }],
206
+ maxSteps: 8,
207
+ maxForceContinues: 2,
208
+ // Transient model failures (429/timeout) retry via the loop's
209
+ // default retryStream policy (maxAttempts 3).
210
+ modelStream: opts.makeSubagentStream(sessionId, modelId, spec.schema ? { name: "result", schema: spec.schema, strict: true } : undefined),
211
+ summarize: opts.summarize,
212
+ contextBudget: opts.contextBudget,
213
+ });
214
+ const handle = existing ? input.resumeHandle : `sub-${crypto.randomUUID()}`;
215
+ if (!existing) {
216
+ subagentHandles.set(handle, {
217
+ session,
218
+ store,
219
+ sessionId,
220
+ workflowId,
221
+ agentId,
222
+ spec,
223
+ status: "running",
224
+ });
225
+ }
226
+ const onAbort = () => session.stop();
227
+ agentSignal?.addEventListener("abort", onAbort, { once: true });
228
+ const loopInput = {
229
+ run: {
230
+ runId: sessionId,
231
+ model: { backendKind: "oma", modelId },
232
+ systemPrompt,
233
+ configRevision: 0,
234
+ },
235
+ workspace: { root: opts.workspaceRoot, access: opts.workspaceAccess },
236
+ };
237
+ // startFollowUp is the resume primitive: same session, no Meta re-send.
238
+ const launch = (history, inputMsg) => existing
239
+ ? session.startFollowUp({ ...loopInput, history: [], input: inputMsg })
240
+ : session.startLoop({ ...loopInput, history, input: inputMsg });
241
+ const finish = async () => {
242
+ // Never launch into an already-aborted signal: an abort that landed
243
+ // before the model stream registered its listener would otherwise
244
+ // leave the loop awaiting an abort event that will never fire.
245
+ if (agentSignal?.aborted) {
246
+ return {
247
+ label,
248
+ text: "",
249
+ ok: false,
250
+ error: agentSignal.reason instanceof Error
251
+ ? agentSignal.reason.message
252
+ : "workflow agent aborted",
253
+ };
254
+ }
255
+ let result;
256
+ try {
257
+ result = await launch([], {
258
+ inputId: agentId,
259
+ // Same envelope as oh-my-pi's subagent-user-prompt template.
260
+ message: { role: "user", text: `Complete assignment thoroughly:\n\n${spec.prompt}` },
261
+ });
262
+ }
263
+ finally {
264
+ agentSignal?.removeEventListener("abort", onAbort);
265
+ }
266
+ // A4: collect the subagent's write/edit artifacts BEFORE the store
267
+ // closes (advisory — failures never fail the run).
268
+ const artifactPaths = new Set();
269
+ try {
270
+ const branch = await store.readBranch(sessionId);
271
+ for (const entry of branch) {
272
+ if (entry.type !== "message")
273
+ continue;
274
+ for (const block of entry.message.blocks ?? []) {
275
+ if (block.type !== "tool_use" || (block.name !== "write" && block.name !== "edit")) {
276
+ continue;
277
+ }
278
+ const p = block.input?.path;
279
+ if (typeof p === "string" && p.length > 0)
280
+ artifactPaths.add(p);
281
+ }
282
+ }
283
+ }
284
+ catch {
285
+ /* best-effort */
286
+ }
287
+ const artifacts = [...artifactPaths];
288
+ // Resume keeps the store alive for the handle; only a fresh dispatch
289
+ // closes it.
290
+ if (!existing) {
291
+ void store.close().catch((err) => {
292
+ console.error(`[workflow] subagent store close failed for ${sessionId}:`, err);
293
+ });
294
+ }
295
+ let { text, output, parseError } = parseAndValidate(result, spec.schema);
296
+ if (parseError && !agentSignal?.aborted && result.status === "completed") {
297
+ // A2: one schema-correction turn — the same session re-runs with the
298
+ // produced messages as history plus an explicit fix instruction. A
299
+ // second violation is terminal.
300
+ const correction = `Your previous final message did not match the required output schema. ` +
301
+ `Return ONLY the corrected JSON. Error: ${parseError}`;
302
+ result = await launch((result.messages ?? []).map((message, i) => ({
303
+ // Synthetic identity: the in-memory subagent store never
304
+ // persists canonical messages, so the id is only for the loop's
305
+ // internal append bookkeeping.
306
+ productEntryId: `sub:${agentId}:${i}`,
307
+ message,
308
+ })), {
309
+ inputId: `${agentId}:schema-fix`,
310
+ message: { role: "user", text: correction },
311
+ });
312
+ ({ text, output, parseError } = parseAndValidate(result, spec.schema));
313
+ }
314
+ const loopError = result.status !== "completed"
315
+ ? (result.error ?? `subagent loop ${result.status}`)
316
+ : undefined;
317
+ const error = agentSignal?.aborted
318
+ ? agentSignal.reason instanceof Error
319
+ ? agentSignal.reason.message
320
+ : "workflow agent timed out"
321
+ : (loopError ?? parseError);
322
+ const agentResult = {
323
+ label,
324
+ text,
325
+ ok: result.status === "completed" && !error,
326
+ ...(output !== undefined ? { output } : {}),
327
+ ...(error ? { error } : {}),
328
+ ...(result.usage ? { usage: result.usage } : {}),
329
+ ...(artifacts.length > 0 ? { artifacts } : {}),
330
+ // Phase 2: the handle is minted on first dispatch; resume calls the
331
+ // same handle back (no new handle on follow-ups).
332
+ ...(existing ? {} : { handle }),
333
+ };
334
+ // A1/F2: best-effort per-subagent state dump — the audit trail
335
+ // survives a crash/abort that the in-memory store cannot. Includes
336
+ // the pinned spec snapshot so a verdict dispute can be traced to
337
+ // exactly what the subagent was asked to do.
338
+ const safeName = (s) => /^[A-Za-z0-9-]+$/.test(s);
339
+ if (opts.workspaceAccess === "read_write" && safeName(workflowId) && safeName(agentId)) {
340
+ try {
341
+ const rel = `.oma/workflow/${workflowId}/${agentId}.session.json`;
342
+ const abs = join(opts.workspaceRoot, rel);
343
+ mkdirSync(dirname(abs), { recursive: true });
344
+ writeFileSync(abs, JSON.stringify({
345
+ workflowId,
346
+ agentId,
347
+ label,
348
+ ok: agentResult.ok,
349
+ ...(agentResult.error ? { error: agentResult.error } : {}),
350
+ status: result.status,
351
+ spec: {
352
+ prompt: spec.prompt,
353
+ ...(spec.label ? { label: spec.label } : {}),
354
+ ...(spec.systemPrompt ? { systemPrompt: spec.systemPrompt } : {}),
355
+ ...(spec.toolNames ? { toolNames: spec.toolNames } : {}),
356
+ ...(spec.modelId ? { modelId: spec.modelId } : {}),
357
+ ...(spec.schema ? { schema: spec.schema } : {}),
358
+ },
359
+ ...(result.usage ? { usage: result.usage } : {}),
360
+ ...(artifacts.length > 0 ? { artifacts } : {}),
361
+ messages: result.messages ?? [],
362
+ updatedAt: Date.now(),
363
+ }, null, 2));
364
+ }
365
+ catch (err) {
366
+ console.error(`[workflow] state dump failed for ${input.agentId}:`, err);
367
+ }
368
+ }
369
+ else if (opts.workspaceAccess !== "read_write") {
370
+ console.warn(`[workflow] read_only workspace: subagent session not dumped for ${agentId}`);
371
+ }
372
+ opts.emit({
373
+ type: "workflow_agent_completed",
374
+ workflowId,
375
+ agentId,
376
+ label,
377
+ ok: agentResult.ok,
378
+ ...(agentResult.error ? { error: agentResult.error } : {}),
379
+ ...(agentResult.usage ? { usage: agentResult.usage } : {}),
380
+ });
381
+ return agentResult;
382
+ };
383
+ // 3.4 Phase 3: fire-and-forget. Acknowledge immediately with the
384
+ // handle; the result lands on the handle for subagent_output.
385
+ if (spec.background) {
386
+ const entry = subagentHandles.get(handle);
387
+ if (entry)
388
+ entry.status = "running";
389
+ void finish()
390
+ .then((agentResult) => {
391
+ const e = subagentHandles.get(handle);
392
+ if (!e)
393
+ return;
394
+ if (e.stopRequested) {
395
+ e.status = "stopped";
396
+ e.result = { ...agentResult, ok: false, error: "stopped", status: "stopped" };
397
+ }
398
+ else {
399
+ e.status = agentResult.ok ? "completed" : "failed";
400
+ e.result = { ...agentResult, status: e.status };
401
+ }
402
+ })
403
+ .catch((err) => {
404
+ const e = subagentHandles.get(handle);
405
+ if (e) {
406
+ e.status = e.stopRequested ? "stopped" : "failed";
407
+ e.result = {
408
+ label,
409
+ text: "",
410
+ ok: false,
411
+ error: err instanceof Error ? err.message : String(err),
412
+ status: e.status,
413
+ };
414
+ }
415
+ });
416
+ return { label, text: "", ok: true, handle, status: "running" };
417
+ }
418
+ return finish();
419
+ }
420
+ catch (err) {
421
+ // Gate failures (cap/budget) are WORKFLOW-level: propagate so the
422
+ // whole fan-out rejects instead of degrading to a failed agent row.
423
+ if (err instanceof WorkflowGateError)
424
+ throw err;
425
+ const message = err instanceof Error ? err.message : String(err);
426
+ const label = input.label ?? agentId;
427
+ opts.emit({
428
+ type: "workflow_agent_completed",
429
+ workflowId,
430
+ agentId,
431
+ label,
432
+ ok: false,
433
+ error: message,
434
+ });
435
+ return { label, text: "", ok: false, error: message };
436
+ }
437
+ finally {
438
+ release();
439
+ }
440
+ }
441
+ /** A3: keep fan-in results small enough to re-inject into the main loop.
442
+ * Long item texts spill to `.oma/workflow/<wfId>/<agentId>.result.md` (the
443
+ * main session reads them back with the read tool); read_only workspaces
444
+ * degrade to inline truncation. The total-inline budget forces spill
445
+ * even when no single item exceeds the per-item ceiling. */
446
+ function spillResults(results, workflowId) {
447
+ const total = results.reduce((acc, r) => acc + r.text.length, 0);
448
+ const forceSpill = total > MAX_TOTAL_INLINE_CHARS;
449
+ return results.map((r, i) => {
450
+ if (!forceSpill && r.text.length <= MAX_INLINE_ITEM_CHARS)
451
+ return r;
452
+ const excerpt = r.text.slice(0, EXCERPT_CHARS);
453
+ if (opts.workspaceAccess !== "read_write") {
454
+ return { ...r, text: `${excerpt}…[truncated]` };
455
+ }
456
+ const rel = `.oma/workflow/${workflowId}/a${i}.result.md`;
457
+ const abs = join(opts.workspaceRoot, rel);
458
+ mkdirSync(dirname(abs), { recursive: true });
459
+ writeFileSync(abs, r.text);
460
+ return { ...r, text: excerpt, resultPath: rel };
461
+ });
462
+ }
463
+ async function runWorkflow(input) {
464
+ opts.emit({
465
+ type: "workflow_started",
466
+ workflowId: input.workflowId,
467
+ label: input.label,
468
+ agentCount: input.items.length,
469
+ });
470
+ // Own controller: a gate failure (cap/budget) or abort must stop
471
+ // in-flight siblings (B1) instead of orphaning them while Promise.all
472
+ // rejects and no terminal event is emitted.
473
+ const controller = new AbortController();
474
+ const combined = input.signal
475
+ ? AbortSignal.any([input.signal, controller.signal])
476
+ : controller.signal;
477
+ try {
478
+ const rawResults = await Promise.all(input.items.map((item, i) => runSubagent({ workflowId: input.workflowId, agentId: `a${i}`, ...item }, combined)));
479
+ if (input.signal?.aborted)
480
+ throw new Error("workflow aborted");
481
+ const results = spillResults(rawResults, input.workflowId);
482
+ const totalTokens = results.reduce((acc, r) => acc +
483
+ (r.usage?.inputTokens ?? 0) +
484
+ (r.usage?.outputTokens ?? 0) +
485
+ (r.usage?.cacheReadTokens ?? 0) +
486
+ (r.usage?.cacheWriteTokens ?? 0), 0);
487
+ const ok = results.every((r) => r.ok);
488
+ opts.emit({
489
+ type: "workflow_completed",
490
+ workflowId: input.workflowId,
491
+ ok,
492
+ agentCount: input.items.length,
493
+ totalTokens,
494
+ });
495
+ return { items: results, totalTokens, ok };
496
+ }
497
+ catch (err) {
498
+ controller.abort();
499
+ opts.emit({
500
+ type: "workflow_failed",
501
+ workflowId: input.workflowId,
502
+ error: err instanceof Error ? err.message : String(err),
503
+ });
504
+ throw err;
505
+ }
506
+ }
507
+ // 3.4 Phase 3 control plane.
508
+ function listSubagents() {
509
+ return [...subagentHandles.entries()].map(([handle, e]) => ({
510
+ handle,
511
+ label: e.spec.label ?? e.agentId,
512
+ status: e.status,
513
+ ...(e.result?.usage ? { usage: e.result.usage } : {}),
514
+ }));
515
+ }
516
+ function getSubagentOutput(handle) {
517
+ const e = subagentHandles.get(handle);
518
+ if (!e)
519
+ return { handle, status: "unknown" };
520
+ if (e.result) {
521
+ // A3 size guard applies to fetched results too.
522
+ const [spilled] = spillResults([e.result], e.workflowId);
523
+ return { handle, status: e.status, result: spilled };
524
+ }
525
+ return { handle, status: e.status };
526
+ }
527
+ function stopSubagent(handle) {
528
+ const e = subagentHandles.get(handle);
529
+ if (!e)
530
+ return { ok: false, error: `unknown subagent handle "${handle}"` };
531
+ e.stopRequested = true;
532
+ e.session.stop();
533
+ return { ok: true };
534
+ }
535
+ function abortAllSubagents() {
536
+ for (const e of subagentHandles.values()) {
537
+ e.stopRequested = true;
538
+ e.session.stop();
539
+ }
540
+ }
541
+ return {
542
+ runSubagent,
543
+ runWorkflow,
544
+ listSubagents,
545
+ getSubagentOutput,
546
+ stopSubagent,
547
+ abortAllSubagents,
548
+ };
549
+ }