specrails-core 6.0.1 → 6.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/README.md +1 -1
- package/dist/agent-runtime/capabilities.js +3 -1
- package/dist/agent-runtime/cli-executor.d.ts +3 -0
- package/dist/agent-runtime/cli-executor.js +50 -13
- package/dist/agent-runtime/cli-process.js +6 -2
- package/dist/agent-runtime/cli.d.ts +15 -0
- package/dist/agent-runtime/cli.js +43 -9
- package/dist/agent-runtime/compact/chat-client.js +2 -1
- package/dist/agent-runtime/config.d.ts +4 -1
- package/dist/agent-runtime/config.js +41 -3
- package/dist/agent-runtime/core-host.d.ts +9 -3
- package/dist/agent-runtime/core-host.js +21 -18
- package/dist/agent-runtime/efficiency-summary.d.ts +4 -1
- package/dist/agent-runtime/efficiency-summary.js +30 -27
- package/dist/agent-runtime/efficiency-types.d.ts +4 -4
- package/dist/agent-runtime/efficiency.d.ts +1 -1
- package/dist/agent-runtime/efficiency.js +7 -6
- package/dist/agent-runtime/engine/__fixtures__/implementation-component.json +90 -0
- package/dist/agent-runtime/engine/__fixtures__/implementation-fixture.d.ts +11 -0
- package/dist/agent-runtime/engine/__fixtures__/implementation-fixture.js +60 -0
- package/dist/agent-runtime/engine/__fixtures__/implementation.json +43 -0
- package/dist/agent-runtime/engine/budget.d.ts +9 -0
- package/dist/agent-runtime/engine/budget.js +33 -0
- package/dist/agent-runtime/engine/canonical-json.d.ts +9 -0
- package/dist/agent-runtime/engine/canonical-json.js +162 -0
- package/dist/agent-runtime/engine/checkpoint/database.d.ts +67 -0
- package/dist/agent-runtime/engine/checkpoint/database.js +304 -0
- package/dist/agent-runtime/engine/checkpoint/lease.d.ts +17 -0
- package/dist/agent-runtime/engine/checkpoint/lease.js +82 -0
- package/dist/agent-runtime/engine/checkpoint/ledger.d.ts +133 -0
- package/dist/agent-runtime/engine/checkpoint/ledger.js +510 -0
- package/dist/agent-runtime/engine/checkpoint/saver.d.ts +25 -0
- package/dist/agent-runtime/engine/checkpoint/saver.js +146 -0
- package/dist/agent-runtime/engine/cli.d.ts +10 -0
- package/dist/agent-runtime/engine/cli.js +172 -0
- package/dist/agent-runtime/engine/compiler.d.ts +23 -0
- package/dist/agent-runtime/engine/compiler.js +261 -0
- package/dist/agent-runtime/engine/composition.d.ts +16 -0
- package/dist/agent-runtime/engine/composition.js +28 -0
- package/dist/agent-runtime/engine/concurrency.d.ts +14 -0
- package/dist/agent-runtime/engine/concurrency.js +55 -0
- package/dist/agent-runtime/engine/contracts.d.ts +273 -0
- package/dist/agent-runtime/engine/contracts.js +11 -0
- package/dist/agent-runtime/engine/definition-schema.d.ts +250 -0
- package/dist/agent-runtime/engine/definition-schema.js +313 -0
- package/dist/agent-runtime/engine/definition-types.d.ts +84 -0
- package/dist/agent-runtime/engine/definition-types.js +3 -0
- package/dist/agent-runtime/engine/definition-validator.d.ts +7 -0
- package/dist/agent-runtime/engine/definition-validator.js +187 -0
- package/dist/agent-runtime/engine/efficiency-summary.d.ts +62 -0
- package/dist/agent-runtime/engine/efficiency-summary.js +31 -0
- package/dist/agent-runtime/engine/events.d.ts +19 -0
- package/dist/agent-runtime/engine/events.js +88 -0
- package/dist/agent-runtime/engine/execution.d.ts +29 -0
- package/dist/agent-runtime/engine/execution.js +121 -0
- package/dist/agent-runtime/engine/expressions.d.ts +11 -0
- package/dist/agent-runtime/engine/expressions.js +172 -0
- package/dist/agent-runtime/engine/fork.d.ts +28 -0
- package/dist/agent-runtime/engine/fork.js +211 -0
- package/dist/agent-runtime/engine/graph-description.d.ts +48 -0
- package/dist/agent-runtime/engine/graph-description.js +16 -0
- package/dist/agent-runtime/engine/implementation-compiler.d.ts +214 -0
- package/dist/agent-runtime/engine/implementation-compiler.js +91 -0
- package/dist/agent-runtime/engine/index.d.ts +14 -0
- package/dist/agent-runtime/engine/index.js +14 -0
- package/dist/agent-runtime/engine/invocation-context.d.ts +11 -0
- package/dist/agent-runtime/engine/invocation-context.js +128 -0
- package/dist/agent-runtime/engine/otel.d.ts +15 -0
- package/dist/agent-runtime/engine/otel.js +132 -0
- package/dist/agent-runtime/engine/output.d.ts +4 -0
- package/dist/agent-runtime/engine/output.js +73 -0
- package/dist/agent-runtime/engine/piece-dependencies.d.ts +50 -0
- package/dist/agent-runtime/engine/piece-dependencies.js +193 -0
- package/dist/agent-runtime/engine/piece-registry.d.ts +19 -0
- package/dist/agent-runtime/engine/piece-registry.js +62 -0
- package/dist/agent-runtime/engine/pieces/assign.d.ts +4 -0
- package/dist/agent-runtime/engine/pieces/assign.js +35 -0
- package/dist/agent-runtime/engine/pieces/control.d.ts +5 -0
- package/dist/agent-runtime/engine/pieces/control.js +49 -0
- package/dist/agent-runtime/engine/pieces/decider.d.ts +5 -0
- package/dist/agent-runtime/engine/pieces/decider.js +70 -0
- package/dist/agent-runtime/engine/pieces/implementation-binding.d.ts +12 -0
- package/dist/agent-runtime/engine/pieces/implementation-binding.js +40 -0
- package/dist/agent-runtime/engine/pieces/implementation-journal.d.ts +25 -0
- package/dist/agent-runtime/engine/pieces/implementation-journal.js +192 -0
- package/dist/agent-runtime/engine/pieces/implementation.d.ts +23 -0
- package/dist/agent-runtime/engine/pieces/implementation.js +176 -0
- package/dist/agent-runtime/engine/pieces/index.d.ts +7 -0
- package/dist/agent-runtime/engine/pieces/index.js +22 -0
- package/dist/agent-runtime/engine/pieces/openspec.d.ts +3 -0
- package/dist/agent-runtime/engine/pieces/openspec.js +69 -0
- package/dist/agent-runtime/engine/pieces/ports.d.ts +48 -0
- package/dist/agent-runtime/engine/pieces/ports.js +1 -0
- package/dist/agent-runtime/engine/pieces/project-memory.d.ts +3 -0
- package/dist/agent-runtime/engine/pieces/project-memory.js +13 -0
- package/dist/agent-runtime/engine/pieces/prompt.d.ts +5 -0
- package/dist/agent-runtime/engine/pieces/prompt.js +130 -0
- package/dist/agent-runtime/engine/pieces/role-turn.d.ts +7 -0
- package/dist/agent-runtime/engine/pieces/role-turn.js +73 -0
- package/dist/agent-runtime/engine/pieces/shared.d.ts +67 -0
- package/dist/agent-runtime/engine/pieces/shared.js +40 -0
- package/dist/agent-runtime/engine/pieces/shell.d.ts +6 -0
- package/dist/agent-runtime/engine/pieces/shell.js +47 -0
- package/dist/agent-runtime/engine/pieces/verify.d.ts +7 -0
- package/dist/agent-runtime/engine/pieces/verify.js +52 -0
- package/dist/agent-runtime/engine/preflight.d.ts +27 -0
- package/dist/agent-runtime/engine/preflight.js +54 -0
- package/dist/agent-runtime/engine/regex.d.ts +3 -0
- package/dist/agent-runtime/engine/regex.js +19 -0
- package/dist/agent-runtime/engine/run-status.d.ts +563 -0
- package/dist/agent-runtime/engine/run-status.js +61 -0
- package/dist/agent-runtime/engine/runs.d.ts +605 -0
- package/dist/agent-runtime/engine/runs.js +247 -0
- package/dist/agent-runtime/engine/state.d.ts +64 -0
- package/dist/agent-runtime/engine/state.js +98 -0
- package/dist/agent-runtime/engine/steering/inbox.d.ts +54 -0
- package/dist/agent-runtime/engine/steering/inbox.js +113 -0
- package/dist/agent-runtime/engine/storage/fork-allocation.d.ts +21 -0
- package/dist/agent-runtime/engine/storage/fork-allocation.js +143 -0
- package/dist/agent-runtime/engine/storage/private-path.d.ts +11 -0
- package/dist/agent-runtime/engine/storage/private-path.js +74 -0
- package/dist/agent-runtime/engine/store/sqlite-store.d.ts +26 -0
- package/dist/agent-runtime/engine/store/sqlite-store.js +227 -0
- package/dist/agent-runtime/evaluation-corpus.d.ts +125 -0
- package/dist/agent-runtime/evaluation-corpus.js +4 -0
- package/dist/agent-runtime/evaluation.d.ts +9 -3
- package/dist/agent-runtime/evaluation.js +50 -8
- package/dist/agent-runtime/executor-types.d.ts +53 -3
- package/dist/agent-runtime/executor-types.js +35 -2
- package/dist/agent-runtime/executors.js +2 -2
- package/dist/agent-runtime/graph/artifacts.d.ts +7 -0
- package/dist/agent-runtime/graph/artifacts.js +82 -9
- package/dist/agent-runtime/graph/nodes.d.ts +2 -0
- package/dist/agent-runtime/graph/nodes.js +2 -2
- package/dist/agent-runtime/graph/roles.d.ts +6 -0
- package/dist/agent-runtime/graph/roles.js +23 -17
- package/dist/agent-runtime/kimi-acp.js +6 -6
- package/dist/agent-runtime/openai-executor.js +17 -12
- package/dist/agent-runtime/openspec.d.ts +17 -2
- package/dist/agent-runtime/openspec.js +54 -20
- package/dist/agent-runtime/prompts.d.ts +4 -3
- package/dist/agent-runtime/prompts.js +26 -5
- package/dist/agent-runtime/recovery.d.ts +1 -1
- package/dist/agent-runtime/repository-context.js +1 -1
- package/dist/agent-runtime/role-routing.js +4 -2
- package/dist/agent-runtime/role-state.d.ts +5 -0
- package/dist/agent-runtime/verification-plan.d.ts +2 -0
- package/dist/agent-runtime/verification-plan.js +10 -0
- package/dist/agent-runtime/workflow-types.d.ts +2 -0
- package/dist/agent-runtime/workflow.d.ts +3 -1
- package/dist/agent-runtime/workflow.js +7 -3
- package/dist/agent-runtime/workspace-tools.d.ts +2 -2
- package/dist/agent-runtime/workspace-tools.js +6 -6
- package/dist/installer/cli.js +5 -0
- package/dist/installer/phases/scaffold.d.ts +2 -2
- package/dist/installer/phases/scaffold.js +3 -7
- package/dist/pipeline/pipeline-state.d.ts +38 -4
- package/dist/pipeline/pipeline-state.js +94 -21
- package/integration-contract.json +83 -11
- package/package.json +12 -5
- package/schemas/agent-runtime.schema.json +93 -1
- package/schemas/workflow-definition.schema.json +312 -0
- package/templates/kimi/specrails/run-skill.mjs +15 -1
- package/templates/commands/specrails/batch-implement.md +0 -32
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { efficiencySummary } from '../efficiency-summary.js';
|
|
2
|
+
/** Derive diagnostics from committed records; status never inspects or mutates a journal. */
|
|
3
|
+
export function definitionEfficiencySummary(ledger, state) {
|
|
4
|
+
const row = ledger.run(), request = JSON.parse(String(row.request_json));
|
|
5
|
+
const candidate = row.candidate_json ? JSON.parse(String(row.candidate_json)) : undefined;
|
|
6
|
+
const verified = row.verified_json ? JSON.parse(String(row.verified_json)) : undefined;
|
|
7
|
+
const completion = row.completion_json ? JSON.parse(String(row.completion_json)) : undefined;
|
|
8
|
+
const receipts = ledger.db.sqlite.prepare('SELECT receipt_json FROM receipts WHERE run_id=? ORDER BY created_at').all(ledger.runId)
|
|
9
|
+
.map(value => JSON.parse(String(value.receipt_json)));
|
|
10
|
+
const current = receipts.find(receipt => receipt.id === verified?.receiptId);
|
|
11
|
+
const evidence = current?.evidence;
|
|
12
|
+
const checks = new Map();
|
|
13
|
+
for (const entry of ledger.db.sqlite.prepare("SELECT value_json FROM piece_state WHERE run_id=? AND key LIKE 'evidence:check:%'").all(ledger.runId)) {
|
|
14
|
+
const value = JSON.parse(String(entry.value_json));
|
|
15
|
+
if (typeof value.evidenceId === 'string')
|
|
16
|
+
checks.set(value.evidenceId, { ...value, id: value.evidenceId });
|
|
17
|
+
}
|
|
18
|
+
for (const receipt of receipts) {
|
|
19
|
+
const source = receipt.evidence;
|
|
20
|
+
for (const command of source.commands ?? [])
|
|
21
|
+
if (typeof command.evidenceId === 'string' && !checks.has(command.evidenceId)) {
|
|
22
|
+
checks.set(command.evidenceId, { ...command, id: command.evidenceId, planHash: source.planHash, candidateHash: receipt.candidateHash });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const native = [...state.history].reverse().map(attempt => attempt.output?.completion).find(value => value?.archive !== undefined);
|
|
26
|
+
const projected = { implementation: completion?.ok ? 'complete' : 'incomplete',
|
|
27
|
+
validation: completion?.verified ? 'verified' : completion && !completion.ok ? 'blocked' : 'pending',
|
|
28
|
+
archive: native?.archive ?? 'pending', delivery: 'pending-host', reasons: completion?.reasons ?? [] };
|
|
29
|
+
return { ...efficiencySummary(state, request.context, request.config, projected, candidate ? { candidateHash: candidate.hash, ...(evidence?.planHash ? { planHash: evidence.planHash } : {}),
|
|
30
|
+
...(evidence ? { verification: { receipt: { commands: evidence.commands ?? [], notRunEvidenceIds: evidence.notRunEvidenceIds } } } : {}) } : undefined, { items: [...checks.values()], available: true }), reservations: ledger.reservationStatus() };
|
|
31
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type DurableEngineEvent, type JsonObject, type JsonValue, type TransientEngineEvent } from './contracts.js';
|
|
2
|
+
export declare const MAX_EVENT_CHARACTERS = 1000000;
|
|
3
|
+
export declare const spanIdFor: (runId: string, sequence: number) => string;
|
|
4
|
+
export declare const traceIdFor: (runId: string) => string;
|
|
5
|
+
/** Bound log strings without dropping lifecycle identity or inventing usage. */
|
|
6
|
+
export declare function boundedJsonLine(value: JsonValue, maximum?: number): string;
|
|
7
|
+
export declare function projectDurableEvent(event: DurableEngineEvent): JsonObject;
|
|
8
|
+
/** The ledger supplies lifecycle truth. Provider progress is explicitly transient. */
|
|
9
|
+
export declare class EngineEventStream {
|
|
10
|
+
private readonly read;
|
|
11
|
+
private readonly emit;
|
|
12
|
+
private readonly observerError;
|
|
13
|
+
private readonly onDurable?;
|
|
14
|
+
private cursor;
|
|
15
|
+
constructor(read: (after: number) => DurableEngineEvent[], emit: (value: JsonObject) => void, after?: number, observerError?: (error: unknown) => void, onDurable?: ((event: DurableEngineEvent) => void) | undefined);
|
|
16
|
+
send(value: JsonObject): void;
|
|
17
|
+
flush(): void;
|
|
18
|
+
progress(event: TransientEngineEvent): void;
|
|
19
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { EngineError } from './contracts.js';
|
|
3
|
+
export const MAX_EVENT_CHARACTERS = 1_000_000;
|
|
4
|
+
export const spanIdFor = (runId, sequence) => createHash('sha256').update(`${runId}:${sequence}`).digest('hex').slice(0, 16);
|
|
5
|
+
export const traceIdFor = (runId) => createHash('sha256').update(runId).digest('hex').slice(0, 32);
|
|
6
|
+
/** Bound log strings without dropping lifecycle identity or inventing usage. */
|
|
7
|
+
export function boundedJsonLine(value, maximum = MAX_EVENT_CHARACTERS) {
|
|
8
|
+
const trim = (entry, depth = 0) => {
|
|
9
|
+
if (depth > 64)
|
|
10
|
+
throw new EngineError('output_limit', 'Event nesting exceeds 64 levels');
|
|
11
|
+
if (typeof entry === 'string')
|
|
12
|
+
return entry.length > 64_000 ? entry.slice(0, 63_980) + '\n[output truncated]' : entry;
|
|
13
|
+
if (Array.isArray(entry))
|
|
14
|
+
return entry.map(item => trim(item, depth + 1));
|
|
15
|
+
if (entry && typeof entry === 'object')
|
|
16
|
+
return Object.fromEntries(Object.entries(entry).map(([key, item]) => [key, trim(item, depth + 1)]));
|
|
17
|
+
return entry;
|
|
18
|
+
};
|
|
19
|
+
const line = JSON.stringify(trim(value));
|
|
20
|
+
if (line.length + 1 > maximum)
|
|
21
|
+
throw new EngineError('output_limit', 'Event exceeds the JSONL transport limit');
|
|
22
|
+
return line + '\n';
|
|
23
|
+
}
|
|
24
|
+
export function projectDurableEvent(event) {
|
|
25
|
+
const payload = event.payload && typeof event.payload === 'object' && !Array.isArray(event.payload) ? event.payload : {};
|
|
26
|
+
if (event.type === 'efficiency_updated')
|
|
27
|
+
return {
|
|
28
|
+
type: 'runtime-efficiency-event', schemaVersion: 1, eventId: `${event.runId}:${event.sequence}`,
|
|
29
|
+
sequence: event.sequence, runId: event.runId, timestamp: event.timestamp, ...(event.attemptId ? { attemptId: event.attemptId } : {}),
|
|
30
|
+
...(event.nodePath ? { nodePath: event.nodePath } : {}), ...(event.branchId ? { branch: event.branchId } : {}),
|
|
31
|
+
kind: 'role-context', payload: event.payload,
|
|
32
|
+
};
|
|
33
|
+
return { type: 'workflow-event', event: {
|
|
34
|
+
...payload, id: `${event.runId}:${event.sequence}`, sequence: event.sequence, runId: event.runId, traceId: traceIdFor(event.runId), spanId: spanIdFor(event.runId, event.sequence),
|
|
35
|
+
type: event.type, timestamp: event.timestamp,
|
|
36
|
+
...(event.nodePath ? { nodePath: event.nodePath, stepId: event.nodePath } : {}),
|
|
37
|
+
...(event.scopeId ? { scopeId: event.scopeId } : {}), ...(event.branchId ? { branch: event.branchId } : {}),
|
|
38
|
+
...(event.visit === undefined ? {} : { visit: event.visit }), ...(event.attempt === undefined ? {} : { attempt: event.attempt }),
|
|
39
|
+
...(event.attemptId ? { attemptId: event.attemptId } : {}),
|
|
40
|
+
} };
|
|
41
|
+
}
|
|
42
|
+
/** The ledger supplies lifecycle truth. Provider progress is explicitly transient. */
|
|
43
|
+
export class EngineEventStream {
|
|
44
|
+
read;
|
|
45
|
+
emit;
|
|
46
|
+
observerError;
|
|
47
|
+
onDurable;
|
|
48
|
+
cursor;
|
|
49
|
+
constructor(read, emit, after = 0, observerError = () => { }, onDurable) {
|
|
50
|
+
this.read = read;
|
|
51
|
+
this.emit = emit;
|
|
52
|
+
this.observerError = observerError;
|
|
53
|
+
this.onDurable = onDurable;
|
|
54
|
+
this.cursor = after;
|
|
55
|
+
}
|
|
56
|
+
send(value) {
|
|
57
|
+
try {
|
|
58
|
+
this.emit(JSON.parse(boundedJsonLine(value)));
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
try {
|
|
62
|
+
this.observerError(error);
|
|
63
|
+
}
|
|
64
|
+
catch { /* Observers cannot replay committed effects. */ }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
flush() {
|
|
68
|
+
for (const event of this.read(this.cursor)) {
|
|
69
|
+
if (event.sequence <= this.cursor)
|
|
70
|
+
throw new EngineError('event_sequence_conflict', 'Durable events must advance monotonically');
|
|
71
|
+
try {
|
|
72
|
+
this.onDurable?.(event);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
try {
|
|
76
|
+
this.observerError(error);
|
|
77
|
+
}
|
|
78
|
+
catch { /* Optional telemetry cannot affect committed work. */ }
|
|
79
|
+
}
|
|
80
|
+
this.send(projectDurableEvent(event));
|
|
81
|
+
this.cursor = event.sequence;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
progress(event) {
|
|
85
|
+
const payload = event.payload && typeof event.payload === 'object' && !Array.isArray(event.payload) ? event.payload : { payload: event.payload };
|
|
86
|
+
this.send({ ...payload, type: event.type });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type AttemptFrame, type DurableEngineEvent, type EngineEffect, type NodeAdmission, type NodeExecutionPort, type PieceResult, type TerminalCommit, type TransientEngineEvent } from './contracts.js';
|
|
2
|
+
import type { RunLedger } from './checkpoint/ledger.js';
|
|
3
|
+
import type { EngineEventStream } from './events.js';
|
|
4
|
+
/** Owns effect/concurrency windows across execution AND durable terminal commit. */
|
|
5
|
+
export declare class DefinitionExecution implements NodeExecutionPort {
|
|
6
|
+
readonly ledger: RunLedger;
|
|
7
|
+
private readonly stream;
|
|
8
|
+
private readonly signal;
|
|
9
|
+
private readonly finalize?;
|
|
10
|
+
private readonly admissions;
|
|
11
|
+
private readonly held;
|
|
12
|
+
private readonly effects;
|
|
13
|
+
private readonly agents;
|
|
14
|
+
private readonly groups;
|
|
15
|
+
constructor(ledger: RunLedger, stream: EngineEventStream, signal: AbortSignal, concurrency?: number, finalize?: ((frame: AttemptFrame, result: PieceResult, effect: EngineEffect) => PieceResult) | undefined);
|
|
16
|
+
enter(input: NodeAdmission): Promise<AttemptFrame>;
|
|
17
|
+
execute(frame: AttemptFrame, effect: EngineEffect, operation: (signal: AbortSignal) => Promise<PieceResult>): Promise<PieceResult>;
|
|
18
|
+
terminal(frame: AttemptFrame, result: PieceResult): TerminalCommit;
|
|
19
|
+
interrupted(frame: AttemptFrame, error: unknown): Promise<void>;
|
|
20
|
+
failed(frame: AttemptFrame, error: unknown, options: {
|
|
21
|
+
retry: boolean;
|
|
22
|
+
}): Promise<{
|
|
23
|
+
retryable: boolean;
|
|
24
|
+
}>;
|
|
25
|
+
committed(events: DurableEngineEvent[]): void;
|
|
26
|
+
progress(event: TransientEngineEvent): void;
|
|
27
|
+
close(): void;
|
|
28
|
+
private release;
|
|
29
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { isGraphInterrupt } from '@langchain/langgraph';
|
|
2
|
+
import { AgentExecutionError } from '../executor-types.js';
|
|
3
|
+
import { EngineError } from './contracts.js';
|
|
4
|
+
import { ConcurrencyGate, RepositoryEffectGate } from './concurrency.js';
|
|
5
|
+
const COORDINATORS = new Set(['component', 'map', 'join', 'implementation']);
|
|
6
|
+
const UNCERTAIN_EFFECT = new Set(['aborted', 'timeout', 'idle_timeout', 'lease_lost']);
|
|
7
|
+
const errorDetails = (error) => ({
|
|
8
|
+
code: error instanceof EngineError || error instanceof AgentExecutionError ? error.code : 'piece_failed',
|
|
9
|
+
message: error instanceof Error ? error.message : String(error),
|
|
10
|
+
});
|
|
11
|
+
/** Owns effect/concurrency windows across execution AND durable terminal commit. */
|
|
12
|
+
export class DefinitionExecution {
|
|
13
|
+
ledger;
|
|
14
|
+
stream;
|
|
15
|
+
signal;
|
|
16
|
+
finalize;
|
|
17
|
+
admissions = new Map();
|
|
18
|
+
held = new Map();
|
|
19
|
+
effects = new RepositoryEffectGate();
|
|
20
|
+
agents;
|
|
21
|
+
groups = new Map();
|
|
22
|
+
constructor(ledger, stream, signal, concurrency = 1, finalize) {
|
|
23
|
+
this.ledger = ledger;
|
|
24
|
+
this.stream = stream;
|
|
25
|
+
this.signal = signal;
|
|
26
|
+
this.finalize = finalize;
|
|
27
|
+
this.agents = new ConcurrencyGate(concurrency);
|
|
28
|
+
}
|
|
29
|
+
async enter(input) {
|
|
30
|
+
if (this.signal.aborted)
|
|
31
|
+
throw new EngineError('aborted', 'Execution cancelled before node admission');
|
|
32
|
+
const frame = this.ledger.enter(input);
|
|
33
|
+
this.admissions.set(frame.attemptId, input);
|
|
34
|
+
this.stream.flush();
|
|
35
|
+
return frame;
|
|
36
|
+
}
|
|
37
|
+
async execute(frame, effect, operation) {
|
|
38
|
+
const completed = this.ledger.resultFor(frame);
|
|
39
|
+
if (completed)
|
|
40
|
+
return structuredClone(completed);
|
|
41
|
+
const admission = this.admissions.get(frame.attemptId);
|
|
42
|
+
if (!admission)
|
|
43
|
+
throw new EngineError('attempt_mismatch', 'Node has not been admitted by this executor');
|
|
44
|
+
let effectRelease = () => { }, agentRelease = () => { };
|
|
45
|
+
const groupReleases = [];
|
|
46
|
+
try {
|
|
47
|
+
if (!COORDINATORS.has(admission.kind))
|
|
48
|
+
effectRelease = await this.effects.acquire(effect, this.signal);
|
|
49
|
+
if (admission.requiresAI && !COORDINATORS.has(admission.kind)) {
|
|
50
|
+
for (const limit of frame.scope.limits ?? []) {
|
|
51
|
+
let group = this.groups.get(limit.id);
|
|
52
|
+
if (!group) {
|
|
53
|
+
group = new ConcurrencyGate(limit.concurrency);
|
|
54
|
+
this.groups.set(limit.id, group);
|
|
55
|
+
}
|
|
56
|
+
if (group.capacity !== limit.concurrency)
|
|
57
|
+
throw new EngineError('scope_conflict', 'A map concurrency limit cannot change within a run');
|
|
58
|
+
groupReleases.push(await group.acquire(this.signal));
|
|
59
|
+
}
|
|
60
|
+
agentRelease = await this.agents.acquire(this.signal);
|
|
61
|
+
}
|
|
62
|
+
this.ledger.lease.assert(this.ledger.token);
|
|
63
|
+
if (this.signal.aborted)
|
|
64
|
+
throw new EngineError('aborted', 'Execution cancelled before effects');
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
effectRelease();
|
|
68
|
+
agentRelease();
|
|
69
|
+
for (const release of groupReleases.reverse())
|
|
70
|
+
release();
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
// LangGraph's saver releases this only after pending writes and terminal
|
|
74
|
+
// evidence commit. Releasing on function return admits a later write before
|
|
75
|
+
// the previous verification receipt is durable and can certify stale work.
|
|
76
|
+
this.held.set(frame.attemptId, () => { agentRelease(); for (const release of groupReleases.reverse())
|
|
77
|
+
release(); effectRelease(); });
|
|
78
|
+
const result = await operation(this.signal);
|
|
79
|
+
const current = this.ledger.scopeSnapshot(frame.scope.id);
|
|
80
|
+
return { ...result, usage: current.usage,
|
|
81
|
+
...(effect === 'write' && !result.receipt ? { verified: null } : {}),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
terminal(frame, result) {
|
|
85
|
+
const completed = this.ledger.resultFor(frame);
|
|
86
|
+
if (completed)
|
|
87
|
+
return this.ledger.terminal(frame, completed);
|
|
88
|
+
const input = this.admissions.get(frame.attemptId);
|
|
89
|
+
return this.ledger.terminal(frame, this.finalize && input && (!COORDINATORS.has(input.kind) || input.kind === 'implementation') ? this.finalize(frame, result, input.effect) : result);
|
|
90
|
+
}
|
|
91
|
+
async interrupted(frame, error) {
|
|
92
|
+
if (!isGraphInterrupt(error))
|
|
93
|
+
this.ledger.prepareInterruption(frame, errorDetails(error));
|
|
94
|
+
// The saver settles __interrupt__/__error__ with its ledger event. Keep the
|
|
95
|
+
// effect window until that transaction; process shutdown releases it too.
|
|
96
|
+
}
|
|
97
|
+
async failed(frame, error, options) {
|
|
98
|
+
const input = this.admissions.get(frame.attemptId), details = errorDetails(error);
|
|
99
|
+
const retryable = options.retry && input?.retry.retrySafe === true && !['aborted', 'lease_lost'].includes(details.code) && !(input.effect === 'write' && UNCERTAIN_EFFECT.has(details.code));
|
|
100
|
+
if (retryable) {
|
|
101
|
+
this.ledger.fail(frame, details, true);
|
|
102
|
+
this.release(frame.attemptId);
|
|
103
|
+
this.stream.flush();
|
|
104
|
+
}
|
|
105
|
+
// A final failure stays running until its atomic terminal marker commits.
|
|
106
|
+
return { retryable };
|
|
107
|
+
}
|
|
108
|
+
committed(events) {
|
|
109
|
+
for (const event of events)
|
|
110
|
+
if (event.attemptId && /^step_(?:succeeded|failed|blocked|paused|interrupted)$/.test(event.type))
|
|
111
|
+
this.release(event.attemptId);
|
|
112
|
+
this.stream.flush();
|
|
113
|
+
}
|
|
114
|
+
progress(event) { this.stream.progress(event); }
|
|
115
|
+
close() { for (const attemptId of this.held.keys())
|
|
116
|
+
this.release(attemptId); }
|
|
117
|
+
release(attemptId) {
|
|
118
|
+
this.held.get(attemptId)?.();
|
|
119
|
+
this.held.delete(attemptId);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type CoreDefinitionState, type JsonObject, type JsonValue } from './contracts.js';
|
|
2
|
+
type Expression = {
|
|
3
|
+
evaluate(state: CoreDefinitionState): unknown;
|
|
4
|
+
};
|
|
5
|
+
/** Bounded regex subset: no backreferences, lookaround or quantified groups. */
|
|
6
|
+
export declare function boundedPattern(pattern: string): RegExp;
|
|
7
|
+
/** Recursive descent over a closed grammar; no eval or executable expression callbacks. */
|
|
8
|
+
export declare function parseExpression(source: string): Expression;
|
|
9
|
+
export declare function validateInterpolations(value: JsonValue): void;
|
|
10
|
+
export declare function interpolateParams(params: JsonObject, vars: Record<string, JsonValue>): JsonObject;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { EngineError } from './contracts.js';
|
|
2
|
+
import { matchesPattern } from './regex.js';
|
|
3
|
+
const forbiddenKeys = new Set(['__proto__', 'prototype', 'constructor']);
|
|
4
|
+
function invalid(message) { throw new EngineError('invalid_expression', message); }
|
|
5
|
+
/** Bounded regex subset: no backreferences, lookaround or quantified groups. */
|
|
6
|
+
export function boundedPattern(pattern) {
|
|
7
|
+
if (pattern.length > 200 || /\\[1-9]|\(\?|\)[*+?{]/.test(pattern))
|
|
8
|
+
invalid('Pattern uses unsupported or unbounded regular-expression features');
|
|
9
|
+
try {
|
|
10
|
+
return new RegExp(pattern);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return invalid('Invalid regular expression');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function lookup(state, path) {
|
|
17
|
+
const parts = path.split('.');
|
|
18
|
+
if (!['$outputs', '$vars', '$verified', '$attempts', '$item'].includes(parts[0]))
|
|
19
|
+
invalid(`Unavailable state channel ${parts[0]}`);
|
|
20
|
+
let value = state;
|
|
21
|
+
for (const part of parts) {
|
|
22
|
+
if (forbiddenKeys.has(part))
|
|
23
|
+
invalid('Prototype property access is not permitted');
|
|
24
|
+
if (!value || typeof value !== 'object' || !Object.hasOwn(value, part))
|
|
25
|
+
return undefined;
|
|
26
|
+
value = value[part];
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
/** Recursive descent over a closed grammar; no eval or executable expression callbacks. */
|
|
31
|
+
export function parseExpression(source) {
|
|
32
|
+
if (!source.trim() || source.length > 4096)
|
|
33
|
+
invalid('Expression must contain 1–4096 characters');
|
|
34
|
+
const tokens = [];
|
|
35
|
+
let cursor = 0;
|
|
36
|
+
while (cursor < source.length) {
|
|
37
|
+
if (/\s/.test(source[cursor])) {
|
|
38
|
+
cursor += 1;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const rest = source.slice(cursor);
|
|
42
|
+
let match;
|
|
43
|
+
if ((match = /^(?:&&|\|\||==|!=|<=|>=|[!<>() ,])/.exec(rest))) {
|
|
44
|
+
tokens.push({ value: match[0], kind: 'symbol' });
|
|
45
|
+
}
|
|
46
|
+
else if ((match = /^\$(?:outputs|vars|verified|attempts|item)(?:\.[A-Za-z0-9_-]+)*/.exec(rest))) {
|
|
47
|
+
if (match[0].split('.').some(part => forbiddenKeys.has(part)))
|
|
48
|
+
invalid('Prototype property access is not permitted');
|
|
49
|
+
tokens.push({ value: match[0], kind: 'path' });
|
|
50
|
+
}
|
|
51
|
+
else if ((match = /^(?:true|false|null|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?|"(?:[^"\\\x00-\x1f]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*")/.exec(rest))) {
|
|
52
|
+
tokens.push({ value: match[0], kind: 'literal', literal: JSON.parse(match[0]) });
|
|
53
|
+
}
|
|
54
|
+
else if ((match = /^(?:exists|matches)\b/.exec(rest))) {
|
|
55
|
+
tokens.push({ value: match[0], kind: 'function' });
|
|
56
|
+
}
|
|
57
|
+
else if ((match = /^\/(?:[^/\\\r\n]|\\.)*\//.exec(rest))) {
|
|
58
|
+
tokens.push({ value: match[0], kind: 'regex', literal: boundedPattern(match[0].slice(1, -1)) });
|
|
59
|
+
}
|
|
60
|
+
else
|
|
61
|
+
invalid(`Unexpected expression token at ${cursor}`);
|
|
62
|
+
cursor += match[0].length;
|
|
63
|
+
if (tokens.length > 512)
|
|
64
|
+
invalid('Expression has too many tokens');
|
|
65
|
+
}
|
|
66
|
+
let index = 0;
|
|
67
|
+
function take(value) { if (tokens[index++]?.value !== value)
|
|
68
|
+
invalid(`Expected ${value}`); }
|
|
69
|
+
function primary(depth) {
|
|
70
|
+
if (depth > 32)
|
|
71
|
+
invalid('Expression nesting exceeds 32');
|
|
72
|
+
const token = tokens[index++];
|
|
73
|
+
if (!token)
|
|
74
|
+
return invalid('Unexpected end of expression');
|
|
75
|
+
if (token.value === '!') {
|
|
76
|
+
const operand = primary(depth + 1);
|
|
77
|
+
return { evaluate: state => !operand.evaluate(state) };
|
|
78
|
+
}
|
|
79
|
+
if (token.value === '(') {
|
|
80
|
+
const body = binary(0, depth + 1);
|
|
81
|
+
take(')');
|
|
82
|
+
return body;
|
|
83
|
+
}
|
|
84
|
+
if (token.kind === 'literal')
|
|
85
|
+
return { evaluate: () => token.literal };
|
|
86
|
+
if (token.kind === 'path')
|
|
87
|
+
return { evaluate: state => lookup(state, token.value) };
|
|
88
|
+
if (token.kind === 'function') {
|
|
89
|
+
take('(');
|
|
90
|
+
const operand = binary(0, depth + 1);
|
|
91
|
+
if (token.value === 'exists') {
|
|
92
|
+
take(')');
|
|
93
|
+
return { evaluate: state => operand.evaluate(state) !== undefined && operand.evaluate(state) !== null };
|
|
94
|
+
}
|
|
95
|
+
take(',');
|
|
96
|
+
const pattern = tokens[index++];
|
|
97
|
+
if (pattern?.kind !== 'regex')
|
|
98
|
+
invalid('matches requires a literal regular expression');
|
|
99
|
+
take(')');
|
|
100
|
+
return { evaluate: state => { const value = operand.evaluate(state); return typeof value === 'string' && matchesPattern(pattern.literal.source, value); } };
|
|
101
|
+
}
|
|
102
|
+
return invalid(`Unexpected ${token.value}`);
|
|
103
|
+
}
|
|
104
|
+
const precedence = { '||': 1, '&&': 2, '==': 3, '!=': 3, '<': 4, '<=': 4, '>': 4, '>=': 4 };
|
|
105
|
+
function binary(min, depth) {
|
|
106
|
+
let left = primary(depth);
|
|
107
|
+
while ((precedence[tokens[index]?.value] ?? -1) >= min) {
|
|
108
|
+
const operator = tokens[index++].value;
|
|
109
|
+
const right = binary(precedence[operator] + 1, depth + 1);
|
|
110
|
+
const previous = left;
|
|
111
|
+
left = { evaluate: state => {
|
|
112
|
+
const a = previous.evaluate(state);
|
|
113
|
+
if (operator === '&&')
|
|
114
|
+
return Boolean(a) && Boolean(right.evaluate(state));
|
|
115
|
+
if (operator === '||')
|
|
116
|
+
return Boolean(a) || Boolean(right.evaluate(state));
|
|
117
|
+
const b = right.evaluate(state);
|
|
118
|
+
if (operator === '==')
|
|
119
|
+
return a === b;
|
|
120
|
+
if (operator === '!=')
|
|
121
|
+
return a !== b;
|
|
122
|
+
if (!((typeof a === 'number' && typeof b === 'number') || (typeof a === 'string' && typeof b === 'string')))
|
|
123
|
+
return false;
|
|
124
|
+
if (operator === '<')
|
|
125
|
+
return a < b;
|
|
126
|
+
if (operator === '<=')
|
|
127
|
+
return a <= b;
|
|
128
|
+
if (operator === '>')
|
|
129
|
+
return a > b;
|
|
130
|
+
return a >= b;
|
|
131
|
+
} };
|
|
132
|
+
}
|
|
133
|
+
return left;
|
|
134
|
+
}
|
|
135
|
+
const expression = binary(0, 0);
|
|
136
|
+
if (index !== tokens.length)
|
|
137
|
+
invalid('Unexpected trailing expression tokens');
|
|
138
|
+
return expression;
|
|
139
|
+
}
|
|
140
|
+
export function validateInterpolations(value) {
|
|
141
|
+
if (typeof value === 'string') {
|
|
142
|
+
const tokens = value.replaceAll('{{{{', '').match(/\{\{[^}]*\}\}/g) ?? [];
|
|
143
|
+
for (const token of tokens)
|
|
144
|
+
if (token.startsWith('{{run.') && !/^\{\{run\.[A-Za-z][A-Za-z0-9_-]*\}\}$/.test(token))
|
|
145
|
+
throw new EngineError('invalid_interpolation', `Invalid runtime variable ${token}`);
|
|
146
|
+
}
|
|
147
|
+
else if (Array.isArray(value))
|
|
148
|
+
value.forEach(validateInterpolations);
|
|
149
|
+
else if (value && typeof value === 'object')
|
|
150
|
+
Object.values(value).forEach(validateInterpolations);
|
|
151
|
+
}
|
|
152
|
+
export function interpolateParams(params, vars) {
|
|
153
|
+
function resolve(value) {
|
|
154
|
+
if (typeof value === 'string')
|
|
155
|
+
return value.replace(/\{\{\{\{|\{\{run\.([A-Za-z][A-Za-z0-9_-]*)\}\}/g, (token, name) => {
|
|
156
|
+
if (token === '{{{{')
|
|
157
|
+
return '{{';
|
|
158
|
+
if (!name || !Object.hasOwn(vars, name))
|
|
159
|
+
throw new EngineError('run_var_missing', `Runtime variable ${name ?? ''} is missing`);
|
|
160
|
+
const replacement = vars[name];
|
|
161
|
+
if (replacement !== null && typeof replacement === 'object')
|
|
162
|
+
return JSON.stringify(replacement);
|
|
163
|
+
return String(replacement);
|
|
164
|
+
});
|
|
165
|
+
if (Array.isArray(value))
|
|
166
|
+
return value.map(resolve);
|
|
167
|
+
if (value && typeof value === 'object')
|
|
168
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, resolve(entry)]));
|
|
169
|
+
return value;
|
|
170
|
+
}
|
|
171
|
+
return resolve(params);
|
|
172
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type PipelineContext } from '../../pipeline/pipeline-state.js';
|
|
2
|
+
import { type ExecutorRegistry } from '../executors.js';
|
|
3
|
+
import { type JsonValue } from './contracts.js';
|
|
4
|
+
export interface ForkRunOptions {
|
|
5
|
+
fromNodePath: string;
|
|
6
|
+
runId: string;
|
|
7
|
+
scopeId?: string;
|
|
8
|
+
visit?: number;
|
|
9
|
+
/** Retry a published child after a lost host acknowledgement. */
|
|
10
|
+
requestId?: string;
|
|
11
|
+
state?: {
|
|
12
|
+
$vars?: Record<string, JsonValue>;
|
|
13
|
+
$outputs?: Record<string, JsonValue>;
|
|
14
|
+
};
|
|
15
|
+
registry?: ExecutorRegistry;
|
|
16
|
+
}
|
|
17
|
+
/** A fork publishes an independently leased historical cut; the source is opened read-only. */
|
|
18
|
+
export declare function forkRun(directory: string, options: ForkRunOptions): Promise<{
|
|
19
|
+
type: "runtime-forked";
|
|
20
|
+
runId: string;
|
|
21
|
+
forkOf: string;
|
|
22
|
+
fromNodePath: string;
|
|
23
|
+
scopeId: string;
|
|
24
|
+
visit: number;
|
|
25
|
+
directory: string;
|
|
26
|
+
context: PipelineContext;
|
|
27
|
+
revision: number;
|
|
28
|
+
}>;
|