nemoris 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +49 -0
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/SECURITY.md +119 -0
- package/bin/nemoris +46 -0
- package/config/agents/agent.toml.example +28 -0
- package/config/agents/default.toml +22 -0
- package/config/agents/orchestrator.toml +18 -0
- package/config/delivery.toml +73 -0
- package/config/embeddings.toml +5 -0
- package/config/identity/default-purpose.md +1 -0
- package/config/identity/default-soul.md +3 -0
- package/config/identity/orchestrator-purpose.md +1 -0
- package/config/identity/orchestrator-soul.md +1 -0
- package/config/improvement-targets.toml +15 -0
- package/config/jobs/heartbeat-check.toml +30 -0
- package/config/jobs/memory-rollup.toml +46 -0
- package/config/jobs/workspace-health.toml +63 -0
- package/config/mcp.toml +16 -0
- package/config/output-contracts.toml +17 -0
- package/config/peers.toml +32 -0
- package/config/peers.toml.example +32 -0
- package/config/policies/memory-default.toml +10 -0
- package/config/policies/memory-heartbeat.toml +5 -0
- package/config/policies/memory-ops.toml +10 -0
- package/config/policies/tools-heartbeat-minimal.toml +8 -0
- package/config/policies/tools-interactive-safe.toml +8 -0
- package/config/policies/tools-ops-bounded.toml +8 -0
- package/config/policies/tools-orchestrator.toml +7 -0
- package/config/providers/anthropic.toml +15 -0
- package/config/providers/ollama.toml +5 -0
- package/config/providers/openai-codex.toml +9 -0
- package/config/providers/openrouter.toml +5 -0
- package/config/router.toml +22 -0
- package/config/runtime.toml +114 -0
- package/config/skills/self-improvement.toml +15 -0
- package/config/skills/telegram-onboarding-spec.md +240 -0
- package/config/skills/workspace-monitor.toml +15 -0
- package/config/task-router.toml +42 -0
- package/install.sh +50 -0
- package/package.json +90 -0
- package/src/auth/auth-profiles.js +169 -0
- package/src/auth/openai-codex-oauth.js +285 -0
- package/src/battle.js +449 -0
- package/src/cli/help.js +265 -0
- package/src/cli/output-filter.js +49 -0
- package/src/cli/runtime-control.js +704 -0
- package/src/cli-main.js +2763 -0
- package/src/cli.js +78 -0
- package/src/config/loader.js +332 -0
- package/src/config/schema-validator.js +214 -0
- package/src/config/toml-lite.js +8 -0
- package/src/daemon/action-handlers.js +71 -0
- package/src/daemon/healing-tick.js +87 -0
- package/src/daemon/health-probes.js +90 -0
- package/src/daemon/notifier.js +57 -0
- package/src/daemon/nurse.js +218 -0
- package/src/daemon/repair-log.js +106 -0
- package/src/daemon/rule-staging.js +90 -0
- package/src/daemon/rules.js +29 -0
- package/src/daemon/telegram-commands.js +54 -0
- package/src/daemon/updater.js +85 -0
- package/src/jobs/job-runner.js +78 -0
- package/src/mcp/consumer.js +129 -0
- package/src/memory/active-recall.js +171 -0
- package/src/memory/backend-manager.js +97 -0
- package/src/memory/backends/file-backend.js +38 -0
- package/src/memory/backends/qmd-backend.js +219 -0
- package/src/memory/embedding-guards.js +24 -0
- package/src/memory/embedding-index.js +118 -0
- package/src/memory/embedding-service.js +179 -0
- package/src/memory/file-index.js +177 -0
- package/src/memory/memory-signature.js +5 -0
- package/src/memory/memory-store.js +648 -0
- package/src/memory/retrieval-planner.js +66 -0
- package/src/memory/scoring.js +145 -0
- package/src/memory/simhash.js +78 -0
- package/src/memory/sqlite-active-store.js +824 -0
- package/src/memory/write-policy.js +36 -0
- package/src/onboarding/aliases.js +33 -0
- package/src/onboarding/auth/api-key.js +224 -0
- package/src/onboarding/auth/ollama-detect.js +42 -0
- package/src/onboarding/clack-prompter.js +77 -0
- package/src/onboarding/doctor.js +530 -0
- package/src/onboarding/lock.js +42 -0
- package/src/onboarding/model-catalog.js +344 -0
- package/src/onboarding/phases/auth.js +589 -0
- package/src/onboarding/phases/build.js +130 -0
- package/src/onboarding/phases/choose.js +82 -0
- package/src/onboarding/phases/detect.js +98 -0
- package/src/onboarding/phases/hatch.js +216 -0
- package/src/onboarding/phases/identity.js +79 -0
- package/src/onboarding/phases/ollama.js +345 -0
- package/src/onboarding/phases/scaffold.js +99 -0
- package/src/onboarding/phases/telegram.js +377 -0
- package/src/onboarding/phases/validate.js +204 -0
- package/src/onboarding/phases/verify.js +206 -0
- package/src/onboarding/platform.js +482 -0
- package/src/onboarding/status-bar.js +95 -0
- package/src/onboarding/templates.js +794 -0
- package/src/onboarding/toml-writer.js +38 -0
- package/src/onboarding/tui.js +250 -0
- package/src/onboarding/uninstall.js +153 -0
- package/src/onboarding/wizard.js +499 -0
- package/src/providers/anthropic.js +168 -0
- package/src/providers/base.js +247 -0
- package/src/providers/circuit-breaker.js +136 -0
- package/src/providers/ollama.js +163 -0
- package/src/providers/openai-codex.js +149 -0
- package/src/providers/openrouter.js +136 -0
- package/src/providers/registry.js +36 -0
- package/src/providers/router.js +16 -0
- package/src/runtime/bootstrap-cache.js +47 -0
- package/src/runtime/capabilities-prompt.js +25 -0
- package/src/runtime/completion-ping.js +99 -0
- package/src/runtime/config-validator.js +121 -0
- package/src/runtime/context-ledger.js +360 -0
- package/src/runtime/cutover-readiness.js +42 -0
- package/src/runtime/daemon.js +729 -0
- package/src/runtime/delivery-ack.js +195 -0
- package/src/runtime/delivery-adapters/local-file.js +41 -0
- package/src/runtime/delivery-adapters/openclaw-cli.js +94 -0
- package/src/runtime/delivery-adapters/openclaw-peer.js +98 -0
- package/src/runtime/delivery-adapters/shadow.js +13 -0
- package/src/runtime/delivery-adapters/standalone-http.js +98 -0
- package/src/runtime/delivery-adapters/telegram.js +104 -0
- package/src/runtime/delivery-adapters/tui.js +128 -0
- package/src/runtime/delivery-manager.js +807 -0
- package/src/runtime/delivery-store.js +168 -0
- package/src/runtime/dependency-health.js +118 -0
- package/src/runtime/envelope.js +114 -0
- package/src/runtime/evaluation.js +1089 -0
- package/src/runtime/exec-approvals.js +216 -0
- package/src/runtime/executor.js +500 -0
- package/src/runtime/failure-ping.js +67 -0
- package/src/runtime/flows.js +83 -0
- package/src/runtime/guards.js +45 -0
- package/src/runtime/handoff.js +51 -0
- package/src/runtime/identity-cache.js +28 -0
- package/src/runtime/improvement-engine.js +109 -0
- package/src/runtime/improvement-harness.js +581 -0
- package/src/runtime/input-sanitiser.js +72 -0
- package/src/runtime/interaction-contract.js +347 -0
- package/src/runtime/lane-readiness.js +226 -0
- package/src/runtime/migration.js +323 -0
- package/src/runtime/model-resolution.js +78 -0
- package/src/runtime/network.js +64 -0
- package/src/runtime/notification-store.js +97 -0
- package/src/runtime/notifier.js +256 -0
- package/src/runtime/orchestrator.js +53 -0
- package/src/runtime/orphan-reaper.js +41 -0
- package/src/runtime/output-contract-schema.js +139 -0
- package/src/runtime/output-contract-validator.js +439 -0
- package/src/runtime/peer-readiness.js +69 -0
- package/src/runtime/peer-registry.js +133 -0
- package/src/runtime/pilot-status.js +108 -0
- package/src/runtime/prompt-builder.js +261 -0
- package/src/runtime/provider-attempt.js +582 -0
- package/src/runtime/report-fallback.js +71 -0
- package/src/runtime/result-normalizer.js +183 -0
- package/src/runtime/retention.js +74 -0
- package/src/runtime/review.js +244 -0
- package/src/runtime/route-job.js +15 -0
- package/src/runtime/run-store.js +38 -0
- package/src/runtime/schedule.js +88 -0
- package/src/runtime/scheduler-state.js +434 -0
- package/src/runtime/scheduler.js +656 -0
- package/src/runtime/session-compactor.js +182 -0
- package/src/runtime/session-search.js +155 -0
- package/src/runtime/slack-inbound.js +249 -0
- package/src/runtime/ssrf.js +102 -0
- package/src/runtime/status-aggregator.js +330 -0
- package/src/runtime/task-contract.js +140 -0
- package/src/runtime/task-packet.js +107 -0
- package/src/runtime/task-router.js +140 -0
- package/src/runtime/telegram-inbound.js +1565 -0
- package/src/runtime/token-counter.js +134 -0
- package/src/runtime/token-estimator.js +59 -0
- package/src/runtime/tool-loop.js +200 -0
- package/src/runtime/transport-server.js +311 -0
- package/src/runtime/tui-server.js +411 -0
- package/src/runtime/ulid.js +44 -0
- package/src/security/ssrf-check.js +197 -0
- package/src/setup.js +369 -0
- package/src/shadow/bridge.js +303 -0
- package/src/skills/loader.js +84 -0
- package/src/tools/catalog.json +49 -0
- package/src/tools/cli-delegate.js +44 -0
- package/src/tools/mcp-client.js +106 -0
- package/src/tools/micro/cancel-task.js +6 -0
- package/src/tools/micro/complete-task.js +6 -0
- package/src/tools/micro/fail-task.js +6 -0
- package/src/tools/micro/http-fetch.js +74 -0
- package/src/tools/micro/index.js +36 -0
- package/src/tools/micro/lcm-recall.js +60 -0
- package/src/tools/micro/list-dir.js +17 -0
- package/src/tools/micro/list-skills.js +46 -0
- package/src/tools/micro/load-skill.js +38 -0
- package/src/tools/micro/memory-search.js +45 -0
- package/src/tools/micro/read-file.js +11 -0
- package/src/tools/micro/session-search.js +54 -0
- package/src/tools/micro/shell-exec.js +43 -0
- package/src/tools/micro/trigger-job.js +79 -0
- package/src/tools/micro/web-search.js +58 -0
- package/src/tools/micro/workspace-paths.js +39 -0
- package/src/tools/micro/write-file.js +14 -0
- package/src/tools/micro/write-memory.js +41 -0
- package/src/tools/registry.js +348 -0
- package/src/tools/tool-result-contract.js +36 -0
- package/src/tui/chat.js +835 -0
- package/src/tui/renderer.js +175 -0
- package/src/tui/socket-client.js +217 -0
- package/src/utils/canonical-json.js +29 -0
- package/src/utils/compaction.js +30 -0
- package/src/utils/env-loader.js +5 -0
- package/src/utils/errors.js +80 -0
- package/src/utils/fs.js +101 -0
- package/src/utils/ids.js +5 -0
- package/src/utils/model-context-limits.js +30 -0
- package/src/utils/token-budget.js +74 -0
- package/src/utils/usage-cost.js +25 -0
- package/src/utils/usage-metrics.js +14 -0
- package/vendor/smol-toml-1.5.2.tgz +0 -0
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sends a failure ping to the Telegram operator when a job fails,
|
|
3
|
+
* and a user-friendly error message to the originating chat.
|
|
4
|
+
* Never throws — all errors are caught and logged internally.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
function isRetryableError(err) {
|
|
8
|
+
const msg = (err?.message || "").toLowerCase();
|
|
9
|
+
const status = err?.status || err?.statusCode || 0;
|
|
10
|
+
return status >= 500 || err?.code === "ETIMEDOUT" ||
|
|
11
|
+
msg.includes("etimedout") || msg.includes("timeout") || msg.includes("fetch failed");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {object} job
|
|
16
|
+
* @param {Error} error
|
|
17
|
+
* @param {object} options
|
|
18
|
+
* @param {string} options.botToken
|
|
19
|
+
* @param {string} options.operatorChatId
|
|
20
|
+
* @param {Function} [options.fetchImpl]
|
|
21
|
+
*/
|
|
22
|
+
export async function sendFailurePing(job, error, options = {}) {
|
|
23
|
+
const { botToken, operatorChatId, fetchImpl } = options;
|
|
24
|
+
|
|
25
|
+
if (!botToken) return;
|
|
26
|
+
if (job.source === "completion_ping") return;
|
|
27
|
+
|
|
28
|
+
const fetchFn = fetchImpl || globalThis.fetch;
|
|
29
|
+
|
|
30
|
+
// Send user-friendly error to the originating chat
|
|
31
|
+
if (job.chat_id) {
|
|
32
|
+
const isTurnTimeout = error?.message === "turn_timeout";
|
|
33
|
+
let userMsg;
|
|
34
|
+
if (isTurnTimeout) {
|
|
35
|
+
userMsg = "⚠️ Turn timed out after 2 minutes. Try again or /stop to reset.";
|
|
36
|
+
} else if (isRetryableError(error)) {
|
|
37
|
+
userMsg = "⚠️ Provider hiccup — couldn't complete that. Try again in a moment.";
|
|
38
|
+
} else {
|
|
39
|
+
const snippet = (error?.message || "unknown error").slice(0, 120);
|
|
40
|
+
userMsg = `⚠️ Something went wrong: ${snippet}. Try again or /stop to reset.`;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
await fetchFn(`https://api.telegram.org/bot${botToken}/sendMessage`, {
|
|
44
|
+
method: "POST",
|
|
45
|
+
headers: { "content-type": "application/json" },
|
|
46
|
+
body: JSON.stringify({ chat_id: job.chat_id, text: userMsg }),
|
|
47
|
+
});
|
|
48
|
+
} catch (sendErr) {
|
|
49
|
+
console.error(JSON.stringify({ service: "error_delivery", event: "failed", error: sendErr.message, jobId: job.job_id }));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Operator notification (detailed)
|
|
54
|
+
if (operatorChatId) {
|
|
55
|
+
const errorSnippet = (error?.message || String(error)).slice(0, 120);
|
|
56
|
+
const text = `⚠️ Job failed\nAgent: ${job.agent_id}\nJob: ${job.job_id}\nError: ${errorSnippet}\nSource: ${job.source}`;
|
|
57
|
+
try {
|
|
58
|
+
await fetchFn(`https://api.telegram.org/bot${botToken}/sendMessage`, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: { "content-type": "application/json" },
|
|
61
|
+
body: JSON.stringify({ chat_id: operatorChatId, text }),
|
|
62
|
+
});
|
|
63
|
+
} catch (sendErr) {
|
|
64
|
+
console.error(JSON.stringify({ service: "failure_ping", error: sendErr.message, jobId: job.job_id }));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-Pillar Flows — coordinates seams between pillars.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export async function flowTaskEscalationToOperator(task, deliveryAck, telegramSend, { chatId, retryDelays } = {}) {
|
|
6
|
+
const chain = JSON.parse(task.escalation_chain);
|
|
7
|
+
const target = chain[task.escalation_index] || "operator";
|
|
8
|
+
if (target !== "operator") {
|
|
9
|
+
throw new Error(`Flow 1 only handles operator escalation, got target: ${target}`);
|
|
10
|
+
}
|
|
11
|
+
const body = [
|
|
12
|
+
`${task.assigned_agent} timed out on: ${task.objective}`,
|
|
13
|
+
task.error_slot ? `Error: ${task.error_slot}` : null,
|
|
14
|
+
`Result so far: ${task.result_slot || "none"}`,
|
|
15
|
+
"Want me to intervene?",
|
|
16
|
+
].filter(Boolean).join("\n");
|
|
17
|
+
return deliveryAck.sendWithGuarantee({
|
|
18
|
+
sourceAgent: task.owner_agent,
|
|
19
|
+
criticality: "decision_required",
|
|
20
|
+
payload: { text: body },
|
|
21
|
+
chatId,
|
|
22
|
+
correlationId: task.envelope_id,
|
|
23
|
+
sendFn: (payload) => telegramSend({ text: payload.text, chatId }),
|
|
24
|
+
retryDelays,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function flowTaskCompletionToMemory(task, writeBuffer) {
|
|
29
|
+
const summary = task.result_slot
|
|
30
|
+
? (typeof task.result_slot === "string" ? task.result_slot : JSON.stringify(task.result_slot))
|
|
31
|
+
: "no result";
|
|
32
|
+
const content = `Task "${task.objective}" completed by ${task.assigned_agent}: ${summary.slice(0, 500)}`;
|
|
33
|
+
writeBuffer.add({
|
|
34
|
+
content,
|
|
35
|
+
confidence: 0.85,
|
|
36
|
+
sourceAgent: task.assigned_agent,
|
|
37
|
+
category: "task_result",
|
|
38
|
+
type: "fact",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function flowDecisionResponseToTask(envelope, operatorMessage, taskContract, { resolveAgent, callerAgent } = {}) {
|
|
43
|
+
const task = taskContract.db.prepare(
|
|
44
|
+
"SELECT * FROM tasks WHERE envelope_id = ?"
|
|
45
|
+
).get(envelope.correlation_id);
|
|
46
|
+
if (!task) return { action: "unstructured", reason: "no linked task" };
|
|
47
|
+
|
|
48
|
+
const msg = operatorMessage.toLowerCase();
|
|
49
|
+
|
|
50
|
+
if (msg.includes("retry")) {
|
|
51
|
+
const newTask = taskContract.createTask({
|
|
52
|
+
ownerAgent: task.owner_agent,
|
|
53
|
+
assignedAgent: task.assigned_agent,
|
|
54
|
+
objective: task.objective,
|
|
55
|
+
deadlineMinutes: Math.max(10, Math.round((new Date(task.deadline_at) - new Date(task.created_at)) / 60000)),
|
|
56
|
+
});
|
|
57
|
+
return { action: "retry", newTaskId: newTask.task_id, originalTaskId: task.task_id };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (msg.includes("cancel")) {
|
|
61
|
+
try {
|
|
62
|
+
taskContract.transition(task.task_id, "cancelled", task.owner_agent);
|
|
63
|
+
} catch { /* may not be in cancellable state */ }
|
|
64
|
+
return { action: "cancel", taskId: task.task_id };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (msg.includes("reassign")) {
|
|
68
|
+
const match = operatorMessage.match(/reassign(?:\s+to)?\s+(\w+)/i);
|
|
69
|
+
const targetName = match?.[1];
|
|
70
|
+
if (targetName && resolveAgent && resolveAgent(targetName)) {
|
|
71
|
+
const newTask = taskContract.createTask({
|
|
72
|
+
ownerAgent: task.owner_agent,
|
|
73
|
+
assignedAgent: targetName,
|
|
74
|
+
objective: task.objective,
|
|
75
|
+
deadlineMinutes: Math.max(10, Math.round((new Date(task.deadline_at) - new Date(task.created_at)) / 60000)),
|
|
76
|
+
});
|
|
77
|
+
return { action: "reassign", newTaskId: newTask.task_id, assignedAgent: targetName };
|
|
78
|
+
}
|
|
79
|
+
return { action: "unstructured", reason: `Could not resolve agent: ${targetName}` };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { action: "unstructured", message: operatorMessage };
|
|
83
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const DEFAULT_PROVIDER_MODE = {
|
|
2
|
+
allowProviderExecution: false,
|
|
3
|
+
allowRemoteProviders: false,
|
|
4
|
+
allowedJobIds: ["heartbeat-check", "workspace-health", "memory-rollup"],
|
|
5
|
+
allowedLanes: ["local_cheap", "local_report", "report_fallback_lowcost"],
|
|
6
|
+
requireHealthyProvider: true
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function readFlag(name) {
|
|
10
|
+
const raw = process.env[name];
|
|
11
|
+
return raw === "1" || raw === "true";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getProviderModePolicy() {
|
|
15
|
+
return {
|
|
16
|
+
...DEFAULT_PROVIDER_MODE,
|
|
17
|
+
allowProviderExecution: readFlag("NEMORIS_ALLOW_PROVIDER_MODE"),
|
|
18
|
+
allowRemoteProviders: readFlag("NEMORIS_ALLOW_REMOTE_PROVIDER_MODE")
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function assertProviderExecutionAllowed({ jobId, modelLane, providerId, interactive = false }) {
|
|
23
|
+
const policy = getProviderModePolicy();
|
|
24
|
+
|
|
25
|
+
if (!policy.allowProviderExecution) {
|
|
26
|
+
throw new Error("Provider mode disabled. Set NEMORIS_ALLOW_PROVIDER_MODE=1 to enable guarded shadow execution.");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Interactive jobs (Telegram, agent dispatch) bypass job-id and lane allowlists
|
|
30
|
+
if (!interactive) {
|
|
31
|
+
if (!policy.allowedJobIds.includes(jobId)) {
|
|
32
|
+
throw new Error(`Provider mode not allowed for job ${jobId}.`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!policy.allowedLanes.includes(modelLane)) {
|
|
36
|
+
throw new Error(`Provider mode not allowed for lane ${modelLane}.`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (providerId !== "ollama" && !policy.allowRemoteProviders) {
|
|
41
|
+
throw new Error(`Remote provider execution blocked for ${providerId}. Set NEMORIS_ALLOW_REMOTE_PROVIDER_MODE=1 to override.`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return policy;
|
|
45
|
+
}
|