aiden-runtime 4.1.5 → 4.6.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/README.md +265 -847
- package/dist/api/server.js +32 -5
- package/dist/cli/v4/aidenCLI.js +536 -152
- package/dist/cli/v4/callbacks.js +170 -0
- package/dist/cli/v4/chatSession.js +245 -3
- package/dist/cli/v4/commands/_runtimeToggleHelpers.js +94 -0
- package/dist/cli/v4/commands/browserDepth.js +45 -0
- package/dist/cli/v4/commands/cron.js +264 -0
- package/dist/cli/v4/commands/daemon.js +541 -0
- package/dist/cli/v4/commands/daemonStatus.js +253 -0
- package/dist/cli/v4/commands/fanout.js +42 -59
- package/dist/cli/v4/commands/help.js +13 -0
- package/dist/cli/v4/commands/index.js +35 -1
- package/dist/cli/v4/commands/mcp.js +80 -54
- package/dist/cli/v4/commands/plannerGuard.js +53 -0
- package/dist/cli/v4/commands/recovery.js +122 -0
- package/dist/cli/v4/commands/runs.js +223 -0
- package/dist/cli/v4/commands/sandbox.js +48 -0
- package/dist/cli/v4/commands/spawnPause.js +93 -0
- package/dist/cli/v4/commands/suggestions.js +68 -0
- package/dist/cli/v4/commands/tce.js +41 -0
- package/dist/cli/v4/commands/trigger.js +378 -0
- package/dist/cli/v4/commands/update.js +95 -3
- package/dist/cli/v4/daemonAgentBuilder.js +145 -0
- package/dist/cli/v4/defaultSoul.js +1 -1
- package/dist/cli/v4/display/capabilityCard.js +26 -0
- package/dist/cli/v4/display.js +18 -8
- package/dist/cli/v4/replyRenderer.js +31 -23
- package/dist/cli/v4/updateBootPrompt.js +170 -0
- package/dist/core/playwrightBridge.js +129 -0
- package/dist/core/v4/aidenAgent.js +527 -5
- package/dist/core/v4/browserState.js +436 -0
- package/dist/core/v4/checkpoint.js +79 -0
- package/dist/core/v4/daemon/bootstrap.js +651 -0
- package/dist/core/v4/daemon/cleanShutdown.js +154 -0
- package/dist/core/v4/daemon/cron/cronBridge.js +126 -0
- package/dist/core/v4/daemon/cron/cronEmitter.js +173 -0
- package/dist/core/v4/daemon/cron/migration.js +199 -0
- package/dist/core/v4/daemon/cron/misfirePolicy.js +115 -0
- package/dist/core/v4/daemon/daemonConfig.js +90 -0
- package/dist/core/v4/daemon/db/connection.js +106 -0
- package/dist/core/v4/daemon/db/migrations.js +362 -0
- package/dist/core/v4/daemon/db/schema/v1.spec.js +18 -0
- package/dist/core/v4/daemon/dispatcher/agentRunner.js +98 -0
- package/dist/core/v4/daemon/dispatcher/budgetGate.js +127 -0
- package/dist/core/v4/daemon/dispatcher/daemonApproval.js +113 -0
- package/dist/core/v4/daemon/dispatcher/dailyBudgetTracker.js +120 -0
- package/dist/core/v4/daemon/dispatcher/dispatcher.js +389 -0
- package/dist/core/v4/daemon/dispatcher/fireRateLimiter.js +113 -0
- package/dist/core/v4/daemon/dispatcher/index.js +53 -0
- package/dist/core/v4/daemon/dispatcher/promptTemplate.js +95 -0
- package/dist/core/v4/daemon/dispatcher/realAgentRunner.js +356 -0
- package/dist/core/v4/daemon/dispatcher/resolveModel.js +93 -0
- package/dist/core/v4/daemon/dispatcher/sessionId.js +93 -0
- package/dist/core/v4/daemon/drain.js +156 -0
- package/dist/core/v4/daemon/eventLoopLag.js +73 -0
- package/dist/core/v4/daemon/health.js +159 -0
- package/dist/core/v4/daemon/idempotencyStore.js +204 -0
- package/dist/core/v4/daemon/index.js +179 -0
- package/dist/core/v4/daemon/instanceTracker.js +99 -0
- package/dist/core/v4/daemon/resourceRegistry.js +150 -0
- package/dist/core/v4/daemon/restartCode.js +32 -0
- package/dist/core/v4/daemon/restartFailureCounter.js +77 -0
- package/dist/core/v4/daemon/runStore.js +144 -0
- package/dist/core/v4/daemon/runtimeLock.js +167 -0
- package/dist/core/v4/daemon/signals.js +50 -0
- package/dist/core/v4/daemon/supervisor.js +272 -0
- package/dist/core/v4/daemon/triggerBus.js +279 -0
- package/dist/core/v4/daemon/triggers/email/allowlist.js +70 -0
- package/dist/core/v4/daemon/triggers/email/automatedSender.js +78 -0
- package/dist/core/v4/daemon/triggers/email/bodyExtractor.js +0 -0
- package/dist/core/v4/daemon/triggers/email/emailSeenStore.js +99 -0
- package/dist/core/v4/daemon/triggers/email/emailSpec.js +107 -0
- package/dist/core/v4/daemon/triggers/email/imapConnection.js +211 -0
- package/dist/core/v4/daemon/triggers/email/index.js +332 -0
- package/dist/core/v4/daemon/triggers/email/seenUids.js +60 -0
- package/dist/core/v4/daemon/triggers/fileObservationsStore.js +93 -0
- package/dist/core/v4/daemon/triggers/fileWatcher.js +253 -0
- package/dist/core/v4/daemon/triggers/fileWatcherSpec.js +88 -0
- package/dist/core/v4/daemon/triggers/fsIdentity.js +42 -0
- package/dist/core/v4/daemon/triggers/globMatcher.js +100 -0
- package/dist/core/v4/daemon/triggers/reconcile.js +206 -0
- package/dist/core/v4/daemon/triggers/settleStat.js +81 -0
- package/dist/core/v4/daemon/triggers/webhook.js +376 -0
- package/dist/core/v4/daemon/triggers/webhookDeliveriesStore.js +109 -0
- package/dist/core/v4/daemon/triggers/webhookIdempotency.js +72 -0
- package/dist/core/v4/daemon/triggers/webhookRateLimit.js +56 -0
- package/dist/core/v4/daemon/triggers/webhookSpec.js +76 -0
- package/dist/core/v4/daemon/triggers/webhookVerifier.js +128 -0
- package/dist/core/v4/daemon/types.js +15 -0
- package/dist/core/v4/dockerSession.js +461 -0
- package/dist/core/v4/dryRun.js +117 -0
- package/dist/core/v4/failureClassifier.js +779 -0
- package/dist/core/v4/providerFallback.js +35 -2
- package/dist/core/v4/recoveryReport.js +449 -0
- package/dist/core/v4/runtimeToggles.js +214 -0
- package/dist/core/v4/sandboxConfig.js +285 -0
- package/dist/core/v4/sandboxFs.js +316 -0
- package/dist/core/v4/selfimprovement/recoveryStore.js +307 -0
- package/dist/core/v4/selfimprovement/signatureBuilder.js +158 -0
- package/dist/core/v4/subagent/childBuilder.js +391 -0
- package/dist/core/v4/subagent/fanout.js +75 -51
- package/dist/core/v4/subagent/spawnPause.js +191 -0
- package/dist/core/v4/subagent/spawnSubAgent.js +310 -0
- package/dist/core/v4/suggestionCatalog.js +41 -0
- package/dist/core/v4/suggestionEngine.js +210 -0
- package/dist/core/v4/toolRegistry.js +37 -3
- package/dist/core/v4/turnState.js +587 -0
- package/dist/core/v4/update/checkUpdate.js +63 -3
- package/dist/core/v4/update/installMethodDetect.js +115 -0
- package/dist/core/v4/update/registryClient.js +121 -0
- package/dist/core/v4/update/skipState.js +75 -0
- package/dist/core/v4/verifier.js +448 -0
- package/dist/core/version.js +1 -1
- package/dist/moat/plannerGuard.js +29 -0
- package/dist/providers/v4/anthropicAdapter.js +31 -3
- package/dist/providers/v4/chatCompletionsAdapter.js +26 -3
- package/dist/providers/v4/codexResponsesAdapter.js +25 -2
- package/dist/providers/v4/ollamaPromptToolsAdapter.js +57 -2
- package/dist/tools/v4/browser/_observer.js +224 -0
- package/dist/tools/v4/browser/browserBlocker.js +396 -0
- package/dist/tools/v4/browser/browserClick.js +18 -1
- package/dist/tools/v4/browser/browserClose.js +18 -1
- package/dist/tools/v4/browser/browserExtract.js +5 -1
- package/dist/tools/v4/browser/browserFill.js +17 -1
- package/dist/tools/v4/browser/browserGetUrl.js +5 -1
- package/dist/tools/v4/browser/browserNavigate.js +16 -1
- package/dist/tools/v4/browser/browserScreenshot.js +5 -1
- package/dist/tools/v4/browser/browserScroll.js +18 -1
- package/dist/tools/v4/browser/browserType.js +17 -1
- package/dist/tools/v4/browser/captchaCheck.js +5 -1
- package/dist/tools/v4/executeCode.js +1 -0
- package/dist/tools/v4/files/fileCopy.js +56 -2
- package/dist/tools/v4/files/fileDelete.js +38 -1
- package/dist/tools/v4/files/fileList.js +12 -1
- package/dist/tools/v4/files/fileMove.js +59 -2
- package/dist/tools/v4/files/filePatch.js +43 -1
- package/dist/tools/v4/files/fileRead.js +12 -1
- package/dist/tools/v4/files/fileWrite.js +41 -1
- package/dist/tools/v4/index.js +88 -61
- package/dist/tools/v4/memory/memoryAdd.js +14 -0
- package/dist/tools/v4/memory/memoryRemove.js +14 -0
- package/dist/tools/v4/memory/memoryReplace.js +15 -0
- package/dist/tools/v4/memory/sessionSummary.js +12 -0
- package/dist/tools/v4/process/processKill.js +19 -0
- package/dist/tools/v4/process/processList.js +1 -0
- package/dist/tools/v4/process/processLogRead.js +1 -0
- package/dist/tools/v4/process/processSpawn.js +13 -0
- package/dist/tools/v4/process/processWait.js +1 -0
- package/dist/tools/v4/sessions/recallSession.js +1 -0
- package/dist/tools/v4/sessions/sessionList.js +1 -0
- package/dist/tools/v4/sessions/sessionSearch.js +1 -0
- package/dist/tools/v4/skills/lookupToolSchema.js +7 -0
- package/dist/tools/v4/skills/skillManage.js +13 -0
- package/dist/tools/v4/skills/skillView.js +1 -0
- package/dist/tools/v4/skills/skillsList.js +1 -0
- package/dist/tools/v4/subagent/spawnSubAgentTool.js +334 -0
- package/dist/tools/v4/subagent/subagentFanout.js +54 -1
- package/dist/tools/v4/system/aidenSelfUpdate.js +16 -0
- package/dist/tools/v4/system/appClose.js +13 -0
- package/dist/tools/v4/system/appInput.js +13 -0
- package/dist/tools/v4/system/appLaunch.js +13 -0
- package/dist/tools/v4/system/clipboardRead.js +1 -0
- package/dist/tools/v4/system/clipboardWrite.js +14 -0
- package/dist/tools/v4/system/mediaKey.js +12 -0
- package/dist/tools/v4/system/mediaSessions.js +1 -0
- package/dist/tools/v4/system/mediaTransport.js +13 -0
- package/dist/tools/v4/system/naturalEvents.js +1 -0
- package/dist/tools/v4/system/nowPlaying.js +1 -0
- package/dist/tools/v4/system/osProcessList.js +1 -0
- package/dist/tools/v4/system/screenshot.js +1 -0
- package/dist/tools/v4/system/systemInfo.js +1 -0
- package/dist/tools/v4/system/volumeSet.js +17 -0
- package/dist/tools/v4/terminal/shellExec.js +81 -9
- package/dist/tools/v4/web/deepResearch.js +1 -0
- package/dist/tools/v4/web/openUrl.js +1 -0
- package/dist/tools/v4/web/webFetch.js +1 -0
- package/dist/tools/v4/web/webPage.js +1 -0
- package/dist/tools/v4/web/webSearch.js +1 -0
- package/dist/tools/v4/web/youtubeSearch.js +1 -0
- package/package.json +13 -3
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/daemon/dispatcher/dispatcher.ts — v4.5 Phase 5a.
|
|
10
|
+
*
|
|
11
|
+
* The bus consumer. Bridges the durable trigger queue
|
|
12
|
+
* (`triggerBus`) to the agent loop (`DaemonAgentRunner`).
|
|
13
|
+
*
|
|
14
|
+
* Loop body (workerCount-bounded, default 1 per Q-P5-1):
|
|
15
|
+
*
|
|
16
|
+
* 1. `triggerBus.claim({ownerId, leaseMs})` — atomic claim.
|
|
17
|
+
* 2. Read trigger spec row (`triggers` table) for fire_rate_limit /
|
|
18
|
+
* prompt_template / deliver_only. Missing row → defaults
|
|
19
|
+
* (no template, not deliver-only, no fire-rate cap).
|
|
20
|
+
* 3. Build sessionId via `buildTriggerSessionId`.
|
|
21
|
+
* 4. Render prompt template (if any) with payload vars. Missing
|
|
22
|
+
* vars + non-empty template → markFailed with
|
|
23
|
+
* `trigger_misconfigured` classification hint.
|
|
24
|
+
* 5. Start lease-renew timer (default 60s cadence, extends by
|
|
25
|
+
* `leaseMs`). Runs for the lifetime of the invocation.
|
|
26
|
+
* 6. Invoke runner. Catch any throw.
|
|
27
|
+
* 7. On clean return → `markDone(eventId, claimToken, runId)`.
|
|
28
|
+
* On throw or error finish-reason → `markFailed` which auto-
|
|
29
|
+
* transitions to `dead_letter` after `maxAttempts`.
|
|
30
|
+
* 8. Renew timer cleared.
|
|
31
|
+
*
|
|
32
|
+
* Concurrency: a semaphore (in-flight count vs workerCount)
|
|
33
|
+
* guards the claim loop. Each claim spawns an async worker that
|
|
34
|
+
* decrements the semaphore on exit. The poll interval is short
|
|
35
|
+
* (250ms) when no event was claimed; it ratchets up via the
|
|
36
|
+
* adaptive backoff inside `_pollOnce` when the bus is empty.
|
|
37
|
+
*
|
|
38
|
+
* Shutdown: `stop(timeoutMs)` flips an `_stopping` flag, then
|
|
39
|
+
* awaits the in-flight worker promises (race against
|
|
40
|
+
* `timeoutMs`). Workers that exceed the deadline are NOT killed
|
|
41
|
+
* (the runner contract is cooperative-stop; runs eventually
|
|
42
|
+
* land in `markFailed` via the claim-lease-expired path on the
|
|
43
|
+
* next instance).
|
|
44
|
+
*/
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.createDispatcher = createDispatcher;
|
|
47
|
+
exports._dispatcherOwnerId = _dispatcherOwnerId;
|
|
48
|
+
const node_crypto_1 = require("node:crypto");
|
|
49
|
+
const sessionId_1 = require("./sessionId");
|
|
50
|
+
const promptTemplate_1 = require("./promptTemplate");
|
|
51
|
+
const agentRunner_1 = require("./agentRunner");
|
|
52
|
+
const realAgentRunner_1 = require("./realAgentRunner");
|
|
53
|
+
// ── Defaults ───────────────────────────────────────────────────────────────
|
|
54
|
+
const DEFAULT_WORKER_COUNT = 1;
|
|
55
|
+
const DEFAULT_POLL_IDLE_MS = 250;
|
|
56
|
+
const DEFAULT_POLL_BUSY_MS = 0; // immediate re-poll while events drain
|
|
57
|
+
const DEFAULT_LEASE_MS = 5 * 60000;
|
|
58
|
+
const DEFAULT_RENEW_MS = 60000;
|
|
59
|
+
const DEFAULT_MAX_ATTEMPTS = 3;
|
|
60
|
+
const DEFAULT_STOP_TIMEOUT_MS = 30000;
|
|
61
|
+
// ── Implementation ─────────────────────────────────────────────────────────
|
|
62
|
+
function createDispatcher(opts) {
|
|
63
|
+
const workerCount = opts.workerCount ?? DEFAULT_WORKER_COUNT;
|
|
64
|
+
const leaseMs = opts.leaseMs ?? DEFAULT_LEASE_MS;
|
|
65
|
+
const renewMs = opts.renewMs ?? DEFAULT_RENEW_MS;
|
|
66
|
+
const maxAttempts = opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
67
|
+
const pollIdleMs = opts.pollIdleMs ?? DEFAULT_POLL_IDLE_MS;
|
|
68
|
+
const log = opts.log ?? (() => { });
|
|
69
|
+
let runner = null;
|
|
70
|
+
// v4.5 Phase 7c — `runnerKind` lets diagnostics + tests distinguish
|
|
71
|
+
// the placeholder-runner phase from the real-agent-runner phase.
|
|
72
|
+
// The factory-bound initial runner is tagged 'placeholder' by
|
|
73
|
+
// default; `installRunner` flips to 'real'.
|
|
74
|
+
let _runnerKind = 'none';
|
|
75
|
+
let _started = false;
|
|
76
|
+
let _stopping = false;
|
|
77
|
+
let _pollTimer = null;
|
|
78
|
+
const _inflight = new Map();
|
|
79
|
+
const _workerPromises = new Set();
|
|
80
|
+
const _stats = {
|
|
81
|
+
claimed: 0,
|
|
82
|
+
succeeded: 0,
|
|
83
|
+
failed: 0,
|
|
84
|
+
deadLetter: 0,
|
|
85
|
+
deliverOnly: 0,
|
|
86
|
+
misconfigured: 0,
|
|
87
|
+
};
|
|
88
|
+
// ── Trigger spec lookup ─────────────────────────────────────────────────
|
|
89
|
+
function readTriggerSpec(sourceKey) {
|
|
90
|
+
try {
|
|
91
|
+
const row = opts.db
|
|
92
|
+
.prepare('SELECT * FROM triggers WHERE id = ?')
|
|
93
|
+
.get(sourceKey);
|
|
94
|
+
return row ?? null;
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
log('warn', `[dispatcher] failed to read trigger spec ${sourceKey}: ${e instanceof Error ? e.message : String(e)}`);
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// ── Build TriggerInvocationContext ──────────────────────────────────────
|
|
102
|
+
function buildContext(event, spec) {
|
|
103
|
+
return {
|
|
104
|
+
triggerId: event.sourceKey,
|
|
105
|
+
source: event.source,
|
|
106
|
+
sourceKey: event.sourceKey,
|
|
107
|
+
fireReason: typeof event.payload.fireReason === 'string'
|
|
108
|
+
? event.payload.fireReason
|
|
109
|
+
: 'trigger_fired',
|
|
110
|
+
eventId: event.id,
|
|
111
|
+
attempt: event.attempts,
|
|
112
|
+
maxAttempts,
|
|
113
|
+
promptTemplate: spec?.prompt_template ?? null,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// ── Render initial message ──────────────────────────────────────────────
|
|
117
|
+
/**
|
|
118
|
+
* Returns the rendered message + missing-vars list. When no
|
|
119
|
+
* template is set, falls back to a structured JSON dump of the
|
|
120
|
+
* payload — the agent gets a readable initial prompt even
|
|
121
|
+
* without operator-supplied templates.
|
|
122
|
+
*/
|
|
123
|
+
function renderInitialMessage(event, template) {
|
|
124
|
+
if (template && template.length > 0) {
|
|
125
|
+
const vars = (0, promptTemplate_1.flattenPayloadToVars)(event.payload);
|
|
126
|
+
// Inject a couple of dispatcher-known fields so templates can
|
|
127
|
+
// reference {{eventId}} / {{source}} / {{attempt}} / etc.
|
|
128
|
+
vars.eventId = event.id;
|
|
129
|
+
vars.source = event.source;
|
|
130
|
+
vars.sourceKey = event.sourceKey;
|
|
131
|
+
vars.attempt = event.attempts;
|
|
132
|
+
const { rendered, missing } = (0, promptTemplate_1.renderPromptTemplate)(template, vars);
|
|
133
|
+
return { message: rendered, missing };
|
|
134
|
+
}
|
|
135
|
+
// Fallback: structured payload header so the model knows the
|
|
136
|
+
// source + can reason over the JSON body. Keeps the initial
|
|
137
|
+
// message useful when the operator skipped the template.
|
|
138
|
+
return {
|
|
139
|
+
message: defaultInitialMessage(event),
|
|
140
|
+
missing: [],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function defaultInitialMessage(event) {
|
|
144
|
+
const header = `Trigger fired: ${event.source} (id=${event.sourceKey})`;
|
|
145
|
+
let body;
|
|
146
|
+
try {
|
|
147
|
+
body = JSON.stringify(event.payload, null, 2);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
body = '<payload not serialisable>';
|
|
151
|
+
}
|
|
152
|
+
return `${header}\n\nPayload:\n\`\`\`json\n${body}\n\`\`\``;
|
|
153
|
+
}
|
|
154
|
+
// ── Lease renewal ───────────────────────────────────────────────────────
|
|
155
|
+
function startRenewTimer(eventId, claimToken) {
|
|
156
|
+
const t = setInterval(() => {
|
|
157
|
+
try {
|
|
158
|
+
const ok = opts.triggerBus.renewClaim(eventId, claimToken, leaseMs);
|
|
159
|
+
if (!ok) {
|
|
160
|
+
log('warn', `[dispatcher] renew failed eventId=${eventId} — claim invalidated`);
|
|
161
|
+
clearInterval(t);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
log('warn', `[dispatcher] renew threw eventId=${eventId}: ${e instanceof Error ? e.message : String(e)}`);
|
|
166
|
+
}
|
|
167
|
+
}, renewMs);
|
|
168
|
+
if (typeof t.unref === 'function')
|
|
169
|
+
t.unref();
|
|
170
|
+
return t;
|
|
171
|
+
}
|
|
172
|
+
// ── Process one claim ───────────────────────────────────────────────────
|
|
173
|
+
async function processClaim(event) {
|
|
174
|
+
const spec = readTriggerSpec(event.sourceKey);
|
|
175
|
+
const context = buildContext(event, spec);
|
|
176
|
+
const sessionId = (0, sessionId_1.buildTriggerSessionId)({
|
|
177
|
+
source: event.source,
|
|
178
|
+
sourceKey: event.sourceKey,
|
|
179
|
+
idempotencyKey: event.idempotencyKey,
|
|
180
|
+
});
|
|
181
|
+
// Record in-flight.
|
|
182
|
+
const inflightRow = {
|
|
183
|
+
eventId: event.id,
|
|
184
|
+
sessionId,
|
|
185
|
+
source: event.source,
|
|
186
|
+
startedAt: Date.now(),
|
|
187
|
+
};
|
|
188
|
+
_inflight.set(event.id, inflightRow);
|
|
189
|
+
// Start renew timer; clean up on every exit path.
|
|
190
|
+
const renewTimer = startRenewTimer(event.id, event.claimToken);
|
|
191
|
+
try {
|
|
192
|
+
// Render initial message + check missing vars.
|
|
193
|
+
const { message, missing } = renderInitialMessage(event, spec?.prompt_template ?? null);
|
|
194
|
+
if (missing.length > 0 && spec?.prompt_template) {
|
|
195
|
+
const reason = `trigger_misconfigured: template references undefined vars: ${missing.join(', ')}`;
|
|
196
|
+
log('warn', `[dispatcher] ${reason} (eventId=${event.id} trigger=${event.sourceKey})`);
|
|
197
|
+
// misconfigured = permanent failure; no cooldown needed since
|
|
198
|
+
// retry won't help, but we still call markFailed (which will
|
|
199
|
+
// dead-letter after maxAttempts).
|
|
200
|
+
opts.triggerBus.markFailed(event.id, event.claimToken, reason, { maxAttempts });
|
|
201
|
+
_stats.misconfigured += 1;
|
|
202
|
+
_stats.failed += 1;
|
|
203
|
+
// markFailed may transition to dead_letter when attempts >= maxAttempts.
|
|
204
|
+
if (event.attempts >= maxAttempts)
|
|
205
|
+
_stats.deadLetter += 1;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
// Build the input + branch on deliverOnly.
|
|
209
|
+
const deliverOnly = spec?.deliver_only === 1;
|
|
210
|
+
const input = {
|
|
211
|
+
sessionId,
|
|
212
|
+
instanceId: opts.instanceId,
|
|
213
|
+
triggerEventId: event.id,
|
|
214
|
+
triggerContext: context,
|
|
215
|
+
initialMessage: message,
|
|
216
|
+
deliverOnly,
|
|
217
|
+
};
|
|
218
|
+
let result;
|
|
219
|
+
if (deliverOnly) {
|
|
220
|
+
result = (0, agentRunner_1.deliverOnlyStub)(input, opts.runStore);
|
|
221
|
+
_stats.deliverOnly += 1;
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
if (!runner) {
|
|
225
|
+
// Safety net — start() guarantees runner is set, but be
|
|
226
|
+
// defensive in case of test misuse.
|
|
227
|
+
throw new Error('dispatcher: runnerFactory has not been invoked');
|
|
228
|
+
}
|
|
229
|
+
// Discard return — we just need it to not be unused.
|
|
230
|
+
void agentRunner_1.buildInitialHistory;
|
|
231
|
+
result = await runner.invoke(input);
|
|
232
|
+
}
|
|
233
|
+
// Map finishReason to bus action.
|
|
234
|
+
if (result.finishReason === 'error') {
|
|
235
|
+
const errMsg = result.error ?? 'agent reported error finish';
|
|
236
|
+
opts.triggerBus.markFailed(event.id, event.claimToken, errMsg, {
|
|
237
|
+
maxAttempts,
|
|
238
|
+
cooldownMs: (0, realAgentRunner_1.computeRetryCooldownMs)(event.attempts),
|
|
239
|
+
});
|
|
240
|
+
_stats.failed += 1;
|
|
241
|
+
if (event.attempts >= maxAttempts)
|
|
242
|
+
_stats.deadLetter += 1;
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
// Success — markDone with the runId.
|
|
246
|
+
opts.triggerBus.markDone(event.id, event.claimToken, result.runId);
|
|
247
|
+
_stats.succeeded += 1;
|
|
248
|
+
}
|
|
249
|
+
catch (e) {
|
|
250
|
+
const msg = e instanceof Error ? (e.stack ?? e.message) : String(e);
|
|
251
|
+
log('error', `[dispatcher] worker threw eventId=${event.id}: ${msg}`);
|
|
252
|
+
try {
|
|
253
|
+
opts.triggerBus.markFailed(event.id, event.claimToken, msg.slice(0, 500), {
|
|
254
|
+
maxAttempts,
|
|
255
|
+
cooldownMs: (0, realAgentRunner_1.computeRetryCooldownMs)(event.attempts),
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
catch { /* bus may be in a weird state; swallow */ }
|
|
259
|
+
_stats.failed += 1;
|
|
260
|
+
if (event.attempts >= maxAttempts)
|
|
261
|
+
_stats.deadLetter += 1;
|
|
262
|
+
}
|
|
263
|
+
finally {
|
|
264
|
+
clearInterval(renewTimer);
|
|
265
|
+
_inflight.delete(event.id);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// ── Worker semaphore + claim loop ───────────────────────────────────────
|
|
269
|
+
async function _pollOnce() {
|
|
270
|
+
if (_stopping)
|
|
271
|
+
return null;
|
|
272
|
+
if (_inflight.size >= workerCount)
|
|
273
|
+
return null;
|
|
274
|
+
const event = opts.triggerBus.claim({ ownerId: opts.ownerId, leaseMs });
|
|
275
|
+
if (!event)
|
|
276
|
+
return null;
|
|
277
|
+
_stats.claimed += 1;
|
|
278
|
+
log('info', `[dispatcher] claimed eventId=${event.id} source=${event.source} attempt=${event.attempts}/${maxAttempts}`);
|
|
279
|
+
const p = processClaim(event).finally(() => {
|
|
280
|
+
_workerPromises.delete(p);
|
|
281
|
+
});
|
|
282
|
+
_workerPromises.add(p);
|
|
283
|
+
return event.id;
|
|
284
|
+
}
|
|
285
|
+
function _schedulePoll() {
|
|
286
|
+
if (_stopping)
|
|
287
|
+
return;
|
|
288
|
+
if (_pollTimer)
|
|
289
|
+
return;
|
|
290
|
+
_pollTimer = setTimeout(async () => {
|
|
291
|
+
_pollTimer = null;
|
|
292
|
+
try {
|
|
293
|
+
const claimedId = await _pollOnce();
|
|
294
|
+
// Adaptive: if we just claimed something, re-poll immediately
|
|
295
|
+
// (drain bus); otherwise wait `pollIdleMs`.
|
|
296
|
+
const next = claimedId !== null ? DEFAULT_POLL_BUSY_MS : pollIdleMs;
|
|
297
|
+
if (!_stopping) {
|
|
298
|
+
_pollTimer = setTimeout(_schedulePollFromTimer, next);
|
|
299
|
+
if (_pollTimer && typeof _pollTimer.unref === 'function')
|
|
300
|
+
_pollTimer.unref();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
catch (e) {
|
|
304
|
+
log('error', `[dispatcher] poll threw: ${e instanceof Error ? e.message : String(e)}`);
|
|
305
|
+
if (!_stopping) {
|
|
306
|
+
_pollTimer = setTimeout(_schedulePollFromTimer, pollIdleMs);
|
|
307
|
+
if (_pollTimer && typeof _pollTimer.unref === 'function')
|
|
308
|
+
_pollTimer.unref();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}, pollIdleMs);
|
|
312
|
+
if (typeof _pollTimer.unref === 'function')
|
|
313
|
+
_pollTimer.unref();
|
|
314
|
+
}
|
|
315
|
+
function _schedulePollFromTimer() {
|
|
316
|
+
_pollTimer = null;
|
|
317
|
+
_schedulePoll();
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
start() {
|
|
321
|
+
if (_started)
|
|
322
|
+
return;
|
|
323
|
+
_started = true;
|
|
324
|
+
runner = opts.runnerFactory();
|
|
325
|
+
// Initial runner from the factory is the placeholder unless the
|
|
326
|
+
// factory itself returned a real runner. The CLI's two-phase
|
|
327
|
+
// bootstrap (Phase 7c) starts with a placeholder factory and
|
|
328
|
+
// calls `installRunner` later with the real one.
|
|
329
|
+
_runnerKind = 'placeholder';
|
|
330
|
+
log('info', `[dispatcher] starting workerCount=${workerCount} leaseMs=${leaseMs} runner=placeholder`);
|
|
331
|
+
_schedulePoll();
|
|
332
|
+
},
|
|
333
|
+
installRunner(next) {
|
|
334
|
+
// Atomic swap — JS single-threaded execution means the read in
|
|
335
|
+
// _pumpOnce / processClaim can never see a half-installed runner.
|
|
336
|
+
runner = next;
|
|
337
|
+
_runnerKind = 'real';
|
|
338
|
+
log('info', `[dispatcher] runner swapped → real (next claim uses new runner)`);
|
|
339
|
+
},
|
|
340
|
+
runnerKind() {
|
|
341
|
+
return _runnerKind;
|
|
342
|
+
},
|
|
343
|
+
async stop(timeoutMs = DEFAULT_STOP_TIMEOUT_MS) {
|
|
344
|
+
_stopping = true;
|
|
345
|
+
if (_pollTimer) {
|
|
346
|
+
clearTimeout(_pollTimer);
|
|
347
|
+
_pollTimer = null;
|
|
348
|
+
}
|
|
349
|
+
// Race in-flight drain against the timeout.
|
|
350
|
+
const drain = Promise.allSettled([..._workerPromises]);
|
|
351
|
+
const deadline = new Promise((resolve) => {
|
|
352
|
+
const t = setTimeout(() => resolve(), timeoutMs);
|
|
353
|
+
if (typeof t.unref === 'function')
|
|
354
|
+
t.unref();
|
|
355
|
+
});
|
|
356
|
+
await Promise.race([drain, deadline]);
|
|
357
|
+
log('info', `[dispatcher] stopped — inflight=${_inflight.size} processed=${_stats.claimed}`);
|
|
358
|
+
},
|
|
359
|
+
inflight() {
|
|
360
|
+
return [..._inflight.values()];
|
|
361
|
+
},
|
|
362
|
+
stats() {
|
|
363
|
+
return { ..._stats };
|
|
364
|
+
},
|
|
365
|
+
async _pumpOnce() {
|
|
366
|
+
if (!runner)
|
|
367
|
+
runner = opts.runnerFactory();
|
|
368
|
+
const event = opts.triggerBus.claim({ ownerId: opts.ownerId, leaseMs });
|
|
369
|
+
if (!event)
|
|
370
|
+
return null;
|
|
371
|
+
_stats.claimed += 1;
|
|
372
|
+
// Track the worker promise so stop() can wait on it.
|
|
373
|
+
const p = processClaim(event);
|
|
374
|
+
_workerPromises.add(p);
|
|
375
|
+
try {
|
|
376
|
+
await p;
|
|
377
|
+
}
|
|
378
|
+
finally {
|
|
379
|
+
_workerPromises.delete(p);
|
|
380
|
+
}
|
|
381
|
+
return event.id;
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
// Unique id helper (kept here so dispatchers in tests can use the
|
|
386
|
+
// same util their producers do when fabricating event ids).
|
|
387
|
+
function _dispatcherOwnerId(prefix = 'disp') {
|
|
388
|
+
return `${prefix}-${(0, node_crypto_1.randomUUID)()}`;
|
|
389
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/daemon/dispatcher/fireRateLimiter.ts — v4.5 Phase 5a.
|
|
10
|
+
*
|
|
11
|
+
* Per-trigger sliding-window fire-rate cap.
|
|
12
|
+
*
|
|
13
|
+
* Q-P5-5(c): unlimited by default. Each trigger spec carries an
|
|
14
|
+
* optional `fireRateLimit` (rows-per-window). When set, this
|
|
15
|
+
* module enforces a per-triggerId sliding window of size
|
|
16
|
+
* `windowMs` (default 1 hour). The Nth fire within the window is
|
|
17
|
+
* the LAST one allowed; the (N+1)th and beyond are blocked.
|
|
18
|
+
*
|
|
19
|
+
* Behaviour when blocked:
|
|
20
|
+
* - `check()` returns `false` + populates `reason` field
|
|
21
|
+
* - producer chooses what to do (typical: insert event + immediately
|
|
22
|
+
* dead-letter so the operator has forensic visibility via
|
|
23
|
+
* `/api/daemon/triggers/<id>/stats`)
|
|
24
|
+
*
|
|
25
|
+
* Anti-thrash motivation (from the prior-systems learning batch): a
|
|
26
|
+
* misconfigured webhook upstream can fire 60k times per minute. A
|
|
27
|
+
* busted file watcher on a temp directory can fire 1000 times per
|
|
28
|
+
* second. The producer-side cap stops the bus from inflating and
|
|
29
|
+
* keeps the operator's ability to query "what blew up" intact.
|
|
30
|
+
*
|
|
31
|
+
* Storage: in-memory `Map<triggerId, number[]>` of fire timestamps
|
|
32
|
+
* within the active window. Pruned lazily on each `check()` —
|
|
33
|
+
* old timestamps drop off the front when they fall outside `now -
|
|
34
|
+
* windowMs`. Bounded by max-fires-per-window (no unbounded growth
|
|
35
|
+
* even if the producer keeps hammering — once over the cap,
|
|
36
|
+
* incoming attempts are rejected immediately without recording).
|
|
37
|
+
*
|
|
38
|
+
* Idempotent reset via `__resetForTests()`.
|
|
39
|
+
*/
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.createFireRateLimiter = createFireRateLimiter;
|
|
42
|
+
exports.getFireRateLimiter = getFireRateLimiter;
|
|
43
|
+
exports.__resetFireRateLimiterSingletonForTests = __resetFireRateLimiterSingletonForTests;
|
|
44
|
+
const DEFAULT_WINDOW_MS = 60 * 60 * 1000; // 1 hour
|
|
45
|
+
function createFireRateLimiter(opts = {}) {
|
|
46
|
+
const windowMs = opts.windowMs ?? DEFAULT_WINDOW_MS;
|
|
47
|
+
const windows = new Map();
|
|
48
|
+
function prune(triggerId, now) {
|
|
49
|
+
const cutoff = now - windowMs;
|
|
50
|
+
const list = windows.get(triggerId);
|
|
51
|
+
if (!list)
|
|
52
|
+
return [];
|
|
53
|
+
// Find the first timestamp >= cutoff and slice. Most-common case:
|
|
54
|
+
// few or zero entries to drop, so iterate from the front.
|
|
55
|
+
let i = 0;
|
|
56
|
+
while (i < list.length && list[i] < cutoff)
|
|
57
|
+
i++;
|
|
58
|
+
if (i > 0) {
|
|
59
|
+
const kept = list.slice(i);
|
|
60
|
+
if (kept.length === 0)
|
|
61
|
+
windows.delete(triggerId);
|
|
62
|
+
else
|
|
63
|
+
windows.set(triggerId, kept);
|
|
64
|
+
return kept;
|
|
65
|
+
}
|
|
66
|
+
return list;
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
check(triggerId, limit, now = Date.now()) {
|
|
70
|
+
// Unlimited path — bypass entirely. No window recording.
|
|
71
|
+
if (limit === null || limit === undefined || limit <= 0) {
|
|
72
|
+
return { allowed: true, windowCount: 0, limit };
|
|
73
|
+
}
|
|
74
|
+
const list = prune(triggerId, now);
|
|
75
|
+
if (list.length >= limit) {
|
|
76
|
+
return {
|
|
77
|
+
allowed: false,
|
|
78
|
+
windowCount: list.length,
|
|
79
|
+
limit,
|
|
80
|
+
reason: `fire-rate cap exceeded: ${list.length}/${limit} per ${Math.round(windowMs / 1000)}s window`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// Allowed — record the fire.
|
|
84
|
+
const next = [...list, now];
|
|
85
|
+
windows.set(triggerId, next);
|
|
86
|
+
return { allowed: true, windowCount: next.length, limit };
|
|
87
|
+
},
|
|
88
|
+
peek(triggerId, now = Date.now()) {
|
|
89
|
+
return prune(triggerId, now).length;
|
|
90
|
+
},
|
|
91
|
+
reset(triggerId) {
|
|
92
|
+
windows.delete(triggerId);
|
|
93
|
+
},
|
|
94
|
+
__resetForTests() {
|
|
95
|
+
windows.clear();
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
// Process-wide singleton — most call sites (producer-side fire
|
|
100
|
+
// gates) want a shared limiter. Tests instantiate their own.
|
|
101
|
+
let _singleton = null;
|
|
102
|
+
/** Return the process-wide limiter, creating it on first call. */
|
|
103
|
+
function getFireRateLimiter() {
|
|
104
|
+
if (!_singleton)
|
|
105
|
+
_singleton = createFireRateLimiter();
|
|
106
|
+
return _singleton;
|
|
107
|
+
}
|
|
108
|
+
/** Test-only — reset the singleton. */
|
|
109
|
+
function __resetFireRateLimiterSingletonForTests() {
|
|
110
|
+
if (_singleton)
|
|
111
|
+
_singleton.__resetForTests();
|
|
112
|
+
_singleton = null;
|
|
113
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/daemon/dispatcher/index.ts — v4.5 Phase 5a barrel.
|
|
10
|
+
*
|
|
11
|
+
* Single import point for the trigger dispatcher subsystem.
|
|
12
|
+
* Callers should import from here (`../dispatcher`) rather than
|
|
13
|
+
* the individual module files so internal refactors stay
|
|
14
|
+
* encapsulated.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.createPerTurnBudgetWatcher = exports.consumePostTurn = exports.evaluatePreTurn = exports.utcDateKey = exports.createDailyBudgetTracker = exports.DEFAULT_DAEMON_APPROVAL_POLICY = exports.isDaemonApprovalPolicy = exports.decideForPolicy = exports.buildDaemonApprovalCallbacks = exports.resolveDaemonModel = exports.RETRY_DECISION = exports.computeRetryCooldownMs = exports.createRealAgentRunner = exports.makeRunner = exports.deliverOnlyStub = exports.buildInitialHistory = exports.__resetFireRateLimiterSingletonForTests = exports.getFireRateLimiter = exports.createFireRateLimiter = exports.flattenPayloadToVars = exports.renderPromptTemplate = exports.parseTriggerSessionId = exports.buildTriggerSessionId = exports._dispatcherOwnerId = exports.createDispatcher = void 0;
|
|
18
|
+
var dispatcher_1 = require("./dispatcher");
|
|
19
|
+
Object.defineProperty(exports, "createDispatcher", { enumerable: true, get: function () { return dispatcher_1.createDispatcher; } });
|
|
20
|
+
Object.defineProperty(exports, "_dispatcherOwnerId", { enumerable: true, get: function () { return dispatcher_1._dispatcherOwnerId; } });
|
|
21
|
+
var sessionId_1 = require("./sessionId");
|
|
22
|
+
Object.defineProperty(exports, "buildTriggerSessionId", { enumerable: true, get: function () { return sessionId_1.buildTriggerSessionId; } });
|
|
23
|
+
Object.defineProperty(exports, "parseTriggerSessionId", { enumerable: true, get: function () { return sessionId_1.parseTriggerSessionId; } });
|
|
24
|
+
var promptTemplate_1 = require("./promptTemplate");
|
|
25
|
+
Object.defineProperty(exports, "renderPromptTemplate", { enumerable: true, get: function () { return promptTemplate_1.renderPromptTemplate; } });
|
|
26
|
+
Object.defineProperty(exports, "flattenPayloadToVars", { enumerable: true, get: function () { return promptTemplate_1.flattenPayloadToVars; } });
|
|
27
|
+
var fireRateLimiter_1 = require("./fireRateLimiter");
|
|
28
|
+
Object.defineProperty(exports, "createFireRateLimiter", { enumerable: true, get: function () { return fireRateLimiter_1.createFireRateLimiter; } });
|
|
29
|
+
Object.defineProperty(exports, "getFireRateLimiter", { enumerable: true, get: function () { return fireRateLimiter_1.getFireRateLimiter; } });
|
|
30
|
+
Object.defineProperty(exports, "__resetFireRateLimiterSingletonForTests", { enumerable: true, get: function () { return fireRateLimiter_1.__resetFireRateLimiterSingletonForTests; } });
|
|
31
|
+
var agentRunner_1 = require("./agentRunner");
|
|
32
|
+
Object.defineProperty(exports, "buildInitialHistory", { enumerable: true, get: function () { return agentRunner_1.buildInitialHistory; } });
|
|
33
|
+
Object.defineProperty(exports, "deliverOnlyStub", { enumerable: true, get: function () { return agentRunner_1.deliverOnlyStub; } });
|
|
34
|
+
Object.defineProperty(exports, "makeRunner", { enumerable: true, get: function () { return agentRunner_1.makeRunner; } });
|
|
35
|
+
// v4.5 Phase 7 — real agent runner + dependencies.
|
|
36
|
+
var realAgentRunner_1 = require("./realAgentRunner");
|
|
37
|
+
Object.defineProperty(exports, "createRealAgentRunner", { enumerable: true, get: function () { return realAgentRunner_1.createRealAgentRunner; } });
|
|
38
|
+
Object.defineProperty(exports, "computeRetryCooldownMs", { enumerable: true, get: function () { return realAgentRunner_1.computeRetryCooldownMs; } });
|
|
39
|
+
Object.defineProperty(exports, "RETRY_DECISION", { enumerable: true, get: function () { return realAgentRunner_1.RETRY_DECISION; } });
|
|
40
|
+
var resolveModel_1 = require("./resolveModel");
|
|
41
|
+
Object.defineProperty(exports, "resolveDaemonModel", { enumerable: true, get: function () { return resolveModel_1.resolveDaemonModel; } });
|
|
42
|
+
var daemonApproval_1 = require("./daemonApproval");
|
|
43
|
+
Object.defineProperty(exports, "buildDaemonApprovalCallbacks", { enumerable: true, get: function () { return daemonApproval_1.buildDaemonApprovalCallbacks; } });
|
|
44
|
+
Object.defineProperty(exports, "decideForPolicy", { enumerable: true, get: function () { return daemonApproval_1.decideForPolicy; } });
|
|
45
|
+
Object.defineProperty(exports, "isDaemonApprovalPolicy", { enumerable: true, get: function () { return daemonApproval_1.isDaemonApprovalPolicy; } });
|
|
46
|
+
Object.defineProperty(exports, "DEFAULT_DAEMON_APPROVAL_POLICY", { enumerable: true, get: function () { return daemonApproval_1.DEFAULT_DAEMON_APPROVAL_POLICY; } });
|
|
47
|
+
var dailyBudgetTracker_1 = require("./dailyBudgetTracker");
|
|
48
|
+
Object.defineProperty(exports, "createDailyBudgetTracker", { enumerable: true, get: function () { return dailyBudgetTracker_1.createDailyBudgetTracker; } });
|
|
49
|
+
Object.defineProperty(exports, "utcDateKey", { enumerable: true, get: function () { return dailyBudgetTracker_1.utcDateKey; } });
|
|
50
|
+
var budgetGate_1 = require("./budgetGate");
|
|
51
|
+
Object.defineProperty(exports, "evaluatePreTurn", { enumerable: true, get: function () { return budgetGate_1.evaluatePreTurn; } });
|
|
52
|
+
Object.defineProperty(exports, "consumePostTurn", { enumerable: true, get: function () { return budgetGate_1.consumePostTurn; } });
|
|
53
|
+
Object.defineProperty(exports, "createPerTurnBudgetWatcher", { enumerable: true, get: function () { return budgetGate_1.createPerTurnBudgetWatcher; } });
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/daemon/dispatcher/promptTemplate.ts — v4.5 Phase 5a.
|
|
10
|
+
*
|
|
11
|
+
* Minimal `{{var}}` interpolation for trigger prompt templates.
|
|
12
|
+
*
|
|
13
|
+
* Per Q-P5-2(a): NO conditionals, NO loops, NO escapes. A trigger's
|
|
14
|
+
* spec.promptTemplate is a one-line-or-multi-line string with
|
|
15
|
+
* `{{path}}`, `{{event}}`, `{{from}}`, `{{subject}}` placeholders.
|
|
16
|
+
* The dispatcher renders it with payload-derived variables when
|
|
17
|
+
* deliverOnly is true OR the agent's initial message comes from a
|
|
18
|
+
* template.
|
|
19
|
+
*
|
|
20
|
+
* Missing-variable behaviour: the placeholder is LEFT IN PLACE and
|
|
21
|
+
* the name is collected in `missing`. The dispatcher decides what
|
|
22
|
+
* to do — currently it classifies a non-empty `missing` array as
|
|
23
|
+
* `trigger_misconfigured` when the template was non-empty.
|
|
24
|
+
*
|
|
25
|
+
* Whitespace inside braces tolerated: `{{ path }}` ≡ `{{path}}`.
|
|
26
|
+
* Unknown shapes (nested braces, `{{ }}` empty name) fail soft:
|
|
27
|
+
* left in place, not collected.
|
|
28
|
+
*
|
|
29
|
+
* Pure module — no I/O, no side effects, fully synchronous.
|
|
30
|
+
*/
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.renderPromptTemplate = renderPromptTemplate;
|
|
33
|
+
exports.flattenPayloadToVars = flattenPayloadToVars;
|
|
34
|
+
const PLACEHOLDER = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g;
|
|
35
|
+
/**
|
|
36
|
+
* Render a template with `{{var}}` placeholders.
|
|
37
|
+
*
|
|
38
|
+
* - Variables are looked up by exact name (after trimming whitespace).
|
|
39
|
+
* - Numeric / boolean values are stringified via `String(v)`.
|
|
40
|
+
* - `null` / `undefined` are treated as missing → placeholder left
|
|
41
|
+
* in the output AND the variable name pushed onto `missing`.
|
|
42
|
+
* - Empty / whitespace-only template → returns `{ rendered: '', missing: [] }`.
|
|
43
|
+
*
|
|
44
|
+
* @param template Raw template string (may be empty).
|
|
45
|
+
* @param vars Variable map. Excess keys are ignored.
|
|
46
|
+
*/
|
|
47
|
+
function renderPromptTemplate(template, vars) {
|
|
48
|
+
if (typeof template !== 'string' || template.length === 0) {
|
|
49
|
+
return { rendered: '', missing: [] };
|
|
50
|
+
}
|
|
51
|
+
const missingSet = new Set();
|
|
52
|
+
const rendered = template.replace(PLACEHOLDER, (match, name) => {
|
|
53
|
+
const v = vars[name];
|
|
54
|
+
if (v === undefined || v === null) {
|
|
55
|
+
missingSet.add(name);
|
|
56
|
+
return match; // leave placeholder in place
|
|
57
|
+
}
|
|
58
|
+
return String(v);
|
|
59
|
+
});
|
|
60
|
+
return { rendered, missing: [...missingSet] };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Convenience helper for the common case where the dispatcher
|
|
64
|
+
* already has a TriggerEventRow payload + a few fixed fields
|
|
65
|
+
* and wants to render the template for that event. Caller can
|
|
66
|
+
* also call `renderPromptTemplate` directly with any var map.
|
|
67
|
+
*
|
|
68
|
+
* Variable shape varies by source — this helper just flattens
|
|
69
|
+
* the payload into top-level keys (e.g. `payload.from` →
|
|
70
|
+
* `{{from}}`). String/number/boolean values pass through;
|
|
71
|
+
* objects/arrays are JSON-stringified so they're at least
|
|
72
|
+
* substitutable (though usually undesirable in a user-facing
|
|
73
|
+
* prompt — Phase 5b cron may extend this with `{{json:foo}}`
|
|
74
|
+
* if needed).
|
|
75
|
+
*/
|
|
76
|
+
function flattenPayloadToVars(payload) {
|
|
77
|
+
const out = {};
|
|
78
|
+
for (const [k, v] of Object.entries(payload)) {
|
|
79
|
+
if (v === null || v === undefined) {
|
|
80
|
+
out[k] = null;
|
|
81
|
+
}
|
|
82
|
+
else if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {
|
|
83
|
+
out[k] = v;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
try {
|
|
87
|
+
out[k] = JSON.stringify(v);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
out[k] = String(v);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|