nemoris 0.1.0 → 0.1.2

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 (248) hide show
  1. package/.env.example +49 -49
  2. package/LICENSE +21 -21
  3. package/README.md +209 -209
  4. package/SECURITY.md +59 -119
  5. package/bin/nemoris +46 -46
  6. package/config/agents/agent.toml.example +28 -28
  7. package/config/agents/content.toml +23 -0
  8. package/config/agents/default.toml +22 -22
  9. package/config/agents/heartbeat.toml +35 -0
  10. package/config/agents/iris.toml +23 -0
  11. package/config/agents/lab.toml +23 -0
  12. package/config/agents/main.toml +45 -0
  13. package/config/agents/nemo.toml +21 -0
  14. package/config/agents/ops.toml +38 -0
  15. package/config/agents/orchestrator.toml +18 -18
  16. package/config/agents/revenue.toml +23 -0
  17. package/config/agents/testyboo.toml +19 -0
  18. package/config/delivery.toml +73 -73
  19. package/config/embeddings.toml +5 -5
  20. package/config/identity/content-purpose.md +11 -0
  21. package/config/identity/content-soul.md +45 -0
  22. package/config/identity/default-purpose.md +1 -1
  23. package/config/identity/default-soul.md +3 -3
  24. package/config/identity/heartbeat-purpose.md +9 -0
  25. package/config/identity/heartbeat-soul.md +16 -0
  26. package/config/identity/iris-purpose.md +17 -0
  27. package/config/identity/iris-soul.md +68 -0
  28. package/config/identity/lab-purpose.md +10 -0
  29. package/config/identity/lab-soul.md +38 -0
  30. package/config/identity/main-purpose.md +17 -0
  31. package/config/identity/main-soul.md +66 -0
  32. package/config/identity/main-user.md +22 -0
  33. package/config/identity/ops-purpose.md +9 -0
  34. package/config/identity/ops-soul.md +16 -0
  35. package/config/identity/orchestrator-purpose.md +1 -1
  36. package/config/identity/orchestrator-soul.md +1 -1
  37. package/config/identity/revenue-purpose.md +9 -0
  38. package/config/identity/revenue-soul.md +41 -0
  39. package/config/identity/testyboo-purpose.md +13 -0
  40. package/config/identity/testyboo-soul.md +20 -0
  41. package/config/improvement-targets.toml +15 -15
  42. package/config/jobs/heartbeat-check.toml +30 -30
  43. package/config/jobs/memory-rollup.toml +46 -46
  44. package/config/jobs/workspace-health.toml +63 -63
  45. package/config/mcp.toml +16 -16
  46. package/config/output-contracts.toml +17 -17
  47. package/config/peers.toml +32 -32
  48. package/config/peers.toml.example +32 -32
  49. package/config/policies/memory-default.toml +10 -10
  50. package/config/policies/memory-heartbeat.toml +5 -5
  51. package/config/policies/memory-ops.toml +10 -10
  52. package/config/policies/tools-heartbeat-minimal.toml +8 -8
  53. package/config/policies/tools-interactive-safe.toml +8 -8
  54. package/config/policies/tools-ops-bounded.toml +8 -8
  55. package/config/policies/tools-orchestrator.toml +7 -7
  56. package/config/providers/anthropic.toml +15 -15
  57. package/config/providers/ollama.toml +5 -5
  58. package/config/providers/openai-codex.toml +9 -9
  59. package/config/providers/openrouter.toml +5 -5
  60. package/config/router.toml +22 -22
  61. package/config/runtime.toml +114 -114
  62. package/config/skills/self-improvement.toml +15 -15
  63. package/config/skills/telegram-onboarding-spec.md +240 -240
  64. package/config/skills/workspace-monitor.toml +15 -15
  65. package/config/task-router.toml +42 -42
  66. package/install.sh +50 -50
  67. package/package.json +91 -90
  68. package/src/auth/auth-profiles.js +169 -169
  69. package/src/auth/openai-codex-oauth.js +285 -285
  70. package/src/battle.js +449 -449
  71. package/src/cli/help.js +265 -265
  72. package/src/cli/output-filter.js +49 -49
  73. package/src/cli/runtime-control.js +704 -704
  74. package/src/cli-main.js +2763 -2763
  75. package/src/cli.js +78 -78
  76. package/src/config/loader.js +332 -332
  77. package/src/config/schema-validator.js +214 -214
  78. package/src/config/toml-lite.js +8 -8
  79. package/src/daemon/action-handlers.js +71 -71
  80. package/src/daemon/healing-tick.js +87 -87
  81. package/src/daemon/health-probes.js +90 -90
  82. package/src/daemon/notifier.js +57 -57
  83. package/src/daemon/nurse.js +218 -218
  84. package/src/daemon/repair-log.js +106 -106
  85. package/src/daemon/rule-staging.js +90 -90
  86. package/src/daemon/rules.js +29 -29
  87. package/src/daemon/telegram-commands.js +54 -54
  88. package/src/daemon/updater.js +85 -85
  89. package/src/jobs/job-runner.js +78 -78
  90. package/src/mcp/consumer.js +129 -129
  91. package/src/memory/active-recall.js +171 -171
  92. package/src/memory/backend-manager.js +97 -97
  93. package/src/memory/backends/file-backend.js +38 -38
  94. package/src/memory/backends/qmd-backend.js +219 -219
  95. package/src/memory/embedding-guards.js +24 -24
  96. package/src/memory/embedding-index.js +118 -118
  97. package/src/memory/embedding-service.js +179 -179
  98. package/src/memory/file-index.js +177 -177
  99. package/src/memory/memory-signature.js +5 -5
  100. package/src/memory/memory-store.js +648 -648
  101. package/src/memory/retrieval-planner.js +66 -66
  102. package/src/memory/scoring.js +145 -145
  103. package/src/memory/simhash.js +78 -78
  104. package/src/memory/sqlite-active-store.js +824 -824
  105. package/src/memory/write-policy.js +36 -36
  106. package/src/onboarding/aliases.js +33 -33
  107. package/src/onboarding/auth/api-key.js +224 -224
  108. package/src/onboarding/auth/ollama-detect.js +42 -42
  109. package/src/onboarding/clack-prompter.js +77 -77
  110. package/src/onboarding/doctor.js +530 -530
  111. package/src/onboarding/lock.js +42 -42
  112. package/src/onboarding/model-catalog.js +344 -344
  113. package/src/onboarding/phases/auth.js +576 -589
  114. package/src/onboarding/phases/build.js +130 -130
  115. package/src/onboarding/phases/choose.js +82 -82
  116. package/src/onboarding/phases/detect.js +98 -98
  117. package/src/onboarding/phases/hatch.js +216 -216
  118. package/src/onboarding/phases/identity.js +79 -79
  119. package/src/onboarding/phases/ollama.js +345 -345
  120. package/src/onboarding/phases/scaffold.js +99 -99
  121. package/src/onboarding/phases/telegram.js +377 -377
  122. package/src/onboarding/phases/validate.js +204 -204
  123. package/src/onboarding/phases/verify.js +206 -206
  124. package/src/onboarding/platform.js +482 -482
  125. package/src/onboarding/status-bar.js +95 -95
  126. package/src/onboarding/templates.js +794 -794
  127. package/src/onboarding/toml-writer.js +38 -38
  128. package/src/onboarding/tui.js +250 -250
  129. package/src/onboarding/uninstall.js +153 -153
  130. package/src/onboarding/wizard.js +516 -499
  131. package/src/providers/anthropic.js +168 -168
  132. package/src/providers/base.js +247 -247
  133. package/src/providers/circuit-breaker.js +136 -136
  134. package/src/providers/ollama.js +163 -163
  135. package/src/providers/openai-codex.js +149 -149
  136. package/src/providers/openrouter.js +136 -136
  137. package/src/providers/registry.js +36 -36
  138. package/src/providers/router.js +16 -16
  139. package/src/runtime/bootstrap-cache.js +47 -47
  140. package/src/runtime/capabilities-prompt.js +25 -25
  141. package/src/runtime/completion-ping.js +99 -99
  142. package/src/runtime/config-validator.js +121 -121
  143. package/src/runtime/context-ledger.js +360 -360
  144. package/src/runtime/cutover-readiness.js +42 -42
  145. package/src/runtime/daemon.js +729 -729
  146. package/src/runtime/delivery-ack.js +195 -195
  147. package/src/runtime/delivery-adapters/local-file.js +41 -41
  148. package/src/runtime/delivery-adapters/openclaw-cli.js +94 -94
  149. package/src/runtime/delivery-adapters/openclaw-peer.js +98 -98
  150. package/src/runtime/delivery-adapters/shadow.js +13 -13
  151. package/src/runtime/delivery-adapters/standalone-http.js +98 -98
  152. package/src/runtime/delivery-adapters/telegram.js +104 -104
  153. package/src/runtime/delivery-adapters/tui.js +128 -128
  154. package/src/runtime/delivery-manager.js +807 -807
  155. package/src/runtime/delivery-store.js +168 -168
  156. package/src/runtime/dependency-health.js +118 -118
  157. package/src/runtime/envelope.js +114 -114
  158. package/src/runtime/evaluation.js +1089 -1089
  159. package/src/runtime/exec-approvals.js +216 -216
  160. package/src/runtime/executor.js +500 -500
  161. package/src/runtime/failure-ping.js +67 -67
  162. package/src/runtime/flows.js +83 -83
  163. package/src/runtime/guards.js +45 -45
  164. package/src/runtime/handoff.js +51 -51
  165. package/src/runtime/identity-cache.js +28 -28
  166. package/src/runtime/improvement-engine.js +109 -109
  167. package/src/runtime/improvement-harness.js +581 -581
  168. package/src/runtime/input-sanitiser.js +72 -72
  169. package/src/runtime/interaction-contract.js +347 -347
  170. package/src/runtime/lane-readiness.js +226 -226
  171. package/src/runtime/migration.js +323 -323
  172. package/src/runtime/model-resolution.js +78 -78
  173. package/src/runtime/network.js +64 -64
  174. package/src/runtime/notification-store.js +97 -97
  175. package/src/runtime/notifier.js +256 -256
  176. package/src/runtime/orchestrator.js +53 -53
  177. package/src/runtime/orphan-reaper.js +41 -41
  178. package/src/runtime/output-contract-schema.js +139 -139
  179. package/src/runtime/output-contract-validator.js +439 -439
  180. package/src/runtime/peer-readiness.js +69 -69
  181. package/src/runtime/peer-registry.js +133 -133
  182. package/src/runtime/pilot-status.js +108 -108
  183. package/src/runtime/prompt-builder.js +261 -261
  184. package/src/runtime/provider-attempt.js +582 -582
  185. package/src/runtime/report-fallback.js +71 -71
  186. package/src/runtime/result-normalizer.js +183 -183
  187. package/src/runtime/retention.js +74 -74
  188. package/src/runtime/review.js +244 -244
  189. package/src/runtime/route-job.js +15 -15
  190. package/src/runtime/run-store.js +38 -38
  191. package/src/runtime/schedule.js +88 -88
  192. package/src/runtime/scheduler-state.js +434 -434
  193. package/src/runtime/scheduler.js +656 -656
  194. package/src/runtime/session-compactor.js +182 -182
  195. package/src/runtime/session-search.js +155 -155
  196. package/src/runtime/slack-inbound.js +249 -249
  197. package/src/runtime/ssrf.js +102 -102
  198. package/src/runtime/status-aggregator.js +330 -330
  199. package/src/runtime/task-contract.js +140 -140
  200. package/src/runtime/task-packet.js +107 -107
  201. package/src/runtime/task-router.js +140 -140
  202. package/src/runtime/telegram-inbound.js +1565 -1565
  203. package/src/runtime/token-counter.js +134 -134
  204. package/src/runtime/token-estimator.js +59 -59
  205. package/src/runtime/tool-loop.js +200 -200
  206. package/src/runtime/transport-server.js +311 -311
  207. package/src/runtime/tui-server.js +411 -411
  208. package/src/runtime/ulid.js +44 -44
  209. package/src/security/ssrf-check.js +197 -197
  210. package/src/setup.js +369 -369
  211. package/src/shadow/bridge.js +303 -303
  212. package/src/skills/loader.js +84 -84
  213. package/src/tools/catalog.json +49 -49
  214. package/src/tools/cli-delegate.js +44 -44
  215. package/src/tools/mcp-client.js +106 -106
  216. package/src/tools/micro/cancel-task.js +6 -6
  217. package/src/tools/micro/complete-task.js +6 -6
  218. package/src/tools/micro/fail-task.js +6 -6
  219. package/src/tools/micro/http-fetch.js +74 -74
  220. package/src/tools/micro/index.js +36 -36
  221. package/src/tools/micro/lcm-recall.js +60 -60
  222. package/src/tools/micro/list-dir.js +17 -17
  223. package/src/tools/micro/list-skills.js +46 -46
  224. package/src/tools/micro/load-skill.js +38 -38
  225. package/src/tools/micro/memory-search.js +45 -45
  226. package/src/tools/micro/read-file.js +11 -11
  227. package/src/tools/micro/session-search.js +54 -54
  228. package/src/tools/micro/shell-exec.js +43 -43
  229. package/src/tools/micro/trigger-job.js +79 -79
  230. package/src/tools/micro/web-search.js +58 -58
  231. package/src/tools/micro/workspace-paths.js +39 -39
  232. package/src/tools/micro/write-file.js +14 -14
  233. package/src/tools/micro/write-memory.js +41 -41
  234. package/src/tools/registry.js +348 -348
  235. package/src/tools/tool-result-contract.js +36 -36
  236. package/src/tui/chat.js +835 -835
  237. package/src/tui/renderer.js +175 -175
  238. package/src/tui/socket-client.js +217 -217
  239. package/src/utils/canonical-json.js +29 -29
  240. package/src/utils/compaction.js +30 -30
  241. package/src/utils/env-loader.js +5 -5
  242. package/src/utils/errors.js +80 -80
  243. package/src/utils/fs.js +101 -101
  244. package/src/utils/ids.js +5 -5
  245. package/src/utils/model-context-limits.js +30 -30
  246. package/src/utils/token-budget.js +74 -74
  247. package/src/utils/usage-cost.js +25 -25
  248. package/src/utils/usage-metrics.js +14 -14
@@ -1,500 +1,500 @@
1
- import path from "node:path";
2
- import crypto from "node:crypto";
3
- import { DatabaseSync } from "node:sqlite";
4
- import { mkdirSync } from "node:fs";
5
- import { Scheduler } from "./scheduler.js";
6
- import { ProviderRegistry } from "../providers/registry.js";
7
- import { RunStore } from "./run-store.js";
8
- import { getProviderModePolicy } from "./guards.js";
9
- import { buildInteractionPlan, formatInteractionContract } from "./interaction-contract.js";
10
- import { InteractionNotifier } from "./notifier.js";
11
- import { TokenCounter } from "./token-counter.js";
12
- import { PeerRegistry } from "./peer-registry.js";
13
- import { createRuntimeId } from "../utils/ids.js";
14
- import { processToolOutput } from "./input-sanitiser.js";
15
- import { ToolRegistry } from "../tools/registry.js";
16
- import { registerMicroToolHandlers } from "../tools/micro/index.js";
17
- import { ExecApprovalGate } from "./exec-approvals.js";
18
- import { CircuitBreaker, ensureBreakerSchema } from "../providers/circuit-breaker.js";
19
- import { SchedulerStateStore } from "./scheduler-state.js";
20
- import { buildCapabilitiesPrompt } from "./capabilities-prompt.js";
21
- import { attemptProvider } from "./provider-attempt.js";
22
- import { buildOutputTemplate, formatOutputContract, formatReportGuidance, normalizeResult } from "./result-normalizer.js";
23
- import { runToolLoop } from "./tool-loop.js";
24
-
25
- export { parseToolUseBlocks, formatToolResults, executeToolCalls } from "./tool-loop.js";
26
-
27
- function buildSystemPrompt(plan, options = {}) {
28
- const nativeStructuredOutput = options.nativeStructuredOutput ?? false;
29
- const responseSchema = options.responseSchema || null;
30
- const toolSchemas = options.toolSchemas || [];
31
- const isInteractive = plan.job.trigger === "interactive";
32
-
33
- const toolText = plan.packet.layers.tools.length
34
- ? `Allowed tools: ${plan.packet.layers.tools.join(", ")}.`
35
- : "No tools allowed.";
36
- const soulText = plan.packet.layers.identity?.soul
37
- ? `${plan.packet.layers.identity.soul.trim()}`
38
- : "";
39
- const purposeText = plan.packet.layers.identity?.purpose
40
- ? `## Purpose\n${plan.packet.layers.identity.purpose.trim()}`
41
- : "";
42
- const userText = plan.packet.layers.identity?.user
43
- ? `${plan.packet.layers.identity.user.trim()}`
44
- : "";
45
-
46
- const contextFileTexts = (plan.packet.layers.identity?.contextFiles || [])
47
- .map(({ name, content }) => `---\n## ${name}\n\n${content.trim()}\n---`)
48
- .join("\n\n");
49
-
50
- if (isInteractive) {
51
- return [
52
- soulText,
53
- userText,
54
- purposeText,
55
- contextFileTexts || null,
56
- buildCapabilitiesPrompt(toolSchemas),
57
- "Respond in plain conversational language. Do not output JSON, structured task packets, or completion sections.",
58
- "Be concise, direct, and helpful. Match the tone of the user's message.",
59
- "If the user asks a question, answer it. If they give a task, do it and report back naturally.",
60
- toolText,
61
- ]
62
- .filter(Boolean)
63
- .join("\n\n");
64
- }
65
-
66
- const interactionText = plan.packet.layers.interactionContract
67
- ? `Interaction contract:\n${formatInteractionContract(plan.packet.layers.interactionContract)}`
68
- : "";
69
- const contractText = plan.packet.layers.outputContract
70
- ? `Output contract:\n${formatOutputContract(plan.packet.layers.outputContract)}`
71
- : "";
72
- const guidanceText = plan.packet.layers.reportGuidance
73
- ? `Report guidance:\n${formatReportGuidance(plan.packet.layers.reportGuidance)}`
74
- : "";
75
- const templateText = !nativeStructuredOutput && plan.packet.layers.outputContract
76
- ? buildOutputTemplate(plan.packet.layers.outputContract)
77
- : null;
78
- const nativeSchemaText = nativeStructuredOutput && plan.packet.layers.outputContract
79
- ? "Native structured output is enabled for this provider. Follow the contract exactly and return only schema-valid content."
80
- : null;
81
- const reasoningFieldText = nativeStructuredOutput && responseSchema?.reasoningField
82
- ? `Use the ${responseSchema.reasoningField} field for compact reasoning before finalizing the structured output.`
83
- : null;
84
-
85
- return [
86
- "You are Nemoris V2 executing a bounded shadow-mode job.",
87
- "Use only the supplied task packet.",
88
- "Do not assume the transcript is the system of record.",
89
- "Summarize the result tightly and list the next actions.",
90
- toolText,
91
- soulText,
92
- purposeText,
93
- interactionText,
94
- contractText,
95
- guidanceText,
96
- nativeSchemaText,
97
- reasoningFieldText,
98
- templateText ? `Follow this output template exactly when possible:\n${templateText}` : null
99
- ]
100
- .filter(Boolean)
101
- .join("\n\n");
102
- }
103
-
104
- function buildUserMessage(plan, options = {}) {
105
- const nativeStructuredOutput = options.nativeStructuredOutput ?? false;
106
- const responseSchema = options.responseSchema || null;
107
- const isInteractive = plan.job.trigger === "interactive";
108
-
109
- const memoryLines = plan.packet.layers.memory
110
- .map((item) => {
111
- const primaryText = item.summary || item.content || item.reason || "";
112
- const rawLine = `- [${item.type}/${item.category || "uncategorized"}] ${item.title || "memory"}: ${primaryText}`;
113
- const source = item.sourceBackend || item.category || "memory";
114
- const { tagged } = processToolOutput(rawLine, source);
115
- return tagged;
116
- })
117
- .join("\n");
118
-
119
- if (isInteractive) {
120
- const parts = [plan.packet.objective];
121
- if (plan.packet.conversationContext?.length) {
122
- const contextLines = plan.packet.conversationContext
123
- .map((turn) => `${turn.role}: ${turn.content}`)
124
- .join("\n");
125
- parts.unshift(`Previous conversation:\n${contextLines}`);
126
- }
127
- if (memoryLines) {
128
- parts.push(`\n(Context from memory:\n${memoryLines})`);
129
- }
130
- return parts.join("\n\n");
131
- }
132
-
133
- const artifactLines = plan.packet.layers.artifacts.length
134
- ? `Artifacts:\n${plan.packet.layers.artifacts.map((item) => {
135
- const { tagged } = processToolOutput(`- ${item}`, "artifact");
136
- return tagged;
137
- }).join("\n")}`
138
- : "Artifacts: none";
139
- const contractReminder = !nativeStructuredOutput && plan.packet.layers.outputContract?.requiredSections?.length
140
- ? `Required sections: ${plan.packet.layers.outputContract.requiredSections.join(", ")}.`
141
- : "Required sections: none.";
142
- const noneReminder = !nativeStructuredOutput && plan.packet.layers.outputContract?.requiredSections?.length
143
- ? "If a required section has no concrete data, include the section anyway and say 'None'."
144
- : null;
145
- const schemaReminder = nativeStructuredOutput
146
- ? "Return only the structured JSON object required by the provider schema."
147
- : "Return JSON with keys: summary, output, nextActions.";
148
- const reasoningReminder = nativeStructuredOutput && responseSchema?.reasoningField
149
- ? `Put concise reasoning in ${responseSchema.reasoningField} before the final output fields.`
150
- : null;
151
-
152
- return [
153
- `Objective: ${plan.packet.objective}`,
154
- `Budget: ${plan.packet.budget.maxTokens} tokens, ${plan.packet.budget.maxRuntimeSeconds}s`,
155
- plan.packet.layers.checkpoint
156
- ? `Checkpoint: status=${plan.packet.layers.checkpoint.status}; next=${(plan.packet.layers.checkpoint.nextActions || []).join(", ") || "none"}`
157
- : "Checkpoint: none",
158
- memoryLines ? `Relevant memory:\n${memoryLines}` : "Relevant memory: none",
159
- artifactLines,
160
- contractReminder,
161
- schemaReminder,
162
- reasoningReminder,
163
- "The output field must satisfy the system output contract.",
164
- noneReminder,
165
- "Prefer specific evidence from memory over generic reassurance.",
166
- "The completion summary must be suitable for an explicit pingback and handoff."
167
- ]
168
- .filter(Boolean)
169
- .join("\n\n");
170
- }
171
-
172
- export class Executor {
173
- constructor({ projectRoot, liveRoot, stateRoot, fetchImpl, execApprovalGate = null } = {}) {
174
- this.projectRoot = projectRoot;
175
- this.liveRoot = liveRoot;
176
- this.stateRoot = stateRoot;
177
- this.scheduler = new Scheduler({ projectRoot, liveRoot, stateRoot });
178
- this.registry = new ProviderRegistry({ fetchImpl });
179
- this.runStore = new RunStore({ rootDir: path.join(stateRoot, "runs") });
180
- this.notifier = new InteractionNotifier({ stateRoot });
181
- this.tokenCounter = new TokenCounter();
182
- this.toolRegistry = new ToolRegistry();
183
- this.stateStore = new SchedulerStateStore({ rootDir: path.join(stateRoot, "scheduler") });
184
- this._breakerDb = null;
185
- this._breakers = new Map();
186
- this.execApprovalGate = execApprovalGate || new ExecApprovalGate({ sendFn: async () => {} });
187
- }
188
-
189
- async initToolRegistry() {
190
- if (this._toolsInitialized) return;
191
- this._toolContext = {};
192
- registerMicroToolHandlers(this.toolRegistry, this._toolContext);
193
- try {
194
- const { join } = await import("node:path");
195
- await this.toolRegistry.loadTools(join(this.projectRoot, "config", "tools"));
196
- } catch {
197
- /* no tools dir is fine */
198
- }
199
- this._toolsInitialized = true;
200
- }
201
-
202
- _getBreaker(modelId, runtimeConfig) {
203
- if (!this._breakerDb) {
204
- const schedulerDir = path.join(this.stateRoot, "scheduler");
205
- mkdirSync(schedulerDir, { recursive: true });
206
- const dbPath = path.join(schedulerDir, "scheduler.sqlite");
207
- this._breakerDb = new DatabaseSync(dbPath, { timeout: 5000 });
208
- ensureBreakerSchema(this._breakerDb);
209
- }
210
- if (this._breakers.has(modelId)) return this._breakers.get(modelId);
211
- const cbConfig = runtimeConfig?.circuit_breaker || runtimeConfig?.circuitBreaker || {};
212
- const breaker = CircuitBreaker.forProvider(modelId, this._breakerDb, {
213
- failureThreshold: cbConfig.failure_threshold ?? 5,
214
- resetTimeoutSeconds: cbConfig.reset_timeout_seconds ?? 30,
215
- transientCodes: cbConfig.transient_codes ?? [408, 429, 500, 502, 503, 504]
216
- });
217
- this._breakers.set(modelId, breaker);
218
- return breaker;
219
- }
220
-
221
- _logUsage({ sessionId, jobId, modelId, providerId, usage, costUsd }) {
222
- this.stateStore.db.prepare(`INSERT INTO usage_log (id, session_id, job_id, ts, model, tokens_in, tokens_out, cost_usd, provider, cache_in, cache_creation)
223
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
224
- .run(
225
- crypto.randomUUID(),
226
- sessionId,
227
- jobId,
228
- Date.now(),
229
- modelId,
230
- usage.tokensIn,
231
- usage.tokensOut,
232
- costUsd,
233
- providerId,
234
- usage.cacheIn,
235
- usage.cacheCreation
236
- );
237
- }
238
-
239
- setExecApprovalGate(execApprovalGate) {
240
- if (execApprovalGate) {
241
- this.execApprovalGate = execApprovalGate;
242
- }
243
- }
244
-
245
- async executeJob(jobId, options = {}) {
246
- const mode = options.mode || "dry-run";
247
- let plan = null;
248
- let routingDecision = null;
249
- let modelId = null;
250
- let providerId = null;
251
- let providerCapabilities = null;
252
- let invocation = null;
253
- let preflight = null;
254
- let fallback = null;
255
- let health = null;
256
- let providerModePolicy = null;
257
- let result = null;
258
- const runId = createRuntimeId("run");
259
-
260
- try {
261
- await this.initToolRegistry();
262
- await this.stateStore.ensureReady();
263
- plan = options.plan || await this.scheduler.hydrateJob(jobId, {
264
- shadowImport: options.shadowImport ?? true,
265
- objective: options.objective,
266
- artifactRefs: options.artifactRefs,
267
- memoryLimit: options.memoryLimit,
268
- reportGuidanceOverride: options.reportGuidanceOverride,
269
- retrievalBlendOverride: options.retrievalBlendOverride
270
- });
271
- const runtime = await this.scheduler.loadRuntime();
272
- if (this._toolContext) {
273
- this._toolContext.workspaceRoot = plan.agent?.workspaceRoot || this.projectRoot;
274
- }
275
- this.runStore.setRetentionPolicy(runtime.runtime?.retention?.runs || {});
276
- this.notifier.configureRetention(runtime.runtime?.retention?.notifications || {});
277
- const peerRegistry = new PeerRegistry(runtime.peers);
278
- const agentPolicy = plan.policies?.tools?.[plan.agent?.tool_policy]
279
- || { default: "deny", allowed: plan.packet.layers.tools || [], blocked: [] };
280
- const toolSchemas = this.toolRegistry.schemasForAgent(plan.agent?.id, agentPolicy);
281
-
282
- const providerAttempt = await attemptProvider({
283
- plan,
284
- runtime,
285
- options: {
286
- ...options,
287
- mode,
288
- registry: this.registry,
289
- tokenCounter: this.tokenCounter,
290
- toolSchemas,
291
- buildSystemPrompt,
292
- buildUserMessage,
293
- getBreaker: this._getBreaker.bind(this)
294
- }
295
- });
296
-
297
- routingDecision = providerAttempt.routingDecision;
298
- modelId = providerAttempt.modelId;
299
- providerId = providerAttempt.providerId;
300
- providerCapabilities = providerAttempt.providerCapabilities;
301
- invocation = providerAttempt.invocation;
302
- preflight = providerAttempt.preflight;
303
- fallback = providerAttempt.fallback;
304
- health = providerAttempt.health;
305
- providerModePolicy = providerAttempt.providerModePolicy;
306
-
307
- const toolLoop = await runToolLoop({
308
- initialResponse: providerAttempt.response,
309
- adapter: providerAttempt.adapter,
310
- invocation,
311
- registry: this.toolRegistry,
312
- policy: agentPolicy,
313
- agentConfig: plan.agent,
314
- approvalGate: this.execApprovalGate,
315
- options: {
316
- enabled: providerAttempt.toolLoopEnabled,
317
- jobId: plan.job.id,
318
- agentId: plan.agent?.id,
319
- maxToolCalls: plan.packet.budget?.maxToolCalls || plan.job?.limits?.max_tool_calls_per_turn || 20,
320
- skipApprovalNotification: options.skipApprovalNotification === true,
321
- }
322
- });
323
-
324
- const ackNotification = await this.notifier.queueAck(plan, {
325
- runId,
326
- mode,
327
- providerId,
328
- providerCapabilities,
329
- modelId,
330
- routingDecision
331
- });
332
-
333
- const normalized = await normalizeResult({
334
- plan,
335
- rawResponse: toolLoop.finalResponse,
336
- toolResults: toolLoop.toolResults,
337
- usage: providerAttempt.toolLoopEnabled ? undefined : providerAttempt.usage,
338
- options: {
339
- mode,
340
- adapter: providerAttempt.adapter,
341
- modelId,
342
- providerId,
343
- pendingApprovalResult: toolLoop.pendingApprovalResult || null,
344
- sessionId: options.sessionId,
345
- logUsage: ({ sessionId, usage, costUsd }) => this._logUsage({
346
- sessionId,
347
- jobId: plan.job.id,
348
- modelId,
349
- providerId,
350
- usage,
351
- costUsd
352
- })
353
- }
354
- });
355
- result = normalized.result;
356
-
357
- const handoffConfig = plan.packet.layers.interactionContract?.handoff || null;
358
- const handoffSuggestions =
359
- handoffConfig?.enabled
360
- ? peerRegistry.suggestPeers({
361
- taskType: plan.job.taskType,
362
- query: handoffConfig.capabilityQuery || plan.job.taskType || plan.job.id,
363
- preferredDeliveryProfile: plan.agent?.deliveryProfile || null,
364
- trustLevel: handoffConfig.trustLevel || null,
365
- limit: handoffConfig.maxSuggestions || 3
366
- })
367
- : [];
368
- const interaction = buildInteractionPlan(plan, result, null, {
369
- handoffSuggestions
370
- });
371
- const run = {
372
- id: runId,
373
- timestamp: new Date().toISOString(),
374
- mode,
375
- providerId,
376
- providerCapabilities,
377
- modelId,
378
- routingDecision,
379
- providerModePolicy,
380
- fallback,
381
- providerHealth: health,
382
- preflight,
383
- invocation,
384
- plan,
385
- result,
386
- usage: normalized.usage,
387
- costUsd: normalized.costUsd,
388
- cached: normalized.cached,
389
- toolCalls: toolLoop.toolResults,
390
- pendingApproval: Boolean(toolLoop.pendingApprovalResult),
391
- interaction,
392
- idempotencyKey: options.idempotencyKey || null
393
- };
394
-
395
- const notificationFiles = [ackNotification?.filePath].filter(Boolean);
396
- if (interaction?.yield?.required) {
397
- const followUpNotification = await this.notifier.queueFollowUp(run);
398
- run.followUp = interaction.yield;
399
- run.followUpFile = followUpNotification?.filePath || null;
400
- notificationFiles.push(followUpNotification?.filePath);
401
- } else {
402
- const completionNotification = await this.notifier.queueCompletion(run);
403
- const handoffNotification = await this.notifier.queueHandoff(run);
404
- notificationFiles.push(completionNotification?.filePath, handoffNotification?.filePath);
405
- }
406
- run.notificationFiles = notificationFiles.filter(Boolean);
407
-
408
- const filePath = await this.runStore.saveRun(jobId, run);
409
- return {
410
- filePath,
411
- ...run
412
- };
413
- } catch (error) {
414
- if (error?.routingDecision) routingDecision = error.routingDecision;
415
- if (error?.providerId) providerId = error.providerId;
416
- if (error?.modelId) modelId = error.modelId;
417
- if (error?.providerCapabilities) providerCapabilities = error.providerCapabilities;
418
- if (error?.invocation) invocation = error.invocation;
419
- if (error?.preflight) preflight = error.preflight;
420
- if (error?.fallback) fallback = error.fallback;
421
-
422
- if (plan) {
423
- const errorNotification = await this.notifier.queueError({
424
- plan,
425
- error,
426
- context: {
427
- runId,
428
- mode,
429
- providerId,
430
- providerCapabilities,
431
- modelId,
432
- routingDecision,
433
- timestamp: new Date().toISOString()
434
- }
435
- });
436
-
437
- const failedRun = {
438
- id: runId,
439
- timestamp: new Date().toISOString(),
440
- mode,
441
- providerId,
442
- providerCapabilities,
443
- modelId,
444
- routingDecision,
445
- providerModePolicy: mode === "provider" ? getProviderModePolicy() : null,
446
- fallback,
447
- preflight,
448
- invocation,
449
- plan,
450
- error: {
451
- message: error.message
452
- },
453
- notificationFiles: [errorNotification?.filePath].filter(Boolean)
454
- };
455
-
456
- const filePath = await this.runStore.saveRun(jobId, failedRun);
457
- error.runFile = filePath;
458
- }
459
-
460
- throw error;
461
- }
462
- }
463
-
464
- async shadowCompare(jobId) {
465
- const [run, jobComparisons] = await Promise.all([
466
- this.executeJob(jobId, { mode: "dry-run", shadowImport: true }),
467
- this.scheduler.compareJobs()
468
- ]);
469
-
470
- const relatedLive = jobComparisons.find((job) => job.v2JobId === jobId)?.closestLiveJobs || [];
471
-
472
- const comparison = {
473
- timestamp: new Date().toISOString(),
474
- jobId,
475
- v2Run: {
476
- mode: run.mode,
477
- providerId: run.providerId,
478
- modelId: run.modelId,
479
- routingDecision: run.routingDecision,
480
- summary: run.result.summary,
481
- output: run.result.output,
482
- providerHealth: run.providerHealth,
483
- runFile: run.filePath
484
- },
485
- liveCandidates: relatedLive,
486
- notes: [
487
- relatedLive.length === 0
488
- ? "No close live cron analogue found."
489
- : `Found ${relatedLive.length} possible live cron analogues.`,
490
- "Use provider mode only after dry-run shape looks correct."
491
- ]
492
- };
493
-
494
- const filePath = await this.runStore.saveRun(`${jobId}-comparison`, comparison);
495
- return {
496
- filePath,
497
- ...comparison
498
- };
499
- }
500
- }
1
+ import path from "node:path";
2
+ import crypto from "node:crypto";
3
+ import { DatabaseSync } from "node:sqlite";
4
+ import { mkdirSync } from "node:fs";
5
+ import { Scheduler } from "./scheduler.js";
6
+ import { ProviderRegistry } from "../providers/registry.js";
7
+ import { RunStore } from "./run-store.js";
8
+ import { getProviderModePolicy } from "./guards.js";
9
+ import { buildInteractionPlan, formatInteractionContract } from "./interaction-contract.js";
10
+ import { InteractionNotifier } from "./notifier.js";
11
+ import { TokenCounter } from "./token-counter.js";
12
+ import { PeerRegistry } from "./peer-registry.js";
13
+ import { createRuntimeId } from "../utils/ids.js";
14
+ import { processToolOutput } from "./input-sanitiser.js";
15
+ import { ToolRegistry } from "../tools/registry.js";
16
+ import { registerMicroToolHandlers } from "../tools/micro/index.js";
17
+ import { ExecApprovalGate } from "./exec-approvals.js";
18
+ import { CircuitBreaker, ensureBreakerSchema } from "../providers/circuit-breaker.js";
19
+ import { SchedulerStateStore } from "./scheduler-state.js";
20
+ import { buildCapabilitiesPrompt } from "./capabilities-prompt.js";
21
+ import { attemptProvider } from "./provider-attempt.js";
22
+ import { buildOutputTemplate, formatOutputContract, formatReportGuidance, normalizeResult } from "./result-normalizer.js";
23
+ import { runToolLoop } from "./tool-loop.js";
24
+
25
+ export { parseToolUseBlocks, formatToolResults, executeToolCalls } from "./tool-loop.js";
26
+
27
+ function buildSystemPrompt(plan, options = {}) {
28
+ const nativeStructuredOutput = options.nativeStructuredOutput ?? false;
29
+ const responseSchema = options.responseSchema || null;
30
+ const toolSchemas = options.toolSchemas || [];
31
+ const isInteractive = plan.job.trigger === "interactive";
32
+
33
+ const toolText = plan.packet.layers.tools.length
34
+ ? `Allowed tools: ${plan.packet.layers.tools.join(", ")}.`
35
+ : "No tools allowed.";
36
+ const soulText = plan.packet.layers.identity?.soul
37
+ ? `${plan.packet.layers.identity.soul.trim()}`
38
+ : "";
39
+ const purposeText = plan.packet.layers.identity?.purpose
40
+ ? `## Purpose\n${plan.packet.layers.identity.purpose.trim()}`
41
+ : "";
42
+ const userText = plan.packet.layers.identity?.user
43
+ ? `${plan.packet.layers.identity.user.trim()}`
44
+ : "";
45
+
46
+ const contextFileTexts = (plan.packet.layers.identity?.contextFiles || [])
47
+ .map(({ name, content }) => `---\n## ${name}\n\n${content.trim()}\n---`)
48
+ .join("\n\n");
49
+
50
+ if (isInteractive) {
51
+ return [
52
+ soulText,
53
+ userText,
54
+ purposeText,
55
+ contextFileTexts || null,
56
+ buildCapabilitiesPrompt(toolSchemas),
57
+ "Respond in plain conversational language. Do not output JSON, structured task packets, or completion sections.",
58
+ "Be concise, direct, and helpful. Match the tone of the user's message.",
59
+ "If the user asks a question, answer it. If they give a task, do it and report back naturally.",
60
+ toolText,
61
+ ]
62
+ .filter(Boolean)
63
+ .join("\n\n");
64
+ }
65
+
66
+ const interactionText = plan.packet.layers.interactionContract
67
+ ? `Interaction contract:\n${formatInteractionContract(plan.packet.layers.interactionContract)}`
68
+ : "";
69
+ const contractText = plan.packet.layers.outputContract
70
+ ? `Output contract:\n${formatOutputContract(plan.packet.layers.outputContract)}`
71
+ : "";
72
+ const guidanceText = plan.packet.layers.reportGuidance
73
+ ? `Report guidance:\n${formatReportGuidance(plan.packet.layers.reportGuidance)}`
74
+ : "";
75
+ const templateText = !nativeStructuredOutput && plan.packet.layers.outputContract
76
+ ? buildOutputTemplate(plan.packet.layers.outputContract)
77
+ : null;
78
+ const nativeSchemaText = nativeStructuredOutput && plan.packet.layers.outputContract
79
+ ? "Native structured output is enabled for this provider. Follow the contract exactly and return only schema-valid content."
80
+ : null;
81
+ const reasoningFieldText = nativeStructuredOutput && responseSchema?.reasoningField
82
+ ? `Use the ${responseSchema.reasoningField} field for compact reasoning before finalizing the structured output.`
83
+ : null;
84
+
85
+ return [
86
+ "You are Nemoris V2 executing a bounded shadow-mode job.",
87
+ "Use only the supplied task packet.",
88
+ "Do not assume the transcript is the system of record.",
89
+ "Summarize the result tightly and list the next actions.",
90
+ toolText,
91
+ soulText,
92
+ purposeText,
93
+ interactionText,
94
+ contractText,
95
+ guidanceText,
96
+ nativeSchemaText,
97
+ reasoningFieldText,
98
+ templateText ? `Follow this output template exactly when possible:\n${templateText}` : null
99
+ ]
100
+ .filter(Boolean)
101
+ .join("\n\n");
102
+ }
103
+
104
+ function buildUserMessage(plan, options = {}) {
105
+ const nativeStructuredOutput = options.nativeStructuredOutput ?? false;
106
+ const responseSchema = options.responseSchema || null;
107
+ const isInteractive = plan.job.trigger === "interactive";
108
+
109
+ const memoryLines = plan.packet.layers.memory
110
+ .map((item) => {
111
+ const primaryText = item.summary || item.content || item.reason || "";
112
+ const rawLine = `- [${item.type}/${item.category || "uncategorized"}] ${item.title || "memory"}: ${primaryText}`;
113
+ const source = item.sourceBackend || item.category || "memory";
114
+ const { tagged } = processToolOutput(rawLine, source);
115
+ return tagged;
116
+ })
117
+ .join("\n");
118
+
119
+ if (isInteractive) {
120
+ const parts = [plan.packet.objective];
121
+ if (plan.packet.conversationContext?.length) {
122
+ const contextLines = plan.packet.conversationContext
123
+ .map((turn) => `${turn.role}: ${turn.content}`)
124
+ .join("\n");
125
+ parts.unshift(`Previous conversation:\n${contextLines}`);
126
+ }
127
+ if (memoryLines) {
128
+ parts.push(`\n(Context from memory:\n${memoryLines})`);
129
+ }
130
+ return parts.join("\n\n");
131
+ }
132
+
133
+ const artifactLines = plan.packet.layers.artifacts.length
134
+ ? `Artifacts:\n${plan.packet.layers.artifacts.map((item) => {
135
+ const { tagged } = processToolOutput(`- ${item}`, "artifact");
136
+ return tagged;
137
+ }).join("\n")}`
138
+ : "Artifacts: none";
139
+ const contractReminder = !nativeStructuredOutput && plan.packet.layers.outputContract?.requiredSections?.length
140
+ ? `Required sections: ${plan.packet.layers.outputContract.requiredSections.join(", ")}.`
141
+ : "Required sections: none.";
142
+ const noneReminder = !nativeStructuredOutput && plan.packet.layers.outputContract?.requiredSections?.length
143
+ ? "If a required section has no concrete data, include the section anyway and say 'None'."
144
+ : null;
145
+ const schemaReminder = nativeStructuredOutput
146
+ ? "Return only the structured JSON object required by the provider schema."
147
+ : "Return JSON with keys: summary, output, nextActions.";
148
+ const reasoningReminder = nativeStructuredOutput && responseSchema?.reasoningField
149
+ ? `Put concise reasoning in ${responseSchema.reasoningField} before the final output fields.`
150
+ : null;
151
+
152
+ return [
153
+ `Objective: ${plan.packet.objective}`,
154
+ `Budget: ${plan.packet.budget.maxTokens} tokens, ${plan.packet.budget.maxRuntimeSeconds}s`,
155
+ plan.packet.layers.checkpoint
156
+ ? `Checkpoint: status=${plan.packet.layers.checkpoint.status}; next=${(plan.packet.layers.checkpoint.nextActions || []).join(", ") || "none"}`
157
+ : "Checkpoint: none",
158
+ memoryLines ? `Relevant memory:\n${memoryLines}` : "Relevant memory: none",
159
+ artifactLines,
160
+ contractReminder,
161
+ schemaReminder,
162
+ reasoningReminder,
163
+ "The output field must satisfy the system output contract.",
164
+ noneReminder,
165
+ "Prefer specific evidence from memory over generic reassurance.",
166
+ "The completion summary must be suitable for an explicit pingback and handoff."
167
+ ]
168
+ .filter(Boolean)
169
+ .join("\n\n");
170
+ }
171
+
172
+ export class Executor {
173
+ constructor({ projectRoot, liveRoot, stateRoot, fetchImpl, execApprovalGate = null } = {}) {
174
+ this.projectRoot = projectRoot;
175
+ this.liveRoot = liveRoot;
176
+ this.stateRoot = stateRoot;
177
+ this.scheduler = new Scheduler({ projectRoot, liveRoot, stateRoot });
178
+ this.registry = new ProviderRegistry({ fetchImpl });
179
+ this.runStore = new RunStore({ rootDir: path.join(stateRoot, "runs") });
180
+ this.notifier = new InteractionNotifier({ stateRoot });
181
+ this.tokenCounter = new TokenCounter();
182
+ this.toolRegistry = new ToolRegistry();
183
+ this.stateStore = new SchedulerStateStore({ rootDir: path.join(stateRoot, "scheduler") });
184
+ this._breakerDb = null;
185
+ this._breakers = new Map();
186
+ this.execApprovalGate = execApprovalGate || new ExecApprovalGate({ sendFn: async () => {} });
187
+ }
188
+
189
+ async initToolRegistry() {
190
+ if (this._toolsInitialized) return;
191
+ this._toolContext = {};
192
+ registerMicroToolHandlers(this.toolRegistry, this._toolContext);
193
+ try {
194
+ const { join } = await import("node:path");
195
+ await this.toolRegistry.loadTools(join(this.projectRoot, "config", "tools"));
196
+ } catch {
197
+ /* no tools dir is fine */
198
+ }
199
+ this._toolsInitialized = true;
200
+ }
201
+
202
+ _getBreaker(modelId, runtimeConfig) {
203
+ if (!this._breakerDb) {
204
+ const schedulerDir = path.join(this.stateRoot, "scheduler");
205
+ mkdirSync(schedulerDir, { recursive: true });
206
+ const dbPath = path.join(schedulerDir, "scheduler.sqlite");
207
+ this._breakerDb = new DatabaseSync(dbPath, { timeout: 5000 });
208
+ ensureBreakerSchema(this._breakerDb);
209
+ }
210
+ if (this._breakers.has(modelId)) return this._breakers.get(modelId);
211
+ const cbConfig = runtimeConfig?.circuit_breaker || runtimeConfig?.circuitBreaker || {};
212
+ const breaker = CircuitBreaker.forProvider(modelId, this._breakerDb, {
213
+ failureThreshold: cbConfig.failure_threshold ?? 5,
214
+ resetTimeoutSeconds: cbConfig.reset_timeout_seconds ?? 30,
215
+ transientCodes: cbConfig.transient_codes ?? [408, 429, 500, 502, 503, 504]
216
+ });
217
+ this._breakers.set(modelId, breaker);
218
+ return breaker;
219
+ }
220
+
221
+ _logUsage({ sessionId, jobId, modelId, providerId, usage, costUsd }) {
222
+ this.stateStore.db.prepare(`INSERT INTO usage_log (id, session_id, job_id, ts, model, tokens_in, tokens_out, cost_usd, provider, cache_in, cache_creation)
223
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
224
+ .run(
225
+ crypto.randomUUID(),
226
+ sessionId,
227
+ jobId,
228
+ Date.now(),
229
+ modelId,
230
+ usage.tokensIn,
231
+ usage.tokensOut,
232
+ costUsd,
233
+ providerId,
234
+ usage.cacheIn,
235
+ usage.cacheCreation
236
+ );
237
+ }
238
+
239
+ setExecApprovalGate(execApprovalGate) {
240
+ if (execApprovalGate) {
241
+ this.execApprovalGate = execApprovalGate;
242
+ }
243
+ }
244
+
245
+ async executeJob(jobId, options = {}) {
246
+ const mode = options.mode || "dry-run";
247
+ let plan = null;
248
+ let routingDecision = null;
249
+ let modelId = null;
250
+ let providerId = null;
251
+ let providerCapabilities = null;
252
+ let invocation = null;
253
+ let preflight = null;
254
+ let fallback = null;
255
+ let health = null;
256
+ let providerModePolicy = null;
257
+ let result = null;
258
+ const runId = createRuntimeId("run");
259
+
260
+ try {
261
+ await this.initToolRegistry();
262
+ await this.stateStore.ensureReady();
263
+ plan = options.plan || await this.scheduler.hydrateJob(jobId, {
264
+ shadowImport: options.shadowImport ?? true,
265
+ objective: options.objective,
266
+ artifactRefs: options.artifactRefs,
267
+ memoryLimit: options.memoryLimit,
268
+ reportGuidanceOverride: options.reportGuidanceOverride,
269
+ retrievalBlendOverride: options.retrievalBlendOverride
270
+ });
271
+ const runtime = await this.scheduler.loadRuntime();
272
+ if (this._toolContext) {
273
+ this._toolContext.workspaceRoot = plan.agent?.workspaceRoot || this.projectRoot;
274
+ }
275
+ this.runStore.setRetentionPolicy(runtime.runtime?.retention?.runs || {});
276
+ this.notifier.configureRetention(runtime.runtime?.retention?.notifications || {});
277
+ const peerRegistry = new PeerRegistry(runtime.peers);
278
+ const agentPolicy = plan.policies?.tools?.[plan.agent?.tool_policy]
279
+ || { default: "deny", allowed: plan.packet.layers.tools || [], blocked: [] };
280
+ const toolSchemas = this.toolRegistry.schemasForAgent(plan.agent?.id, agentPolicy);
281
+
282
+ const providerAttempt = await attemptProvider({
283
+ plan,
284
+ runtime,
285
+ options: {
286
+ ...options,
287
+ mode,
288
+ registry: this.registry,
289
+ tokenCounter: this.tokenCounter,
290
+ toolSchemas,
291
+ buildSystemPrompt,
292
+ buildUserMessage,
293
+ getBreaker: this._getBreaker.bind(this)
294
+ }
295
+ });
296
+
297
+ routingDecision = providerAttempt.routingDecision;
298
+ modelId = providerAttempt.modelId;
299
+ providerId = providerAttempt.providerId;
300
+ providerCapabilities = providerAttempt.providerCapabilities;
301
+ invocation = providerAttempt.invocation;
302
+ preflight = providerAttempt.preflight;
303
+ fallback = providerAttempt.fallback;
304
+ health = providerAttempt.health;
305
+ providerModePolicy = providerAttempt.providerModePolicy;
306
+
307
+ const toolLoop = await runToolLoop({
308
+ initialResponse: providerAttempt.response,
309
+ adapter: providerAttempt.adapter,
310
+ invocation,
311
+ registry: this.toolRegistry,
312
+ policy: agentPolicy,
313
+ agentConfig: plan.agent,
314
+ approvalGate: this.execApprovalGate,
315
+ options: {
316
+ enabled: providerAttempt.toolLoopEnabled,
317
+ jobId: plan.job.id,
318
+ agentId: plan.agent?.id,
319
+ maxToolCalls: plan.packet.budget?.maxToolCalls || plan.job?.limits?.max_tool_calls_per_turn || 20,
320
+ skipApprovalNotification: options.skipApprovalNotification === true,
321
+ }
322
+ });
323
+
324
+ const ackNotification = await this.notifier.queueAck(plan, {
325
+ runId,
326
+ mode,
327
+ providerId,
328
+ providerCapabilities,
329
+ modelId,
330
+ routingDecision
331
+ });
332
+
333
+ const normalized = await normalizeResult({
334
+ plan,
335
+ rawResponse: toolLoop.finalResponse,
336
+ toolResults: toolLoop.toolResults,
337
+ usage: providerAttempt.toolLoopEnabled ? undefined : providerAttempt.usage,
338
+ options: {
339
+ mode,
340
+ adapter: providerAttempt.adapter,
341
+ modelId,
342
+ providerId,
343
+ pendingApprovalResult: toolLoop.pendingApprovalResult || null,
344
+ sessionId: options.sessionId,
345
+ logUsage: ({ sessionId, usage, costUsd }) => this._logUsage({
346
+ sessionId,
347
+ jobId: plan.job.id,
348
+ modelId,
349
+ providerId,
350
+ usage,
351
+ costUsd
352
+ })
353
+ }
354
+ });
355
+ result = normalized.result;
356
+
357
+ const handoffConfig = plan.packet.layers.interactionContract?.handoff || null;
358
+ const handoffSuggestions =
359
+ handoffConfig?.enabled
360
+ ? peerRegistry.suggestPeers({
361
+ taskType: plan.job.taskType,
362
+ query: handoffConfig.capabilityQuery || plan.job.taskType || plan.job.id,
363
+ preferredDeliveryProfile: plan.agent?.deliveryProfile || null,
364
+ trustLevel: handoffConfig.trustLevel || null,
365
+ limit: handoffConfig.maxSuggestions || 3
366
+ })
367
+ : [];
368
+ const interaction = buildInteractionPlan(plan, result, null, {
369
+ handoffSuggestions
370
+ });
371
+ const run = {
372
+ id: runId,
373
+ timestamp: new Date().toISOString(),
374
+ mode,
375
+ providerId,
376
+ providerCapabilities,
377
+ modelId,
378
+ routingDecision,
379
+ providerModePolicy,
380
+ fallback,
381
+ providerHealth: health,
382
+ preflight,
383
+ invocation,
384
+ plan,
385
+ result,
386
+ usage: normalized.usage,
387
+ costUsd: normalized.costUsd,
388
+ cached: normalized.cached,
389
+ toolCalls: toolLoop.toolResults,
390
+ pendingApproval: Boolean(toolLoop.pendingApprovalResult),
391
+ interaction,
392
+ idempotencyKey: options.idempotencyKey || null
393
+ };
394
+
395
+ const notificationFiles = [ackNotification?.filePath].filter(Boolean);
396
+ if (interaction?.yield?.required) {
397
+ const followUpNotification = await this.notifier.queueFollowUp(run);
398
+ run.followUp = interaction.yield;
399
+ run.followUpFile = followUpNotification?.filePath || null;
400
+ notificationFiles.push(followUpNotification?.filePath);
401
+ } else {
402
+ const completionNotification = await this.notifier.queueCompletion(run);
403
+ const handoffNotification = await this.notifier.queueHandoff(run);
404
+ notificationFiles.push(completionNotification?.filePath, handoffNotification?.filePath);
405
+ }
406
+ run.notificationFiles = notificationFiles.filter(Boolean);
407
+
408
+ const filePath = await this.runStore.saveRun(jobId, run);
409
+ return {
410
+ filePath,
411
+ ...run
412
+ };
413
+ } catch (error) {
414
+ if (error?.routingDecision) routingDecision = error.routingDecision;
415
+ if (error?.providerId) providerId = error.providerId;
416
+ if (error?.modelId) modelId = error.modelId;
417
+ if (error?.providerCapabilities) providerCapabilities = error.providerCapabilities;
418
+ if (error?.invocation) invocation = error.invocation;
419
+ if (error?.preflight) preflight = error.preflight;
420
+ if (error?.fallback) fallback = error.fallback;
421
+
422
+ if (plan) {
423
+ const errorNotification = await this.notifier.queueError({
424
+ plan,
425
+ error,
426
+ context: {
427
+ runId,
428
+ mode,
429
+ providerId,
430
+ providerCapabilities,
431
+ modelId,
432
+ routingDecision,
433
+ timestamp: new Date().toISOString()
434
+ }
435
+ });
436
+
437
+ const failedRun = {
438
+ id: runId,
439
+ timestamp: new Date().toISOString(),
440
+ mode,
441
+ providerId,
442
+ providerCapabilities,
443
+ modelId,
444
+ routingDecision,
445
+ providerModePolicy: mode === "provider" ? getProviderModePolicy() : null,
446
+ fallback,
447
+ preflight,
448
+ invocation,
449
+ plan,
450
+ error: {
451
+ message: error.message
452
+ },
453
+ notificationFiles: [errorNotification?.filePath].filter(Boolean)
454
+ };
455
+
456
+ const filePath = await this.runStore.saveRun(jobId, failedRun);
457
+ error.runFile = filePath;
458
+ }
459
+
460
+ throw error;
461
+ }
462
+ }
463
+
464
+ async shadowCompare(jobId) {
465
+ const [run, jobComparisons] = await Promise.all([
466
+ this.executeJob(jobId, { mode: "dry-run", shadowImport: true }),
467
+ this.scheduler.compareJobs()
468
+ ]);
469
+
470
+ const relatedLive = jobComparisons.find((job) => job.v2JobId === jobId)?.closestLiveJobs || [];
471
+
472
+ const comparison = {
473
+ timestamp: new Date().toISOString(),
474
+ jobId,
475
+ v2Run: {
476
+ mode: run.mode,
477
+ providerId: run.providerId,
478
+ modelId: run.modelId,
479
+ routingDecision: run.routingDecision,
480
+ summary: run.result.summary,
481
+ output: run.result.output,
482
+ providerHealth: run.providerHealth,
483
+ runFile: run.filePath
484
+ },
485
+ liveCandidates: relatedLive,
486
+ notes: [
487
+ relatedLive.length === 0
488
+ ? "No close live cron analogue found."
489
+ : `Found ${relatedLive.length} possible live cron analogues.`,
490
+ "Use provider mode only after dry-run shape looks correct."
491
+ ]
492
+ };
493
+
494
+ const filePath = await this.runStore.saveRun(`${jobId}-comparison`, comparison);
495
+ return {
496
+ filePath,
497
+ ...comparison
498
+ };
499
+ }
500
+ }