@lazyingart/agintiflow 0.20.206 → 0.20.207
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/package.json +3 -2
- package/scripts/smoke-progressive-tool-selection.js +130 -0
- package/scripts/smoke-skills.js +7 -0
- package/skills/data-analysis/SKILL.md +3 -2
- package/src/agent-runner.js +52 -7
- package/src/integration-core-event-projector.js +116 -32
- package/src/integration-native-executor.js +90 -15
- package/src/integration-run-registry.js +44 -14
- package/src/integration-runtime-authority.js +1602 -208
- package/src/integration-session-persistence.js +504 -0
- package/src/interactive-cli.js +1 -1
- package/src/local-failure-recovery.js +194 -0
- package/src/progressive-tool-selection.js +146 -0
- package/src/session-store.js +503 -416
- package/src/skill-library.js +7 -2
- package/src/task-profiles.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.207",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AgInTiFlow is a project-aware agent workspace for hybrid wet-dry R&D, hardware-aware intelligence, software automation, and industrial workflows.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -152,6 +152,7 @@
|
|
|
152
152
|
"smoke:runtime-core": "node scripts/smoke-runtime-core.js",
|
|
153
153
|
"smoke:localllm-auto-max": "node scripts/smoke-localllm-auto-max.js",
|
|
154
154
|
"smoke:localllm-code-routing": "node scripts/smoke-localllm-code-routing.js",
|
|
155
|
+
"smoke:local-failure-recovery": "node scripts/smoke-local-failure-recovery.js",
|
|
155
156
|
"smoke:local-resource-policy": "node scripts/smoke-local-resource-policy.js",
|
|
156
157
|
"smoke:context-budget-recovery": "node scripts/smoke-context-budget-recovery.js",
|
|
157
158
|
"smoke:localllm-model-tiers": "node scripts/smoke-localllm-model-tiers.js",
|
|
@@ -188,7 +189,7 @@
|
|
|
188
189
|
"storage:migrate": "node bin/aginti-cli.js storage migrate",
|
|
189
190
|
"publish:env": "node scripts/npm-publish-from-env.js publish --access public",
|
|
190
191
|
"publish:env:whoami": "node scripts/npm-publish-from-env.js whoami",
|
|
191
|
-
"test": "npm run check && npm run smoke:integration-production-mount && npm run smoke:localllm-provider && npm run smoke:localllm-model-tiers && npm run smoke:localllm-code-routing && npm run smoke:localllm-auto-max && npm run smoke:local-resource-policy && npm run smoke:context-budget-recovery && npm run smoke:session-runtime && npm run smoke:runtime-core && npm run smoke:progressive-tools && npm run smoke:truthful-completion && npm run smoke:writing-specialist-routing && npm run eval:local-first-agent && npm run eval:provider-attribution && npm run smoke:runtime-compat && npm run smoke:autoupdate && npm run smoke:web-api && npm run smoke:math-rendering && npm run smoke:web-ui && npm run smoke:web-autostart && npm run smoke:webapp-command && npm run smoke:web-port-fallback && npm run smoke:docker-command && npm run smoke:coding-tools && npm run smoke:dynamic-step-budget && npm run smoke:execution-policy && npm run smoke:aaps-adapter && npm run smoke:auxiliary-tools && npm run smoke:perception-research && npm run smoke:deep-research && npm run smoke:public-research && npm run smoke:safe-chat && npm run smoke:auth && npm run smoke:agentlink && npm run smoke:canvas-artifacts && npm run smoke:capabilities && npm run smoke:mcp && npm run smoke:model-roles && npm run smoke:platform && npm run smoke:permission-modes && npm run smoke:skills && npm run smoke:skillmesh && npm run smoke:supervision-ledger && npm run smoke:tmux-tools && npm run smoke:long-jobs && npm run smoke:run-stdin && npm run smoke:cli-chat && npm run smoke:inbox",
|
|
192
|
+
"test": "npm run check && npm run smoke:integration-production-mount && npm run smoke:localllm-provider && npm run smoke:localllm-model-tiers && npm run smoke:localllm-code-routing && npm run smoke:local-failure-recovery && npm run smoke:localllm-auto-max && npm run smoke:local-resource-policy && npm run smoke:context-budget-recovery && npm run smoke:session-runtime && npm run smoke:runtime-core && npm run smoke:progressive-tools && npm run smoke:truthful-completion && npm run smoke:writing-specialist-routing && npm run eval:local-first-agent && npm run eval:provider-attribution && npm run smoke:runtime-compat && npm run smoke:autoupdate && npm run smoke:web-api && npm run smoke:math-rendering && npm run smoke:web-ui && npm run smoke:web-autostart && npm run smoke:webapp-command && npm run smoke:web-port-fallback && npm run smoke:docker-command && npm run smoke:coding-tools && npm run smoke:dynamic-step-budget && npm run smoke:execution-policy && npm run smoke:aaps-adapter && npm run smoke:auxiliary-tools && npm run smoke:perception-research && npm run smoke:deep-research && npm run smoke:public-research && npm run smoke:safe-chat && npm run smoke:auth && npm run smoke:agentlink && npm run smoke:canvas-artifacts && npm run smoke:capabilities && npm run smoke:mcp && npm run smoke:model-roles && npm run smoke:platform && npm run smoke:permission-modes && npm run smoke:skills && npm run smoke:skillmesh && npm run smoke:supervision-ledger && npm run smoke:tmux-tools && npm run smoke:long-jobs && npm run smoke:run-stdin && npm run smoke:cli-chat && npm run smoke:inbox",
|
|
192
193
|
"pack:dry-run": "npm pack --dry-run",
|
|
193
194
|
"smoke:capabilities": "node scripts/smoke-capabilities.js"
|
|
194
195
|
},
|
|
@@ -204,6 +204,136 @@ sameNames(
|
|
|
204
204
|
assert(!names(artifactValidationTools).includes("search_files"), "artifact validation phase reopened broad search");
|
|
205
205
|
assert(!names(artifactValidationTools).includes("open_url"), "artifact validation phase reopened browser discovery");
|
|
206
206
|
|
|
207
|
+
const localFailureRecoveryTools = selectProgressiveTools(allTools, {
|
|
208
|
+
config: { provider: "localllm", localFailureRecoveryActive: true },
|
|
209
|
+
goal: "Recover from repeated failed edits without restarting the task.",
|
|
210
|
+
profile: "code",
|
|
211
|
+
messages: [
|
|
212
|
+
{ role: "assistant", tool_calls: [{ id: "failed-edit", function: { name: "apply_patch", arguments: "{}" } }] },
|
|
213
|
+
{ role: "tool", tool_call_id: "failed-edit", content: '{"ok":false}' },
|
|
214
|
+
],
|
|
215
|
+
});
|
|
216
|
+
sameNames(
|
|
217
|
+
localFailureRecoveryTools,
|
|
218
|
+
["read_file", "apply_patch", "write_file", "run_command", "search_files", "inspect_project", "finish"],
|
|
219
|
+
"local failure recovery did not restore the bounded repair surface"
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const dataDiscoveryStarterTools = selectProgressiveTools(allTools, {
|
|
223
|
+
config: { provider: "localllm" },
|
|
224
|
+
goal: "Clean these experiment exports and leave a reproducible analysis.",
|
|
225
|
+
profile: "data",
|
|
226
|
+
});
|
|
227
|
+
sameNames(
|
|
228
|
+
dataDiscoveryStarterTools,
|
|
229
|
+
["inspect_project", "finish"],
|
|
230
|
+
"local data task exposed mutation tools before project inspection"
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const dataInspectionMessages = [
|
|
234
|
+
{ role: "user", content: "Goal: Clean these experiment exports and leave a reproducible analysis." },
|
|
235
|
+
{
|
|
236
|
+
role: "assistant",
|
|
237
|
+
tool_calls: [{ id: "data-inspect", function: { name: "inspect_project", arguments: '{"path":"."}' } }],
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
role: "tool",
|
|
241
|
+
tool_call_id: "data-inspect",
|
|
242
|
+
content: JSON.stringify({
|
|
243
|
+
ok: true,
|
|
244
|
+
recommendedReads: ["README.md", "tests/test_analysis.py"],
|
|
245
|
+
manifestFiles: [{ path: "README.md" }],
|
|
246
|
+
testFiles: [{ path: "tests/test_analysis.py" }],
|
|
247
|
+
topLevel: [
|
|
248
|
+
{ path: "analysis.py", type: "file" },
|
|
249
|
+
{ path: "TASK.md", type: "file" },
|
|
250
|
+
],
|
|
251
|
+
}),
|
|
252
|
+
},
|
|
253
|
+
];
|
|
254
|
+
const dataContextTools = selectProgressiveTools(allTools, {
|
|
255
|
+
config: { provider: "localllm" },
|
|
256
|
+
goal: "Clean these experiment exports and leave a reproducible analysis.",
|
|
257
|
+
profile: "data",
|
|
258
|
+
messages: dataInspectionMessages,
|
|
259
|
+
});
|
|
260
|
+
sameNames(
|
|
261
|
+
dataContextTools,
|
|
262
|
+
["read_file", "finish"],
|
|
263
|
+
"local data task exposed mutation tools before reading project contracts"
|
|
264
|
+
);
|
|
265
|
+
assertStrict.deepEqual(
|
|
266
|
+
enumFor(dataContextTools, "read_file", "path"),
|
|
267
|
+
["README.md"],
|
|
268
|
+
"local data instruction phase did not constrain read_file to discovered instruction paths"
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const dataImplementationMessages = [
|
|
272
|
+
...dataInspectionMessages,
|
|
273
|
+
{
|
|
274
|
+
role: "assistant",
|
|
275
|
+
tool_calls: [{ id: "data-readme", function: { name: "read_file", arguments: '{"path":"README.md"}' } }],
|
|
276
|
+
},
|
|
277
|
+
{ role: "tool", tool_call_id: "data-readme", content: '{"ok":true,"path":"README.md"}' },
|
|
278
|
+
];
|
|
279
|
+
const dataImplementationTools = selectProgressiveTools(allTools, {
|
|
280
|
+
config: { provider: "localllm" },
|
|
281
|
+
goal: "Clean these experiment exports and leave a reproducible analysis.",
|
|
282
|
+
profile: "data",
|
|
283
|
+
messages: dataImplementationMessages,
|
|
284
|
+
});
|
|
285
|
+
sameNames(
|
|
286
|
+
dataImplementationTools,
|
|
287
|
+
["read_file", "finish"],
|
|
288
|
+
"local data task exposed mutation tools before reading an analyzer/config/test surface"
|
|
289
|
+
);
|
|
290
|
+
assertStrict.deepEqual(
|
|
291
|
+
enumFor(dataImplementationTools, "read_file", "path"),
|
|
292
|
+
["tests/test_analysis.py", "analysis.py"],
|
|
293
|
+
"local data context phase did not constrain read_file to discovered analysis paths"
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
const dataReadyMessages = [
|
|
297
|
+
...dataImplementationMessages,
|
|
298
|
+
{
|
|
299
|
+
role: "assistant",
|
|
300
|
+
tool_calls: [
|
|
301
|
+
{ id: "data-test", function: { name: "read_file", arguments: '{"path":"tests/test_analysis.py"}' } },
|
|
302
|
+
],
|
|
303
|
+
},
|
|
304
|
+
{ role: "tool", tool_call_id: "data-test", content: '{"ok":true,"path":"tests/test_analysis.py"}' },
|
|
305
|
+
];
|
|
306
|
+
const dataReadyTools = selectProgressiveTools(allTools, {
|
|
307
|
+
config: { provider: "localllm" },
|
|
308
|
+
goal: "Clean these experiment exports and leave a reproducible analysis.",
|
|
309
|
+
profile: "data",
|
|
310
|
+
messages: dataReadyMessages,
|
|
311
|
+
});
|
|
312
|
+
sameNames(dataReadyTools, LOCAL_COMPACT_CODE_TOOL_NAMES, "local data task did not unlock after bounded discovery");
|
|
313
|
+
|
|
314
|
+
const dataRecoveryBeforeDiscoveryTools = selectProgressiveTools(allTools, {
|
|
315
|
+
config: { provider: "localllm", localFailureRecoveryActive: true },
|
|
316
|
+
goal: "Recover this data task after a failed mutation.",
|
|
317
|
+
profile: "data",
|
|
318
|
+
});
|
|
319
|
+
sameNames(
|
|
320
|
+
dataRecoveryBeforeDiscoveryTools,
|
|
321
|
+
["inspect_project", "finish"],
|
|
322
|
+
"local recovery bypassed the incomplete data discovery gate"
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
const dataRecoveryAfterDiscoveryTools = selectProgressiveTools(allTools, {
|
|
326
|
+
config: { provider: "localllm", localFailureRecoveryActive: true },
|
|
327
|
+
goal: "Recover this data task after a failed mutation.",
|
|
328
|
+
profile: "data",
|
|
329
|
+
messages: dataReadyMessages,
|
|
330
|
+
});
|
|
331
|
+
sameNames(
|
|
332
|
+
dataRecoveryAfterDiscoveryTools,
|
|
333
|
+
["read_file", "apply_patch", "write_file", "run_command", "search_files", "inspect_project", "finish"],
|
|
334
|
+
"local recovery remained trapped after data discovery completed"
|
|
335
|
+
);
|
|
336
|
+
|
|
207
337
|
const artifactRepairTools = selectProgressiveTools(allTools, {
|
|
208
338
|
config: { provider: "localllm", artifactValidationPhase: true, artifactValidationNeedsRepair: true },
|
|
209
339
|
goal: "Repair the exact report from deterministic preflight evidence.",
|
package/scripts/smoke-skills.js
CHANGED
|
@@ -90,6 +90,13 @@ assert(selectedIds("debug a C# dotnet web API").includes("dotnet-csharp"), ".NET
|
|
|
90
90
|
assert(selectedIds("fix a PHP Laravel composer project").includes("php"), "PHP prompt did not select php");
|
|
91
91
|
assert(selectedIds("repair a Ruby Rails app with RSpec").includes("ruby"), "Ruby prompt did not select ruby");
|
|
92
92
|
assert(selectedIds("clean a CSV dataset and make plots").includes("data-analysis"), "data prompt did not select data-analysis");
|
|
93
|
+
const focusedDataSkills = selectedIds(
|
|
94
|
+
"these experiment exports are a mess; clean them, make a useful plot and report",
|
|
95
|
+
"data"
|
|
96
|
+
);
|
|
97
|
+
assert(focusedDataSkills.includes("data-analysis"), "explicit data profile omitted its core analysis skill");
|
|
98
|
+
assert(!focusedDataSkills.includes("novel-writing"), "explicit data profile selected unrelated novel guidance");
|
|
99
|
+
assert(!focusedDataSkills.includes("media-transcription-report"), "explicit data profile selected unrelated media guidance");
|
|
93
100
|
assert(selectedIds("convert scanned PDF EPUB and image sources to markdown with OCR").includes("source-ingestion"), "source ingestion prompt did not select source-ingestion");
|
|
94
101
|
assert(selectedIds("use json schema to fetch valid structured json for each chunk in parallel").includes("structured-json"), "structured JSON prompt did not select structured-json");
|
|
95
102
|
assert(selectedIds("write README API docs and a tutorial").includes("docs-knowledge"), "docs prompt did not select docs-knowledge");
|
|
@@ -23,7 +23,8 @@ tools:
|
|
|
23
23
|
---
|
|
24
24
|
# Data Analysis And ETL
|
|
25
25
|
|
|
26
|
-
Preserve raw
|
|
26
|
+
Preserve raw/source/input exports as immutable evidence unless the user explicitly authorizes an in-place source change. Start with `inspect_project`, then read project instructions plus the relevant existing analyzer, configuration, and tests before editing or running ad hoc calculations. Inspect schemas, row counts, missing values, duplicate rows, units, aliases, time zones, and suspicious outliers before writing conclusions.
|
|
27
27
|
|
|
28
|
-
Prefer
|
|
28
|
+
Prefer the project's existing reproducible analyzer over one-off chat math; repair or extend it when needed. Save cleaned data, plots, and reports at the exact declared output paths. Keep machine outputs separate from raw inputs and make the report explain methods, evidence, limitations, and the practical result.
|
|
29
29
|
|
|
30
|
+
After an edit or patch conflict, reread the exact target and error before choosing a different bounded repair. Never repeat an unchanged failing patch. Rerun the smallest relevant check, then the full analysis and project tests. Verify cleaned schemas and row counts, inspect plot metadata or pixels, remove only generated caches, and do not claim success until every requested artifact exists and can be regenerated.
|
package/src/agent-runner.js
CHANGED
|
@@ -7,6 +7,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
7
7
|
import { chromium } from "playwright";
|
|
8
8
|
import { createClient, createPlan, requestNextStep } from "./model-client.js";
|
|
9
9
|
import { SessionStore } from "./session-store.js";
|
|
10
|
+
import { assertIntegrationRunAgentInvocation } from "./integration-session-persistence.js";
|
|
10
11
|
import { captureSnapshot } from "./snapshot.js";
|
|
11
12
|
import { checkToolUse } from "./guardrails.js";
|
|
12
13
|
import { ensureDockerSandboxReady, runDockerSandboxCommand } from "./docker-sandbox.js";
|
|
@@ -60,6 +61,12 @@ import {
|
|
|
60
61
|
resolveLocalCodeRoute,
|
|
61
62
|
restoreLocalCodePolicy,
|
|
62
63
|
} from "./local-code-routing.js";
|
|
64
|
+
import {
|
|
65
|
+
activateLocalFailureRecovery,
|
|
66
|
+
applyLocalFailureRecovery,
|
|
67
|
+
decideLocalFailureRecovery,
|
|
68
|
+
localFailureRecoveryInstruction,
|
|
69
|
+
} from "./local-failure-recovery.js";
|
|
63
70
|
import { resolveRuntimeConfig } from "./config.js";
|
|
64
71
|
import {
|
|
65
72
|
captureSessionRuntime,
|
|
@@ -236,8 +243,9 @@ function withSelectedSkillReadOnlyRoots(config = {}, state = {}) {
|
|
|
236
243
|
function throwIfAborted(config) {
|
|
237
244
|
if (config.abortSignal?.aborted) {
|
|
238
245
|
const reason = config.abortSignal.reason;
|
|
239
|
-
|
|
240
|
-
error
|
|
246
|
+
if (reason instanceof Error) throw reason;
|
|
247
|
+
const error = new Error("Run interrupted by user.");
|
|
248
|
+
error.name = "AbortError";
|
|
241
249
|
throw error;
|
|
242
250
|
}
|
|
243
251
|
}
|
|
@@ -2524,9 +2532,10 @@ export function recordExactOutputProgress(state = {}, toolResult = {}, config =
|
|
|
2524
2532
|
}
|
|
2525
2533
|
|
|
2526
2534
|
export function nextStepRuntimeConfig(config = {}, state = {}) {
|
|
2535
|
+
const runtimeConfig = applyLocalFailureRecovery(config, state);
|
|
2527
2536
|
if (state.meta?.artifactProgress?.complete) {
|
|
2528
2537
|
return {
|
|
2529
|
-
...
|
|
2538
|
+
...runtimeConfig,
|
|
2530
2539
|
artifactValidationPhase: true,
|
|
2531
2540
|
convergenceOutputPhase: false,
|
|
2532
2541
|
artifactValidationNeedsRepair: state.meta.artifactProgress.needsRepair === true,
|
|
@@ -2540,10 +2549,10 @@ export function nextStepRuntimeConfig(config = {}, state = {}) {
|
|
|
2540
2549
|
};
|
|
2541
2550
|
}
|
|
2542
2551
|
const staticTotal = Number(state.meta?.toolLoop?.staticTotal || 0);
|
|
2543
|
-
if (staticTotal < STATIC_DISCOVERY_CONVERGENCE_LIMIT) return
|
|
2552
|
+
if (staticTotal < STATIC_DISCOVERY_CONVERGENCE_LIMIT) return runtimeConfig;
|
|
2544
2553
|
const requiresPerSourceChecks = state.meta?.scs?.taskContract?.requiresPerSourceChecks === true;
|
|
2545
2554
|
return {
|
|
2546
|
-
...
|
|
2555
|
+
...runtimeConfig,
|
|
2547
2556
|
convergenceOutputPhase: true,
|
|
2548
2557
|
convergenceAllowRunCommand: requiresPerSourceChecks,
|
|
2549
2558
|
};
|
|
@@ -4166,11 +4175,15 @@ async function recordToolContractViolation({ config, state, store, observers, va
|
|
|
4166
4175
|
total: totalViolationCount,
|
|
4167
4176
|
lastCode: validation.code || "TOOL_CALL_INVALID",
|
|
4168
4177
|
};
|
|
4178
|
+
const localRecovery = decideLocalFailureRecovery(config, state);
|
|
4179
|
+
const deferStopToLocalRecovery =
|
|
4180
|
+
violationCount >= 2 && localRecovery.active === true && localRecovery.activated === true;
|
|
4169
4181
|
const result = {
|
|
4170
4182
|
ok: false,
|
|
4171
4183
|
blocked: true,
|
|
4172
|
-
recoverable: violationCount < 2,
|
|
4173
|
-
stopRun: violationCount >= 2,
|
|
4184
|
+
recoverable: violationCount < 2 || deferStopToLocalRecovery,
|
|
4185
|
+
stopRun: violationCount >= 2 && !deferStopToLocalRecovery,
|
|
4186
|
+
localFailureRecoveryPending: deferStopToLocalRecovery,
|
|
4174
4187
|
violationCount,
|
|
4175
4188
|
reason: validation.reason || "The model returned an invalid tool call and it was not dispatched.",
|
|
4176
4189
|
category: "tool-contract-violation",
|
|
@@ -4524,6 +4537,7 @@ async function recordPreInferenceFailure({ error, config, state, store, observer
|
|
|
4524
4537
|
}
|
|
4525
4538
|
|
|
4526
4539
|
export async function runAgent(config) {
|
|
4540
|
+
assertIntegrationRunAgentInvocation(config);
|
|
4527
4541
|
const incomingConfig = config;
|
|
4528
4542
|
const sessionId = config.resume || config.sessionId || `web-agent-${crypto.randomUUID()}`;
|
|
4529
4543
|
const store = new SessionStore(config.sessionsDir, sessionId, {
|
|
@@ -5027,6 +5041,37 @@ export async function runAgent(config) {
|
|
|
5027
5041
|
const step = state.stepsCompleted + 1;
|
|
5028
5042
|
throwIfAborted(config);
|
|
5029
5043
|
await injectQueuedUserMessages(store, state, observers);
|
|
5044
|
+
const localFailureRecovery = activateLocalFailureRecovery(config, state);
|
|
5045
|
+
if (localFailureRecovery.active) {
|
|
5046
|
+
config = applyLocalFailureRecovery(config, state);
|
|
5047
|
+
}
|
|
5048
|
+
if (localFailureRecovery.activated) {
|
|
5049
|
+
state.provider = config.provider;
|
|
5050
|
+
state.model = config.model;
|
|
5051
|
+
state.meta.runtimeConfig = captureSessionRuntime(config, {
|
|
5052
|
+
revision: state.meta.runtimeConfig?.revision || 1,
|
|
5053
|
+
});
|
|
5054
|
+
const recoveryInstruction = localFailureRecoveryInstruction(localFailureRecovery);
|
|
5055
|
+
state.messages.push({ role: "user", content: recoveryInstruction });
|
|
5056
|
+
const detail = {
|
|
5057
|
+
step,
|
|
5058
|
+
fromModel: localFailureRecovery.fromModel,
|
|
5059
|
+
model: localFailureRecovery.model,
|
|
5060
|
+
failureCount: localFailureRecovery.failureCount,
|
|
5061
|
+
repeatedSignatureCount: localFailureRecovery.repeatedSignatureCount,
|
|
5062
|
+
contractViolationCount: localFailureRecovery.contractViolationCount || 0,
|
|
5063
|
+
failedTools: localFailureRecovery.failedTools || [],
|
|
5064
|
+
reason: localFailureRecovery.reason,
|
|
5065
|
+
};
|
|
5066
|
+
await store.appendEvent("provider.local_failure_recovery", detail);
|
|
5067
|
+
observers.event("provider.local_failure_recovery", detail);
|
|
5068
|
+
emitConsole(
|
|
5069
|
+
config,
|
|
5070
|
+
`Local route recovery: ${localFailureRecovery.fromModel} -> ${localFailureRecovery.model} after repeated tool failures.`,
|
|
5071
|
+
{ kind: "meta" }
|
|
5072
|
+
);
|
|
5073
|
+
await store.saveState(state);
|
|
5074
|
+
}
|
|
5030
5075
|
const snapshot = await buildSnapshot(browserState, store, step, config);
|
|
5031
5076
|
state.meta.lastUrl = snapshot.url || state.meta.lastUrl;
|
|
5032
5077
|
await saveBrowserState(browserState, store).catch(() => {});
|
|
@@ -59,7 +59,7 @@ const FORBIDDEN_KEYS = new Set([
|
|
|
59
59
|
"url",
|
|
60
60
|
]);
|
|
61
61
|
const ABSOLUTE_PATH_OR_SECRET_PATTERN =
|
|
62
|
-
/(?:^|[\s("'
|
|
62
|
+
/(?:^|[\s("'`<>\[{=])(?:file:\/\/\/[^\s"'`<>)\]}]+|\/(?!\/)[^\s"'`<>)\]}]+|[A-Za-z]:[\\/][^\s"'`<>)\]}]+|\\\\[^\\/\s"'`<>)\]}]+\\[^\s"'`<>)\]}]+|(?:api[_-]?key|token|secret|password)\s*[:=])/iu;
|
|
63
63
|
const EVENT_LEDGER_STORE_REQUIRED_KEYS = Object.freeze([
|
|
64
64
|
"owner",
|
|
65
65
|
"authority",
|
|
@@ -383,29 +383,103 @@ export function createIntegrationCoreEventProjector(options = {}) {
|
|
|
383
383
|
}
|
|
384
384
|
}
|
|
385
385
|
|
|
386
|
+
function toolStateKey(scope) {
|
|
387
|
+
return `${scope.principalId}\n${scope.browserSessionId}\n${scope.threadId}\n${scope.runId}`;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function validateLedgerHeadCursor(headInput, label) {
|
|
391
|
+
const seq = Number(headInput?.seq ?? 0);
|
|
392
|
+
const hash = String(headInput?.hash ?? "0".repeat(64));
|
|
393
|
+
if (
|
|
394
|
+
!Number.isSafeInteger(seq) ||
|
|
395
|
+
seq < 0 ||
|
|
396
|
+
!/^[a-f0-9]{64}$/u.test(hash) ||
|
|
397
|
+
((seq === 0) !== (hash === "0".repeat(64)))
|
|
398
|
+
) {
|
|
399
|
+
authorityFail("PUBLIC_EVENT_LEDGER_CORRUPT", `${label} cursor is invalid.`);
|
|
400
|
+
}
|
|
401
|
+
return Object.freeze({ seq, hash });
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function assertPublicEventEnvelopeHash(event, label) {
|
|
405
|
+
const checked = createPublicIntegrationEvent({
|
|
406
|
+
threadId: event.threadId,
|
|
407
|
+
runId: event.runId,
|
|
408
|
+
seq: event.seq,
|
|
409
|
+
type: event.type,
|
|
410
|
+
payload: event.payload,
|
|
411
|
+
createdAt: event.createdAt,
|
|
412
|
+
previousHash: event.previousHash,
|
|
413
|
+
});
|
|
414
|
+
if (
|
|
415
|
+
event.schemaVersion !== checked.schemaVersion ||
|
|
416
|
+
event.id !== checked.id ||
|
|
417
|
+
event.hash !== checked.hash ||
|
|
418
|
+
contractDigest(event) !== contractDigest(checked)
|
|
419
|
+
) {
|
|
420
|
+
authorityFail("PUBLIC_EVENT_LEDGER_CORRUPT", `${label} hash is invalid.`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function loadLedgerHeadAndLastEvent(ledger, scope) {
|
|
425
|
+
if (
|
|
426
|
+
!ledger ||
|
|
427
|
+
typeof ledger.loadHead !== "function" ||
|
|
428
|
+
typeof ledger.loadCursor !== "function" ||
|
|
429
|
+
typeof ledger.loadEventsAfter !== "function"
|
|
430
|
+
) {
|
|
431
|
+
authorityFail("PUBLIC_EVENT_LEDGER_UNAVAILABLE", "Public event ledger head is unavailable.");
|
|
432
|
+
}
|
|
433
|
+
const head = validateLedgerHeadCursor(await ledger.loadHead(), "Public event ledger head");
|
|
434
|
+
if (head.seq === 0) return Object.freeze({ head, lastEvent: null });
|
|
435
|
+
const cursor = validateLedgerHeadCursor(await ledger.loadCursor(head.seq), "Public event ledger head event");
|
|
436
|
+
if (cursor.seq !== head.seq || cursor.hash !== head.hash) {
|
|
437
|
+
authorityFail("PUBLIC_EVENT_LEDGER_CORRUPT", "Public event ledger head cursor does not match the last event.");
|
|
438
|
+
}
|
|
439
|
+
const events = await ledger.loadEventsAfter(head.seq - 1);
|
|
440
|
+
if (!Array.isArray(events) || events.length !== 1) {
|
|
441
|
+
authorityFail("PUBLIC_EVENT_LEDGER_CORRUPT", "Public event ledger did not return the exact last event.");
|
|
442
|
+
}
|
|
443
|
+
const [lastEvent] = events;
|
|
444
|
+
assertExactPublicEventView(lastEvent, "Public event ledger last event");
|
|
445
|
+
if (
|
|
446
|
+
lastEvent.threadId !== scope.threadId ||
|
|
447
|
+
lastEvent.runId !== scope.runId ||
|
|
448
|
+
lastEvent.seq !== head.seq ||
|
|
449
|
+
lastEvent.hash !== head.hash
|
|
450
|
+
) {
|
|
451
|
+
authorityFail("PUBLIC_EVENT_LEDGER_CORRUPT", "Public event ledger last event does not match the requested head.");
|
|
452
|
+
}
|
|
453
|
+
if (
|
|
454
|
+
(lastEvent.principalId !== undefined && lastEvent.principalId !== scope.principalId) ||
|
|
455
|
+
(lastEvent.browserSessionId !== undefined && lastEvent.browserSessionId !== scope.browserSessionId)
|
|
456
|
+
) {
|
|
457
|
+
authorityFail("PUBLIC_EVENT_LEDGER_CORRUPT", "Public event ledger last event substituted scope.");
|
|
458
|
+
}
|
|
459
|
+
assertPublicEventEnvelopeHash(lastEvent, "Public event ledger last event");
|
|
460
|
+
return Object.freeze({ head, lastEvent });
|
|
461
|
+
}
|
|
462
|
+
|
|
386
463
|
async function appendProjectedEvent(projectedInput, scopeInput) {
|
|
387
464
|
const scope = assertScope(scopeInput);
|
|
388
465
|
const projected = projectedInput;
|
|
389
466
|
if (!projected) return null;
|
|
467
|
+
const projectedIsTerminal = TERMINAL_TYPES.has(projected.type);
|
|
468
|
+
if ((projected.terminal === true) !== projectedIsTerminal) {
|
|
469
|
+
authorityFail("UNSUPPORTED_CORE_EVENT", "Projected event terminal flag does not match its public type.", { status: 400 });
|
|
470
|
+
}
|
|
471
|
+
const stateKey = toolStateKey(scope);
|
|
390
472
|
const ledger =
|
|
391
473
|
typeof eventLedgerStore.ledgerForRun === "function"
|
|
392
474
|
? eventLedgerStore.ledgerForRun(scope)
|
|
393
475
|
: null;
|
|
394
476
|
assertLedgerScope(ledger || {}, scope);
|
|
395
|
-
const head =
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
: { seq: 0, hash: "0".repeat(64) };
|
|
399
|
-
const previousSeq = Number(head?.seq || 0);
|
|
400
|
-
const previousHash = String(head?.hash || "0".repeat(64));
|
|
401
|
-
if (
|
|
402
|
-
!Number.isSafeInteger(previousSeq) ||
|
|
403
|
-
previousSeq < 0 ||
|
|
404
|
-
!/^[a-f0-9]{64}$/u.test(previousHash) ||
|
|
405
|
-
((previousSeq === 0) !== (previousHash === "0".repeat(64)))
|
|
406
|
-
) {
|
|
407
|
-
authorityFail("PUBLIC_EVENT_LEDGER_CORRUPT", "Public event ledger head cursor is invalid.");
|
|
477
|
+
const { head, lastEvent } = await loadLedgerHeadAndLastEvent(ledger, scope);
|
|
478
|
+
if (lastEvent && TERMINAL_TYPES.has(lastEvent.type)) {
|
|
479
|
+
authorityFail("PUBLIC_EVENT_LEDGER_CORRUPT", "Public event ledger terminal finality blocks later run events.");
|
|
408
480
|
}
|
|
481
|
+
const previousSeq = head.seq;
|
|
482
|
+
const previousHash = head.hash;
|
|
409
483
|
if (
|
|
410
484
|
projected.expectedPreviousSeq !== undefined &&
|
|
411
485
|
(projected.expectedPreviousSeq !== previousSeq || projected.expectedPreviousHash !== previousHash)
|
|
@@ -418,11 +492,23 @@ export function createIntegrationCoreEventProjector(options = {}) {
|
|
|
418
492
|
createdAt: projected.createdAt,
|
|
419
493
|
});
|
|
420
494
|
assertExactPublicEventView(event, "Public event ledger append");
|
|
495
|
+
const checked = createPublicIntegrationEvent({
|
|
496
|
+
threadId: event.threadId,
|
|
497
|
+
runId: event.runId,
|
|
498
|
+
seq: event.seq,
|
|
499
|
+
type: event.type,
|
|
500
|
+
payload: event.payload,
|
|
501
|
+
createdAt: event.createdAt,
|
|
502
|
+
previousHash: event.previousHash,
|
|
503
|
+
});
|
|
421
504
|
if (
|
|
422
505
|
event.threadId !== scope.threadId ||
|
|
423
506
|
event.runId !== scope.runId ||
|
|
507
|
+
event.schemaVersion !== checked.schemaVersion ||
|
|
508
|
+
event.id !== checked.id ||
|
|
424
509
|
event.seq !== previousSeq + 1 ||
|
|
425
510
|
event.previousHash !== previousHash ||
|
|
511
|
+
event.hash !== checked.hash ||
|
|
426
512
|
event.type !== projected.type ||
|
|
427
513
|
event.createdAt !== projected.createdAt ||
|
|
428
514
|
contractDigest(event.payload) !== contractDigest(projected.payload)
|
|
@@ -435,16 +521,7 @@ export function createIntegrationCoreEventProjector(options = {}) {
|
|
|
435
521
|
) {
|
|
436
522
|
authorityFail("PUBLIC_EVENT_LEDGER_CORRUPT", "Public event ledger append substituted the requested scope.");
|
|
437
523
|
}
|
|
438
|
-
|
|
439
|
-
threadId: event.threadId,
|
|
440
|
-
runId: event.runId,
|
|
441
|
-
seq: event.seq,
|
|
442
|
-
type: event.type,
|
|
443
|
-
payload: event.payload,
|
|
444
|
-
createdAt: event.createdAt,
|
|
445
|
-
previousHash: event.previousHash,
|
|
446
|
-
});
|
|
447
|
-
if (checked.hash !== event.hash) {
|
|
524
|
+
if (checked.hash !== event.hash || contractDigest(checked) !== contractDigest(event)) {
|
|
448
525
|
authorityFail("PUBLIC_EVENT_LEDGER_CORRUPT", "Appended public event hash is invalid.");
|
|
449
526
|
}
|
|
450
527
|
if (ledger && typeof ledger.loadHead === "function") {
|
|
@@ -453,36 +530,39 @@ export function createIntegrationCoreEventProjector(options = {}) {
|
|
|
453
530
|
authorityFail("PUBLIC_EVENT_LEDGER_CORRUPT", "Public event ledger head did not advance to the appended event.");
|
|
454
531
|
}
|
|
455
532
|
}
|
|
456
|
-
if (projected.terminal)
|
|
533
|
+
if (projected.terminal) {
|
|
534
|
+
toolStateByRun.delete(stateKey);
|
|
535
|
+
}
|
|
457
536
|
return Object.freeze({ ...checked, terminal: projected.terminal });
|
|
458
537
|
}
|
|
459
538
|
|
|
460
539
|
async function appendCoreEvent(type, data, scopeInput) {
|
|
461
540
|
const scope = assertScope(scopeInput);
|
|
462
541
|
const rawType = String(type || "");
|
|
542
|
+
const stateKey = toolStateKey(scope);
|
|
463
543
|
const needsToolState = TOOL_EVENT_TYPES.has(rawType);
|
|
464
|
-
const hadState = toolStateByRun.has(
|
|
465
|
-
let toolState = needsToolState ? toolStateByRun.get(
|
|
544
|
+
const hadState = toolStateByRun.has(stateKey);
|
|
545
|
+
let toolState = needsToolState ? toolStateByRun.get(stateKey) : undefined;
|
|
466
546
|
if (needsToolState && !toolState) {
|
|
467
547
|
toolState = { nextOrdinal: 0, activeByTool: new Map() };
|
|
468
|
-
toolStateByRun.set(
|
|
548
|
+
toolStateByRun.set(stateKey, toolState);
|
|
469
549
|
}
|
|
470
550
|
const snapshot = needsToolState ? snapshotToolState(toolState) : null;
|
|
471
551
|
let projected = null;
|
|
472
552
|
try {
|
|
473
553
|
projected = projectCoreEvent(type, data, { now, runId: scope.runId, ...(needsToolState ? { toolState } : {}) });
|
|
474
554
|
} catch (error) {
|
|
475
|
-
if (needsToolState) restoreToolState(
|
|
555
|
+
if (needsToolState) restoreToolState(stateKey, hadState, snapshot);
|
|
476
556
|
throw error;
|
|
477
557
|
}
|
|
478
558
|
if (!projected) {
|
|
479
|
-
if (needsToolState) restoreToolState(
|
|
559
|
+
if (needsToolState) restoreToolState(stateKey, hadState, snapshot);
|
|
480
560
|
return null;
|
|
481
561
|
}
|
|
482
562
|
try {
|
|
483
563
|
return await appendProjectedEvent(projected, scope);
|
|
484
564
|
} catch (error) {
|
|
485
|
-
if (needsToolState) restoreToolState(
|
|
565
|
+
if (needsToolState) restoreToolState(stateKey, hadState, snapshot);
|
|
486
566
|
throw error;
|
|
487
567
|
}
|
|
488
568
|
}
|
|
@@ -496,8 +576,12 @@ export function createIntegrationCoreEventProjector(options = {}) {
|
|
|
496
576
|
owner: "aginti",
|
|
497
577
|
authority: "aginti",
|
|
498
578
|
appendCoreEvent,
|
|
499
|
-
appendProjectedEvent,
|
|
500
579
|
appendAuthorityTerminalEvent,
|
|
580
|
+
clearRun(scopeInput, options = {}) {
|
|
581
|
+
const scope = assertScope(scopeInput);
|
|
582
|
+
const stateKey = toolStateKey(scope);
|
|
583
|
+
toolStateByRun.delete(stateKey);
|
|
584
|
+
},
|
|
501
585
|
projectCoreEvent(type, data, scope = {}) {
|
|
502
586
|
return projectCoreEvent(type, data, { now, runId: scope.runId || "" });
|
|
503
587
|
},
|