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.
- package/.env.example +49 -49
- package/LICENSE +21 -21
- package/README.md +209 -209
- package/SECURITY.md +59 -119
- package/bin/nemoris +46 -46
- package/config/agents/agent.toml.example +28 -28
- package/config/agents/content.toml +23 -0
- package/config/agents/default.toml +22 -22
- package/config/agents/heartbeat.toml +35 -0
- package/config/agents/iris.toml +23 -0
- package/config/agents/lab.toml +23 -0
- package/config/agents/main.toml +45 -0
- package/config/agents/nemo.toml +21 -0
- package/config/agents/ops.toml +38 -0
- package/config/agents/orchestrator.toml +18 -18
- package/config/agents/revenue.toml +23 -0
- package/config/agents/testyboo.toml +19 -0
- package/config/delivery.toml +73 -73
- package/config/embeddings.toml +5 -5
- package/config/identity/content-purpose.md +11 -0
- package/config/identity/content-soul.md +45 -0
- package/config/identity/default-purpose.md +1 -1
- package/config/identity/default-soul.md +3 -3
- package/config/identity/heartbeat-purpose.md +9 -0
- package/config/identity/heartbeat-soul.md +16 -0
- package/config/identity/iris-purpose.md +17 -0
- package/config/identity/iris-soul.md +68 -0
- package/config/identity/lab-purpose.md +10 -0
- package/config/identity/lab-soul.md +38 -0
- package/config/identity/main-purpose.md +17 -0
- package/config/identity/main-soul.md +66 -0
- package/config/identity/main-user.md +22 -0
- package/config/identity/ops-purpose.md +9 -0
- package/config/identity/ops-soul.md +16 -0
- package/config/identity/orchestrator-purpose.md +1 -1
- package/config/identity/orchestrator-soul.md +1 -1
- package/config/identity/revenue-purpose.md +9 -0
- package/config/identity/revenue-soul.md +41 -0
- package/config/identity/testyboo-purpose.md +13 -0
- package/config/identity/testyboo-soul.md +20 -0
- package/config/improvement-targets.toml +15 -15
- package/config/jobs/heartbeat-check.toml +30 -30
- package/config/jobs/memory-rollup.toml +46 -46
- package/config/jobs/workspace-health.toml +63 -63
- package/config/mcp.toml +16 -16
- package/config/output-contracts.toml +17 -17
- package/config/peers.toml +32 -32
- package/config/peers.toml.example +32 -32
- package/config/policies/memory-default.toml +10 -10
- package/config/policies/memory-heartbeat.toml +5 -5
- package/config/policies/memory-ops.toml +10 -10
- package/config/policies/tools-heartbeat-minimal.toml +8 -8
- package/config/policies/tools-interactive-safe.toml +8 -8
- package/config/policies/tools-ops-bounded.toml +8 -8
- package/config/policies/tools-orchestrator.toml +7 -7
- package/config/providers/anthropic.toml +15 -15
- package/config/providers/ollama.toml +5 -5
- package/config/providers/openai-codex.toml +9 -9
- package/config/providers/openrouter.toml +5 -5
- package/config/router.toml +22 -22
- package/config/runtime.toml +114 -114
- package/config/skills/self-improvement.toml +15 -15
- package/config/skills/telegram-onboarding-spec.md +240 -240
- package/config/skills/workspace-monitor.toml +15 -15
- package/config/task-router.toml +42 -42
- package/install.sh +50 -50
- package/package.json +91 -90
- package/src/auth/auth-profiles.js +169 -169
- package/src/auth/openai-codex-oauth.js +285 -285
- package/src/battle.js +449 -449
- package/src/cli/help.js +265 -265
- package/src/cli/output-filter.js +49 -49
- package/src/cli/runtime-control.js +704 -704
- package/src/cli-main.js +2763 -2763
- package/src/cli.js +78 -78
- package/src/config/loader.js +332 -332
- package/src/config/schema-validator.js +214 -214
- package/src/config/toml-lite.js +8 -8
- package/src/daemon/action-handlers.js +71 -71
- package/src/daemon/healing-tick.js +87 -87
- package/src/daemon/health-probes.js +90 -90
- package/src/daemon/notifier.js +57 -57
- package/src/daemon/nurse.js +218 -218
- package/src/daemon/repair-log.js +106 -106
- package/src/daemon/rule-staging.js +90 -90
- package/src/daemon/rules.js +29 -29
- package/src/daemon/telegram-commands.js +54 -54
- package/src/daemon/updater.js +85 -85
- package/src/jobs/job-runner.js +78 -78
- package/src/mcp/consumer.js +129 -129
- package/src/memory/active-recall.js +171 -171
- package/src/memory/backend-manager.js +97 -97
- package/src/memory/backends/file-backend.js +38 -38
- package/src/memory/backends/qmd-backend.js +219 -219
- package/src/memory/embedding-guards.js +24 -24
- package/src/memory/embedding-index.js +118 -118
- package/src/memory/embedding-service.js +179 -179
- package/src/memory/file-index.js +177 -177
- package/src/memory/memory-signature.js +5 -5
- package/src/memory/memory-store.js +648 -648
- package/src/memory/retrieval-planner.js +66 -66
- package/src/memory/scoring.js +145 -145
- package/src/memory/simhash.js +78 -78
- package/src/memory/sqlite-active-store.js +824 -824
- package/src/memory/write-policy.js +36 -36
- package/src/onboarding/aliases.js +33 -33
- package/src/onboarding/auth/api-key.js +224 -224
- package/src/onboarding/auth/ollama-detect.js +42 -42
- package/src/onboarding/clack-prompter.js +77 -77
- package/src/onboarding/doctor.js +530 -530
- package/src/onboarding/lock.js +42 -42
- package/src/onboarding/model-catalog.js +344 -344
- package/src/onboarding/phases/auth.js +576 -589
- package/src/onboarding/phases/build.js +130 -130
- package/src/onboarding/phases/choose.js +82 -82
- package/src/onboarding/phases/detect.js +98 -98
- package/src/onboarding/phases/hatch.js +216 -216
- package/src/onboarding/phases/identity.js +79 -79
- package/src/onboarding/phases/ollama.js +345 -345
- package/src/onboarding/phases/scaffold.js +99 -99
- package/src/onboarding/phases/telegram.js +377 -377
- package/src/onboarding/phases/validate.js +204 -204
- package/src/onboarding/phases/verify.js +206 -206
- package/src/onboarding/platform.js +482 -482
- package/src/onboarding/status-bar.js +95 -95
- package/src/onboarding/templates.js +794 -794
- package/src/onboarding/toml-writer.js +38 -38
- package/src/onboarding/tui.js +250 -250
- package/src/onboarding/uninstall.js +153 -153
- package/src/onboarding/wizard.js +516 -499
- package/src/providers/anthropic.js +168 -168
- package/src/providers/base.js +247 -247
- package/src/providers/circuit-breaker.js +136 -136
- package/src/providers/ollama.js +163 -163
- package/src/providers/openai-codex.js +149 -149
- package/src/providers/openrouter.js +136 -136
- package/src/providers/registry.js +36 -36
- package/src/providers/router.js +16 -16
- package/src/runtime/bootstrap-cache.js +47 -47
- package/src/runtime/capabilities-prompt.js +25 -25
- package/src/runtime/completion-ping.js +99 -99
- package/src/runtime/config-validator.js +121 -121
- package/src/runtime/context-ledger.js +360 -360
- package/src/runtime/cutover-readiness.js +42 -42
- package/src/runtime/daemon.js +729 -729
- package/src/runtime/delivery-ack.js +195 -195
- package/src/runtime/delivery-adapters/local-file.js +41 -41
- package/src/runtime/delivery-adapters/openclaw-cli.js +94 -94
- package/src/runtime/delivery-adapters/openclaw-peer.js +98 -98
- package/src/runtime/delivery-adapters/shadow.js +13 -13
- package/src/runtime/delivery-adapters/standalone-http.js +98 -98
- package/src/runtime/delivery-adapters/telegram.js +104 -104
- package/src/runtime/delivery-adapters/tui.js +128 -128
- package/src/runtime/delivery-manager.js +807 -807
- package/src/runtime/delivery-store.js +168 -168
- package/src/runtime/dependency-health.js +118 -118
- package/src/runtime/envelope.js +114 -114
- package/src/runtime/evaluation.js +1089 -1089
- package/src/runtime/exec-approvals.js +216 -216
- package/src/runtime/executor.js +500 -500
- package/src/runtime/failure-ping.js +67 -67
- package/src/runtime/flows.js +83 -83
- package/src/runtime/guards.js +45 -45
- package/src/runtime/handoff.js +51 -51
- package/src/runtime/identity-cache.js +28 -28
- package/src/runtime/improvement-engine.js +109 -109
- package/src/runtime/improvement-harness.js +581 -581
- package/src/runtime/input-sanitiser.js +72 -72
- package/src/runtime/interaction-contract.js +347 -347
- package/src/runtime/lane-readiness.js +226 -226
- package/src/runtime/migration.js +323 -323
- package/src/runtime/model-resolution.js +78 -78
- package/src/runtime/network.js +64 -64
- package/src/runtime/notification-store.js +97 -97
- package/src/runtime/notifier.js +256 -256
- package/src/runtime/orchestrator.js +53 -53
- package/src/runtime/orphan-reaper.js +41 -41
- package/src/runtime/output-contract-schema.js +139 -139
- package/src/runtime/output-contract-validator.js +439 -439
- package/src/runtime/peer-readiness.js +69 -69
- package/src/runtime/peer-registry.js +133 -133
- package/src/runtime/pilot-status.js +108 -108
- package/src/runtime/prompt-builder.js +261 -261
- package/src/runtime/provider-attempt.js +582 -582
- package/src/runtime/report-fallback.js +71 -71
- package/src/runtime/result-normalizer.js +183 -183
- package/src/runtime/retention.js +74 -74
- package/src/runtime/review.js +244 -244
- package/src/runtime/route-job.js +15 -15
- package/src/runtime/run-store.js +38 -38
- package/src/runtime/schedule.js +88 -88
- package/src/runtime/scheduler-state.js +434 -434
- package/src/runtime/scheduler.js +656 -656
- package/src/runtime/session-compactor.js +182 -182
- package/src/runtime/session-search.js +155 -155
- package/src/runtime/slack-inbound.js +249 -249
- package/src/runtime/ssrf.js +102 -102
- package/src/runtime/status-aggregator.js +330 -330
- package/src/runtime/task-contract.js +140 -140
- package/src/runtime/task-packet.js +107 -107
- package/src/runtime/task-router.js +140 -140
- package/src/runtime/telegram-inbound.js +1565 -1565
- package/src/runtime/token-counter.js +134 -134
- package/src/runtime/token-estimator.js +59 -59
- package/src/runtime/tool-loop.js +200 -200
- package/src/runtime/transport-server.js +311 -311
- package/src/runtime/tui-server.js +411 -411
- package/src/runtime/ulid.js +44 -44
- package/src/security/ssrf-check.js +197 -197
- package/src/setup.js +369 -369
- package/src/shadow/bridge.js +303 -303
- package/src/skills/loader.js +84 -84
- package/src/tools/catalog.json +49 -49
- package/src/tools/cli-delegate.js +44 -44
- package/src/tools/mcp-client.js +106 -106
- package/src/tools/micro/cancel-task.js +6 -6
- package/src/tools/micro/complete-task.js +6 -6
- package/src/tools/micro/fail-task.js +6 -6
- package/src/tools/micro/http-fetch.js +74 -74
- package/src/tools/micro/index.js +36 -36
- package/src/tools/micro/lcm-recall.js +60 -60
- package/src/tools/micro/list-dir.js +17 -17
- package/src/tools/micro/list-skills.js +46 -46
- package/src/tools/micro/load-skill.js +38 -38
- package/src/tools/micro/memory-search.js +45 -45
- package/src/tools/micro/read-file.js +11 -11
- package/src/tools/micro/session-search.js +54 -54
- package/src/tools/micro/shell-exec.js +43 -43
- package/src/tools/micro/trigger-job.js +79 -79
- package/src/tools/micro/web-search.js +58 -58
- package/src/tools/micro/workspace-paths.js +39 -39
- package/src/tools/micro/write-file.js +14 -14
- package/src/tools/micro/write-memory.js +41 -41
- package/src/tools/registry.js +348 -348
- package/src/tools/tool-result-contract.js +36 -36
- package/src/tui/chat.js +835 -835
- package/src/tui/renderer.js +175 -175
- package/src/tui/socket-client.js +217 -217
- package/src/utils/canonical-json.js +29 -29
- package/src/utils/compaction.js +30 -30
- package/src/utils/env-loader.js +5 -5
- package/src/utils/errors.js +80 -80
- package/src/utils/fs.js +101 -101
- package/src/utils/ids.js +5 -5
- package/src/utils/model-context-limits.js +30 -30
- package/src/utils/token-budget.js +74 -74
- package/src/utils/usage-cost.js +25 -25
- package/src/utils/usage-metrics.js +14 -14
package/src/runtime/executor.js
CHANGED
|
@@ -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
|
+
}
|