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,146 @@
|
|
|
1
|
+
import { BaseCheckpointSaver, copyCheckpoint, WRITES_IDX_MAP } from '@langchain/langgraph-checkpoint';
|
|
2
|
+
import { EngineError } from '../contracts.js';
|
|
3
|
+
const configOf = (row) => ({ configurable: { thread_id: row.thread_id, checkpoint_ns: row.checkpoint_ns, checkpoint_id: row.checkpoint_id } });
|
|
4
|
+
function identity(config, taskId) {
|
|
5
|
+
const { thread_id, checkpoint_ns = '', checkpoint_id = '' } = config.configurable ?? {};
|
|
6
|
+
if (typeof thread_id !== 'string' || !thread_id || typeof checkpoint_ns !== 'string' || typeof checkpoint_id !== 'string')
|
|
7
|
+
throw new EngineError('invalid_arguments', 'Checkpoint thread, namespace and ID must be strings');
|
|
8
|
+
if (taskId !== undefined && (!taskId || !checkpoint_id))
|
|
9
|
+
throw new EngineError('invalid_arguments', 'Pending writes require checkpoint and task IDs');
|
|
10
|
+
return { checkpointThreadId: thread_id, taskCheckpointNs: checkpoint_ns, checkpointId: checkpoint_id, taskId: taskId ?? '' };
|
|
11
|
+
}
|
|
12
|
+
/** LangGraph adapter. Terminal evidence and pending writes have exactly one commit owner. */
|
|
13
|
+
export class SqliteRunSaver extends BaseCheckpointSaver {
|
|
14
|
+
database;
|
|
15
|
+
ledger;
|
|
16
|
+
options;
|
|
17
|
+
constructor(database, ledger, options = {}) {
|
|
18
|
+
super();
|
|
19
|
+
this.database = database;
|
|
20
|
+
this.ledger = ledger;
|
|
21
|
+
this.options = options;
|
|
22
|
+
}
|
|
23
|
+
assertWriter() {
|
|
24
|
+
if (!this.ledger)
|
|
25
|
+
throw new EngineError('read_only', 'Read-only saver cannot mutate checkpoints');
|
|
26
|
+
this.ledger.lease.assert(this.ledger.token);
|
|
27
|
+
return this.ledger;
|
|
28
|
+
}
|
|
29
|
+
async tuple(row) {
|
|
30
|
+
const pendingWrites = [];
|
|
31
|
+
for (const write of this.database.sqlite.prepare('SELECT * FROM writes WHERE thread_id=? AND checkpoint_ns=? AND checkpoint_id=? ORDER BY task_id,idx').all(row.thread_id, row.checkpoint_ns, row.checkpoint_id)) {
|
|
32
|
+
pendingWrites.push([String(write.task_id), String(write.channel), await this.serde.loadsTyped(String(write.type), write.value)]);
|
|
33
|
+
}
|
|
34
|
+
return { config: configOf(row), checkpoint: await this.serde.loadsTyped(String(row.type), row.checkpoint),
|
|
35
|
+
metadata: await this.serde.loadsTyped(String(row.metadata_type), row.metadata),
|
|
36
|
+
pendingWrites, ...(row.parent_checkpoint_id ? { parentConfig: configOf({ ...row, checkpoint_id: row.parent_checkpoint_id }) } : {}) };
|
|
37
|
+
}
|
|
38
|
+
async getTuple(config) {
|
|
39
|
+
const task = identity(config);
|
|
40
|
+
const row = task.checkpointId ? this.database.sqlite.prepare('SELECT * FROM checkpoints WHERE thread_id=? AND checkpoint_ns=? AND checkpoint_id=?').get(task.checkpointThreadId, task.taskCheckpointNs, task.checkpointId)
|
|
41
|
+
: this.database.sqlite.prepare('SELECT * FROM checkpoints WHERE thread_id=? AND checkpoint_ns=? ORDER BY checkpoint_id DESC LIMIT 1').get(task.checkpointThreadId, task.taskCheckpointNs);
|
|
42
|
+
return row ? this.tuple(row) : undefined;
|
|
43
|
+
}
|
|
44
|
+
async *list(config, options = {}) {
|
|
45
|
+
const clauses = [], values = [];
|
|
46
|
+
for (const key of ['thread_id', 'checkpoint_ns', 'checkpoint_id']) {
|
|
47
|
+
const value = config.configurable?.[key];
|
|
48
|
+
if (value !== undefined) {
|
|
49
|
+
if (typeof value !== 'string')
|
|
50
|
+
throw new EngineError('invalid_arguments', 'Checkpoint selectors must be strings');
|
|
51
|
+
clauses.push(`${key}=?`);
|
|
52
|
+
values.push(value);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (options.before?.configurable?.checkpoint_id) {
|
|
56
|
+
clauses.push('checkpoint_id<?');
|
|
57
|
+
values.push(String(options.before.configurable.checkpoint_id));
|
|
58
|
+
}
|
|
59
|
+
if (options.limit !== undefined && (!Number.isSafeInteger(options.limit) || options.limit < 0))
|
|
60
|
+
throw new EngineError('invalid_arguments', 'Checkpoint limit must be a non-negative integer');
|
|
61
|
+
let remaining = options.limit ?? Infinity;
|
|
62
|
+
const rows = this.database.sqlite.prepare('SELECT * FROM checkpoints' + (clauses.length ? ' WHERE ' + clauses.join(' AND ') : '') + ' ORDER BY checkpoint_id DESC').all(...values);
|
|
63
|
+
for (const row of rows) {
|
|
64
|
+
if (remaining === 0)
|
|
65
|
+
break;
|
|
66
|
+
const tuple = await this.tuple(row);
|
|
67
|
+
if (options.filter && !Object.entries(options.filter).every(([key, value]) => JSON.stringify(tuple.metadata?.[key]) === JSON.stringify(value)))
|
|
68
|
+
continue;
|
|
69
|
+
remaining--;
|
|
70
|
+
yield tuple;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async put(config, checkpoint, metadata) {
|
|
74
|
+
const task = identity(config), ledger = this.assertWriter();
|
|
75
|
+
const [[type, bytes], [metadataType, metadataBytes]] = await Promise.all([this.serde.dumpsTyped(copyCheckpoint(checkpoint)), this.serde.dumpsTyped(metadata)]);
|
|
76
|
+
const next = { configurable: { thread_id: task.checkpointThreadId, checkpoint_ns: task.taskCheckpointNs, checkpoint_id: checkpoint.id } };
|
|
77
|
+
this.options.fault?.('before-snapshot');
|
|
78
|
+
this.database.transaction('checkpoint-snapshot', () => {
|
|
79
|
+
this.assertWriter();
|
|
80
|
+
this.database.put('checkpoints', { thread_id: task.checkpointThreadId, checkpoint_ns: task.taskCheckpointNs, checkpoint_id: checkpoint.id,
|
|
81
|
+
parent_checkpoint_id: task.checkpointId || null, type, checkpoint: bytes, metadata: metadataBytes, metadata_type: metadataType });
|
|
82
|
+
if (task.taskCheckpointNs === '')
|
|
83
|
+
this.database.put('runs', { ...ledger.run(), head_checkpoint_json: JSON.stringify(next), current_revision: this.database.transactionRevision });
|
|
84
|
+
});
|
|
85
|
+
this.options.fault?.('after-snapshot');
|
|
86
|
+
return next;
|
|
87
|
+
}
|
|
88
|
+
async putWrites(config, writes, taskId) {
|
|
89
|
+
const task = identity(config, taskId), ledger = this.assertWriter();
|
|
90
|
+
// Serializer callbacks never run with a SQLite transaction held.
|
|
91
|
+
const dumped = await Promise.all(writes.map(async ([channel, value], index) => ({ channel, value, index: WRITES_IDX_MAP[channel] ?? index, blob: await this.serde.dumpsTyped(value) })));
|
|
92
|
+
const markers = dumped.filter(write => write.channel === '$commit');
|
|
93
|
+
if (markers.length > 1)
|
|
94
|
+
throw new EngineError('terminal_conflict', 'One physical task may settle only one terminal marker');
|
|
95
|
+
const marker = markers[0]?.value;
|
|
96
|
+
if (marker && (!marker.frame || !marker.result || typeof marker.digest !== 'string'))
|
|
97
|
+
throw new EngineError('terminal_mismatch', 'Malformed terminal marker');
|
|
98
|
+
this.options.fault?.('before-writes');
|
|
99
|
+
const events = this.database.transaction('pending-writes-and-ledger', () => {
|
|
100
|
+
this.assertWriter();
|
|
101
|
+
for (const write of dumped) {
|
|
102
|
+
const key = { thread_id: task.checkpointThreadId, checkpoint_ns: task.taskCheckpointNs, checkpoint_id: task.checkpointId, task_id: taskId, idx: write.index };
|
|
103
|
+
const prior = this.database.get('writes', key);
|
|
104
|
+
if (prior && write.index >= 0) {
|
|
105
|
+
if (marker && (prior.channel !== write.channel || prior.type !== write.blob[0] || !Buffer.from(prior.value).equals(Buffer.from(write.blob[1]))))
|
|
106
|
+
throw new EngineError('terminal_conflict', 'A settled task cannot change its pending writes');
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
this.database.put('writes', { ...key, channel: write.channel, type: write.blob[0], value: write.blob[1] });
|
|
110
|
+
}
|
|
111
|
+
this.options.fault?.('between-writes-ledger');
|
|
112
|
+
const committed = marker ? ledger.commitTerminal(marker, task) : [];
|
|
113
|
+
for (const write of dumped.filter(write => write.channel === '__interrupt__')) {
|
|
114
|
+
// LangGraph's public PendingWrite contains one interrupt per channel write.
|
|
115
|
+
const value = write.value;
|
|
116
|
+
if (!value || typeof value.id !== 'string')
|
|
117
|
+
throw new EngineError('interrupt_invalid', 'LangGraph interrupt IDs are required');
|
|
118
|
+
committed.push(...ledger.commitInterrupts(task, [value]));
|
|
119
|
+
}
|
|
120
|
+
for (const write of dumped.filter(write => write.channel === '__error__'))
|
|
121
|
+
committed.push(...ledger.commitError(task, write.value));
|
|
122
|
+
return committed;
|
|
123
|
+
});
|
|
124
|
+
this.options.fault?.('after-writes');
|
|
125
|
+
if (events.length && this.options.onEvents) {
|
|
126
|
+
try {
|
|
127
|
+
await this.options.onEvents(events);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
try {
|
|
131
|
+
this.options.onObserverError?.(error);
|
|
132
|
+
}
|
|
133
|
+
catch { /* Delivery cannot roll back durable execution. */ }
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async deleteThread(threadId) {
|
|
138
|
+
this.database.transaction('checkpoint-thread-deleted', () => {
|
|
139
|
+
this.assertWriter();
|
|
140
|
+
for (const table of ['writes', 'checkpoints']) {
|
|
141
|
+
for (const row of this.database.sqlite.prepare(`SELECT * FROM ${table} WHERE thread_id=?`).all(threadId))
|
|
142
|
+
this.database.delete(table, row);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { NODE_KINDS_VERSION } from './piece-registry.js';
|
|
2
|
+
export { NODE_KINDS_VERSION };
|
|
3
|
+
type Flags = Record<string, string | boolean>;
|
|
4
|
+
/**
|
|
5
|
+
* Resume and status select the engine from the frozen request (`workflow.engine`). A request
|
|
6
|
+
* without the block is a legacy run. A definition database without a request (a fork destination
|
|
7
|
+
* created before its request was materialized) still resolves to this engine.
|
|
8
|
+
*/
|
|
9
|
+
export declare function isDefinitionCommand(flags: Flags, command: string): boolean;
|
|
10
|
+
export declare function runDefinitionCommand(flags: Flags, positionals: string[], emit: (value: unknown) => void): Promise<number>;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { validatePipelineContext } from '../../pipeline/pipeline-state.js';
|
|
5
|
+
import { validateRuntimeConfig } from '../config.js';
|
|
6
|
+
import { CORE_WORKFLOW_VERSION } from '../core-host.js';
|
|
7
|
+
import { EngineError } from './contracts.js';
|
|
8
|
+
import { workflowDefinitionSchema } from './definition-schema.js';
|
|
9
|
+
import { MAX_STEERING_BYTES } from './steering/inbox.js';
|
|
10
|
+
import { MAX_DEFINITION_BYTES } from './canonical-json.js';
|
|
11
|
+
import { validateWorkflowDefinition } from './definition-validator.js';
|
|
12
|
+
import { configuredRoles } from './preflight.js';
|
|
13
|
+
import { validationPieceRegistry } from './pieces/index.js';
|
|
14
|
+
import { NODE_KINDS_VERSION } from './piece-registry.js';
|
|
15
|
+
import { cancelRun, createRun, definitionRunDirectory, freezeRunRequest, frozenRequestEngine, frozenRequestFile, resumeRun, signalRun } from './runs.js';
|
|
16
|
+
import { statusRun } from './run-status.js';
|
|
17
|
+
import { forkRun } from './fork.js';
|
|
18
|
+
export { NODE_KINDS_VERSION };
|
|
19
|
+
const required = (flags, key) => {
|
|
20
|
+
const value = flags[key];
|
|
21
|
+
if (typeof value !== 'string' || !value.trim())
|
|
22
|
+
throw new EngineError('invalid_arguments', `--${key} requires a value`);
|
|
23
|
+
return value;
|
|
24
|
+
};
|
|
25
|
+
const optional = (flags, key) => flags[key] === undefined ? undefined : required(flags, key);
|
|
26
|
+
const read = (filename) => JSON.parse(readFileSync(filename, 'utf8'));
|
|
27
|
+
/** Keep definition bytes intact so duplicate keys and malformed UTF-8 are rejected. */
|
|
28
|
+
async function stdinBytes(maximum = MAX_DEFINITION_BYTES) {
|
|
29
|
+
const chunks = [];
|
|
30
|
+
let size = 0;
|
|
31
|
+
for await (const value of process.stdin) {
|
|
32
|
+
const chunk = Buffer.isBuffer(value) ? value : Buffer.from(String(value));
|
|
33
|
+
size += chunk.length;
|
|
34
|
+
if (size > maximum)
|
|
35
|
+
throw new EngineError('invalid_arguments', `Standard input exceeds ${maximum} bytes`);
|
|
36
|
+
chunks.push(chunk);
|
|
37
|
+
}
|
|
38
|
+
return Buffer.concat(chunks);
|
|
39
|
+
}
|
|
40
|
+
function directory(flags) {
|
|
41
|
+
if (flags['run-dir'] !== undefined && flags.context !== undefined)
|
|
42
|
+
throw new EngineError('invalid_arguments', 'Use --run-dir or --context, not both');
|
|
43
|
+
return flags['run-dir'] !== undefined ? path.resolve(required(flags, 'run-dir')) : definitionRunDirectory(validatePipelineContext(read(required(flags, 'context'))));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Resume and status select the engine from the frozen request (`workflow.engine`). A request
|
|
47
|
+
* without the block is a legacy run. A definition database without a request (a fork destination
|
|
48
|
+
* created before its request was materialized) still resolves to this engine.
|
|
49
|
+
*/
|
|
50
|
+
export function isDefinitionCommand(flags, command) {
|
|
51
|
+
if (['workflows', 'fork', 'signal', 'cancel'].includes(command) || flags.definition !== undefined && command === 'run')
|
|
52
|
+
return true;
|
|
53
|
+
if (!['resume', 'status'].includes(command))
|
|
54
|
+
return false;
|
|
55
|
+
if (flags['run-dir'] !== undefined)
|
|
56
|
+
return true;
|
|
57
|
+
if (flags.context === undefined)
|
|
58
|
+
return false;
|
|
59
|
+
const context = validatePipelineContext(read(required(flags, 'context'))), requestFile = frozenRequestFile(context);
|
|
60
|
+
if (existsSync(requestFile))
|
|
61
|
+
return frozenRequestEngine(read(requestFile)) === 2;
|
|
62
|
+
return existsSync(path.join(definitionRunDirectory(context), 'run.sqlite'));
|
|
63
|
+
}
|
|
64
|
+
/** Public workflow identity carried by status and results, matching the frozen request block. */
|
|
65
|
+
const workflowIdentity = (workflow) => ({ ...workflow, definitionHash: workflow.version, engine: 2 });
|
|
66
|
+
export async function runDefinitionCommand(flags, positionals, emit) {
|
|
67
|
+
const command = positionals[0];
|
|
68
|
+
if (command === 'workflows') {
|
|
69
|
+
if (!positionals[1] || positionals[1] === 'list') {
|
|
70
|
+
emit({ type: 'runtime-workflows', nodeKindsVersion: NODE_KINDS_VERSION, nodeKinds: validationPieceRegistry().catalog(), definitionSchema: workflowDefinitionSchema,
|
|
71
|
+
builtins: [{ id: 'specrails-implementation', version: CORE_WORKFLOW_VERSION, deprecated: false }] });
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
if (positionals[1] !== 'validate' || flags.stdin !== true)
|
|
75
|
+
throw new EngineError('invalid_arguments', 'Use workflows validate --stdin [--config file | --structural]');
|
|
76
|
+
if (flags.structural !== undefined && flags.structural !== true)
|
|
77
|
+
throw new EngineError('invalid_arguments', '--structural is a boolean flag');
|
|
78
|
+
const roles = flags.config === undefined ? {} : configuredRoles(validateRuntimeConfig(read(required(flags, 'config'))));
|
|
79
|
+
const validation = validateWorkflowDefinition(await stdinBytes(), validationPieceRegistry(), roles, { structural: flags.structural === true });
|
|
80
|
+
emit({ ...validation, ...(flags.structural ? { roleResolution: 'deferred' } : {}) });
|
|
81
|
+
return validation.ok ? 0 : 1;
|
|
82
|
+
}
|
|
83
|
+
if (command === 'status') {
|
|
84
|
+
const status = await statusRun(directory(flags), flags.compact === true);
|
|
85
|
+
emit({ ...status, workflow: workflowIdentity(status.workflow) });
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
if (command === 'signal') {
|
|
89
|
+
if (flags.stdin !== true)
|
|
90
|
+
throw new EngineError('invalid_arguments', 'Signal requires --stdin');
|
|
91
|
+
const text = new TextDecoder('utf-8', { fatal: true }).decode(await stdinBytes(MAX_STEERING_BYTES));
|
|
92
|
+
const message = await signalRun(directory(flags), text, optional(flags, 'request-id'));
|
|
93
|
+
emit({ type: 'runtime-signal-accepted', id: message.id, acceptedAt: message.createdAt });
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
if (command === 'cancel') {
|
|
97
|
+
emit({ type: 'runtime-cancellation-accepted', ...await cancelRun(directory(flags), optional(flags, 'request-id')) });
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
const cutOptions = () => ({ ...(flags['scope-id'] === undefined ? {} : { scopeId: required(flags, 'scope-id') }),
|
|
101
|
+
...(flags.visit === undefined ? {} : { visit: Number(required(flags, 'visit')) }) });
|
|
102
|
+
if (command === 'fork') {
|
|
103
|
+
const fork = await forkRun(directory(flags), { fromNodePath: required(flags, 'from'), runId: required(flags, 'run-id'), ...cutOptions(),
|
|
104
|
+
...(flags['request-id'] === undefined ? {} : { requestId: required(flags, 'request-id') }),
|
|
105
|
+
...(flags.state === undefined ? {} : { state: read(required(flags, 'state')) }) });
|
|
106
|
+
await freezeRunRequest(fork.directory);
|
|
107
|
+
emit(fork);
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
const controller = new AbortController(), abort = () => controller.abort(new EngineError('aborted', 'Process termination requested'));
|
|
111
|
+
process.on('SIGINT', abort);
|
|
112
|
+
process.on('SIGTERM', abort);
|
|
113
|
+
try {
|
|
114
|
+
let previous;
|
|
115
|
+
let result;
|
|
116
|
+
if (command === 'run') {
|
|
117
|
+
if (flags.workflow !== undefined)
|
|
118
|
+
throw new EngineError('invalid_arguments', '--definition and --workflow are mutually exclusive');
|
|
119
|
+
result = await createRun({ context: read(required(flags, 'context')), config: read(required(flags, 'config')),
|
|
120
|
+
definition: readFileSync(required(flags, 'definition')), change: optional(flags, 'change'), signal: controller.signal, onEvent: emit });
|
|
121
|
+
}
|
|
122
|
+
else if (command === 'resume') {
|
|
123
|
+
if (flags.definition !== undefined || flags.config !== undefined)
|
|
124
|
+
throw new EngineError('invalid_arguments', 'Resume uses the frozen definition and config');
|
|
125
|
+
if ((flags.answer !== undefined || flags.approve !== undefined) && (flags.recover !== undefined || flags.invalidate !== undefined))
|
|
126
|
+
throw new EngineError('invalid_arguments', 'Human answers and recovery cannot be combined');
|
|
127
|
+
let target = directory(flags);
|
|
128
|
+
previous = await statusRun(target);
|
|
129
|
+
if (flags.invalidate !== undefined) {
|
|
130
|
+
if (flags.recover !== undefined)
|
|
131
|
+
throw new EngineError('invalid_arguments', 'Invalidate selects a new historical run; use recovery separately on that run');
|
|
132
|
+
const fork = await forkRun(target, { fromNodePath: required(flags, 'invalidate'),
|
|
133
|
+
runId: optional(flags, 'run-id') ?? previous.state.runId.slice(0, 96) + '-invalidate-' + randomUUID().slice(0, 8), ...cutOptions() });
|
|
134
|
+
await freezeRunRequest(fork.directory);
|
|
135
|
+
emit(fork);
|
|
136
|
+
target = fork.directory;
|
|
137
|
+
previous = await statusRun(target);
|
|
138
|
+
}
|
|
139
|
+
const answers = {}, pending = previous.state.pendingInterrupts;
|
|
140
|
+
if (flags.answer !== undefined) {
|
|
141
|
+
const matches = pending.filter(item => item.kind === 'question' && (flags['interrupt-id'] === undefined || item.id === required(flags, 'interrupt-id')));
|
|
142
|
+
if (matches.length !== 1)
|
|
143
|
+
throw new EngineError('interrupt_ambiguous', 'Select one pending question with --interrupt-id');
|
|
144
|
+
answers[matches[0].id] = { answer: required(flags, 'answer') };
|
|
145
|
+
}
|
|
146
|
+
if (flags.approve !== undefined)
|
|
147
|
+
for (const node of required(flags, 'approve').split(',')) {
|
|
148
|
+
const matches = pending.filter(item => ['approval', 'gate'].includes(item.kind) && (item.nodePath === node || item.id === node));
|
|
149
|
+
if (matches.length !== 1)
|
|
150
|
+
throw new EngineError('interrupt_ambiguous', 'Approval must identify exactly one pending node or interrupt');
|
|
151
|
+
answers[matches[0].id] = { approved: true };
|
|
152
|
+
}
|
|
153
|
+
result = await resumeRun(target, { signal: controller.signal, onEvent: emit, answers,
|
|
154
|
+
recover: optional(flags, 'recover')?.split(','),
|
|
155
|
+
...(flags.context && flags.invalidate === undefined ? { context: validatePipelineContext(read(required(flags, 'context'))) } : {}) });
|
|
156
|
+
}
|
|
157
|
+
else
|
|
158
|
+
throw new EngineError('invalid_arguments', `Unsupported definition command ${command}`);
|
|
159
|
+
const delta = (key) => {
|
|
160
|
+
const current = result.state.usage[key], prior = previous ? previous.state.usage[key] : 0;
|
|
161
|
+
return current === null || prior === null ? null : Math.max(0, current - prior);
|
|
162
|
+
};
|
|
163
|
+
emit({ type: 'runtime-result', engineVersion: 2, ...result.state, revision: result.revision, eventCursor: result.eventCursor, workflow: workflowIdentity(result.workflow), completion: result.completion,
|
|
164
|
+
...('forkOf' in result ? { forkOf: result.forkOf } : {}), ...('error' in result ? { error: result.error } : {}),
|
|
165
|
+
usage: result.state.usage, invocationUsage: { costUsd: delta('costUsd'), inputTokens: delta('inputTokens'), outputTokens: delta('outputTokens') }, metrics: result.metrics, efficiencySummary: result.efficiencySummary });
|
|
166
|
+
return result.state.status === 'succeeded' ? 0 : result.state.status === 'paused' ? 2 : 1;
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
process.off('SIGINT', abort);
|
|
170
|
+
process.off('SIGTERM', abort);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type BaseCheckpointSaver, type CompiledStateGraph } from '@langchain/langgraph';
|
|
2
|
+
import { type CoreDefinitionState, type JsonValue, type NodeExecutionPort } from './contracts.js';
|
|
3
|
+
import type { RoleCatalog, WorkflowDefinition } from './definition-types.js';
|
|
4
|
+
import type { PieceRegistry } from './piece-registry.js';
|
|
5
|
+
import { definitionStateSchema } from './state.js';
|
|
6
|
+
import type { ImplementationAdapter } from './pieces/implementation.js';
|
|
7
|
+
import type { JsonObject } from './contracts.js';
|
|
8
|
+
type DefinitionSchema = ReturnType<typeof definitionStateSchema>;
|
|
9
|
+
export type CompiledDefinitionGraph = CompiledStateGraph<CoreDefinitionState, Partial<CoreDefinitionState>, string, DefinitionSchema['spec'], DefinitionSchema['spec']>;
|
|
10
|
+
export interface CompileDefinitionOptions {
|
|
11
|
+
roles?: RoleCatalog;
|
|
12
|
+
checkpointer?: BaseCheckpointSaver;
|
|
13
|
+
/** Frozen host context collections available to map; never fetched by the compiler. */
|
|
14
|
+
collections?: {
|
|
15
|
+
tickets?: JsonValue[];
|
|
16
|
+
repositories?: JsonValue[];
|
|
17
|
+
};
|
|
18
|
+
/** Bound to the admitted change and real Core journal by the composition root. */
|
|
19
|
+
implementation?: (params: JsonObject) => ImplementationAdapter;
|
|
20
|
+
}
|
|
21
|
+
/** No provider or persistence side effects occur while compiling a validated document. */
|
|
22
|
+
export declare function compileWorkflowDefinition(definition: WorkflowDefinition, registry: PieceRegistry, execution: NodeExecutionPort, options?: CompileDefinitionOptions): CompiledDefinitionGraph;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { END, interrupt, isGraphInterrupt, Send, START, StateGraph } from '@langchain/langgraph';
|
|
2
|
+
import { EngineError } from './contracts.js';
|
|
3
|
+
import { validateWorkflowDefinition } from './definition-validator.js';
|
|
4
|
+
import { interpolateParams } from './expressions.js';
|
|
5
|
+
import { definitionStateSchema, initialDefinitionState, mergeHistory } from './state.js';
|
|
6
|
+
import { boundPieceOutput } from './output.js';
|
|
7
|
+
import { compileImplementationGraph, implementationContext, implementationInput } from './implementation-compiler.js';
|
|
8
|
+
class ClassifiedRetry extends Error {
|
|
9
|
+
cause;
|
|
10
|
+
constructor(cause) {
|
|
11
|
+
super(cause instanceof Error ? cause.message : String(cause));
|
|
12
|
+
this.cause = cause;
|
|
13
|
+
this.name = 'ClassifiedRetry';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** No provider or persistence side effects occur while compiling a validated document. */
|
|
17
|
+
export function compileWorkflowDefinition(definition, registry, execution, options = {}) {
|
|
18
|
+
const validated = validateWorkflowDefinition(definition, registry, options.roles, { published: true });
|
|
19
|
+
if (!validated.ok)
|
|
20
|
+
throw new EngineError('invalid_definition', 'Cannot compile an invalid definition', validated.errors.map(error => ({ ...error })));
|
|
21
|
+
const frozen = validated.definition;
|
|
22
|
+
return compileBody(frozen, true);
|
|
23
|
+
function compileBody(body, root) {
|
|
24
|
+
const schema = definitionStateSchema(frozen.policies?.historyMaxChars, root ? { id: 'root', nodePathPrefix: '' } : null);
|
|
25
|
+
const graph = new StateGraph(schema);
|
|
26
|
+
for (const [id, node] of Object.entries(body.nodes)) {
|
|
27
|
+
const piece = registry.get(node.kind);
|
|
28
|
+
const componentBody = node.kind === 'component' || node.kind === 'map' ? frozen.components?.[String(node.params[node.kind === 'map' ? 'body' : 'ref'])] : undefined;
|
|
29
|
+
const child = componentBody ? compileBody(componentBody, false) : undefined;
|
|
30
|
+
const implementation = node.kind === 'implementation' ? options.implementation?.(node.params) : undefined;
|
|
31
|
+
if (node.kind === 'implementation' && !implementation)
|
|
32
|
+
throw new EngineError('implementation_adapter_required', 'Implementation requires the real Core journal adapter');
|
|
33
|
+
const implementationChild = implementation ? compileImplementationGraph(implementation, execution) : undefined;
|
|
34
|
+
const effect = child || implementationChild ? 'read' : registry.effect(node.kind, node.params, options.roles ?? {});
|
|
35
|
+
const outcomes = node.kind === 'component' ? componentBody?.outputs ?? ['next', 'failed'] : registry.outcomes(node.kind, node.params);
|
|
36
|
+
const branchName = '_core_map_' + id;
|
|
37
|
+
const mapPredecessor = node.kind === 'join' ? Object.entries(body.nodes).find(([, value]) => value.kind === 'map' && value.ends.next === id)?.[0] : undefined;
|
|
38
|
+
const retry = { maxAttempts: node.retry?.maxAttempts ?? (piece.descriptor.requiresAI ? 2 : 1), retrySafe: effect === 'read', initialIntervalMs: node.retry?.backoffMs ?? 5000 };
|
|
39
|
+
graph.addNode(id, async (state, config) => {
|
|
40
|
+
const info = config.executionInfo;
|
|
41
|
+
if (!info?.threadId)
|
|
42
|
+
throw new EngineError('execution_identity_missing', 'Durable definitions require public task identity and a checkpoint thread');
|
|
43
|
+
const nodePath = state.$scope.nodePathPrefix ? state.$scope.nodePathPrefix + '/' + id : id;
|
|
44
|
+
const frame = await execution.enter({ nodePath, kind: node.kind, effect, requiresAI: piece.descriptor.requiresAI,
|
|
45
|
+
acceptsSteering: piece.descriptor.requiresAI && ['prompt', 'role-turn'].includes(node.kind) && node.params.appendSteering !== false, scope: state.$scope,
|
|
46
|
+
task: { checkpointThreadId: info.threadId, checkpointId: info.checkpointId, taskCheckpointNs: info.checkpointNs, taskId: info.taskId }, retry });
|
|
47
|
+
let result;
|
|
48
|
+
let mapPlan;
|
|
49
|
+
try {
|
|
50
|
+
const interpolated = interpolateParams(node.params, state.$vars);
|
|
51
|
+
const params = node.kind === 'end' && root ? { ...interpolated, requiresVerified: interpolated.requiresVerified === true || frozen.delivery?.requiresVerified === true } : interpolated;
|
|
52
|
+
if (node.kind === 'map') {
|
|
53
|
+
const items = mapItems(params.over, state, options.collections);
|
|
54
|
+
result = { outcome: 'next', output: { total: items.length }, branches: { [id]: { total: items.length, results: [], visitId: frame.visitId, transition: frame.transition } } };
|
|
55
|
+
// The item vector is stored once in the parent checkpoint, not once per Send argument.
|
|
56
|
+
mapPlan = { frame, items };
|
|
57
|
+
}
|
|
58
|
+
else if (node.kind === 'join' && mapPredecessor) {
|
|
59
|
+
const branch = state.$branches[mapPredecessor];
|
|
60
|
+
if (!branch || branch.results.length !== branch.total)
|
|
61
|
+
throw new EngineError('join_incomplete', 'Join cannot run before every branch has settled');
|
|
62
|
+
const ok = branch.results.filter(value => value.outcome === 'next' || value.outcome === 'success').length;
|
|
63
|
+
const passes = params.reduce === 'collect' || (params.reduce === 'any-ok' ? ok > 0 : ok === branch.total);
|
|
64
|
+
result = { outcome: passes ? 'next' : 'fail', output: { total: branch.total, ok, failed: branch.total - ok, results: branch.results.map(value => ({ index: value.index, outcome: value.outcome, output: value.output })) } };
|
|
65
|
+
}
|
|
66
|
+
else if (implementationChild && implementation) {
|
|
67
|
+
result = await execution.execute(frame, 'read', async (signal) => {
|
|
68
|
+
const context = implementationContext(state, frame, signal, execution);
|
|
69
|
+
const initial = await implementation.initialize(context);
|
|
70
|
+
const completed = await implementationChild.invoke({ ...implementationInput(state, frame), ...initial }, config);
|
|
71
|
+
const summarized = await implementation.summarize(completed, context);
|
|
72
|
+
if (completed.$nodeResult?.status === 'blocked')
|
|
73
|
+
summarized.status = 'blocked';
|
|
74
|
+
if (completed.$nodeResult?.error)
|
|
75
|
+
summarized.error = { code: summarized.status === 'blocked' ? 'implementation_blocked' : 'implementation_failed', message: completed.$nodeResult.error };
|
|
76
|
+
return summarized;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
else if (child && componentBody) {
|
|
80
|
+
const input = componentInput(state, frame, params.inputs, componentBody);
|
|
81
|
+
const completed = await child.invoke(input, config);
|
|
82
|
+
const exit = completed.$exit;
|
|
83
|
+
if (!exit)
|
|
84
|
+
throw new EngineError('component_exit_missing', `Component ${nodePath} did not produce an exit`);
|
|
85
|
+
result = { outcome: exit.outcome, status: exit.status, output: { outputs: completed.$outputs, completion: { ...exit.completion } },
|
|
86
|
+
candidate: completed.$candidate ?? undefined, verified: completed.$verified, usage: completed.$usage, completion: exit.completion,
|
|
87
|
+
...(exit.error ? { error: exit.error } : {}) };
|
|
88
|
+
if (!result.candidate)
|
|
89
|
+
delete result.candidate;
|
|
90
|
+
}
|
|
91
|
+
else
|
|
92
|
+
result = await execution.execute(frame, effect, signal => piece.execute(params, {
|
|
93
|
+
state: structuredClone(state), frame, signal, progress: event => execution.progress({ ...event, payload: {
|
|
94
|
+
...(event.payload && typeof event.payload === 'object' && !Array.isArray(event.payload) ? event.payload : { payload: event.payload }),
|
|
95
|
+
runId: frame.runId, nodePath: frame.nodePath, scopeId: frame.scope.id,
|
|
96
|
+
...(frame.scope.branchId ? { branch: frame.scope.branchId } : {}),
|
|
97
|
+
attemptId: frame.attemptId, attempt: frame.attempt, visit: frame.visit,
|
|
98
|
+
} }),
|
|
99
|
+
interrupt: request => interrupt(request),
|
|
100
|
+
}));
|
|
101
|
+
if (!outcomes.includes(result.outcome) && node.kind !== 'end' && result.status !== 'failed')
|
|
102
|
+
throw new EngineError('invalid_piece_outcome', `Piece ${id} returned undeclared outcome ${result.outcome}`);
|
|
103
|
+
result = { ...result, ...(result.output !== undefined ? { output: boundPieceOutput(result.output) } : {}),
|
|
104
|
+
...(result.history ? { history: mergeHistory([], result.history, frozen.policies?.historyMaxChars ?? 1500) } : {}) };
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (isGraphInterrupt(error)) {
|
|
108
|
+
await execution.interrupted(frame, error);
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
if (child || implementationChild) {
|
|
112
|
+
await execution.interrupted(frame, error);
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
const code = error instanceof EngineError || (error && typeof error === 'object' && 'code' in error) ? String(error.code) : 'piece_failed';
|
|
116
|
+
if (effect === 'write' && ['aborted', 'timeout', 'idle_timeout', 'lease_lost'].includes(code)) {
|
|
117
|
+
await execution.interrupted(frame, error);
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
const allowedCodes = node.retry?.retryOn ?? ['idle_timeout', 'provider_request_error'];
|
|
121
|
+
const failure = await execution.failed(frame, error, { retry: frame.attempt < retry.maxAttempts && allowedCodes.includes(code) });
|
|
122
|
+
if (failure.retryable)
|
|
123
|
+
throw new ClassifiedRetry(error);
|
|
124
|
+
result = { outcome: 'failed', status: 'failed', error: { code, message: error instanceof Error ? error.message : String(error) } };
|
|
125
|
+
}
|
|
126
|
+
const exitsBody = node.kind === 'end' || node.ends[result.outcome] === null || (!Object.hasOwn(node.ends, result.outcome) && result.status === 'failed');
|
|
127
|
+
result = { ...result, completesRun: root && exitsBody, ...(exitsBody && !result.completion ? { completion: implicitCompletion(result, state) } : {}) };
|
|
128
|
+
if (root && exitsBody && frozen.delivery?.requiresVerified && result.completion && !result.completion.verified) {
|
|
129
|
+
result.completion = { ...result.completion, ok: false, reasons: [...new Set([...result.completion.reasons, 'unverified'])] };
|
|
130
|
+
}
|
|
131
|
+
const marker = execution.terminal(frame, result);
|
|
132
|
+
const patch = resultPatch(id, frame, marker.result, marker);
|
|
133
|
+
if (mapPlan)
|
|
134
|
+
patch.$maps = { [id]: mapPlan };
|
|
135
|
+
if (node.kind === 'join' && mapPredecessor && state.$maps[mapPredecessor])
|
|
136
|
+
patch.$maps = { [mapPredecessor]: { ...state.$maps[mapPredecessor], items: [] } };
|
|
137
|
+
if (exitsBody) {
|
|
138
|
+
const completion = marker.result.completion ?? implicitCompletion(result, state);
|
|
139
|
+
patch.$exit = { outcome: typeof node.params.exit === 'string' ? node.params.exit : completion.ok ? 'next' : 'failed',
|
|
140
|
+
transition: frame.transition, completion, status: marker.result.status ?? 'succeeded', ...(result.error ? { error: result.error } : {}) };
|
|
141
|
+
}
|
|
142
|
+
return patch;
|
|
143
|
+
}, { ...(implementationChild ? { subgraphs: [implementationChild] } : child && node.kind !== 'map' ? { subgraphs: [child] } : {}), ...(node.kind === 'join' ? { defer: true } : {}), retryPolicy: { maxAttempts: retry.maxAttempts, initialInterval: retry.initialIntervalMs, jitter: false, retryOn: error => error instanceof ClassifiedRetry } });
|
|
144
|
+
if (node.kind === 'map' && child && componentBody) {
|
|
145
|
+
graph.addNode(branchName, async (task, config) => {
|
|
146
|
+
const input = componentInput(task.seed, task.frame, undefined, componentBody);
|
|
147
|
+
input.$scope.id += '/' + task.index;
|
|
148
|
+
input.$scope.branchId = task.frame.visitId + ':' + task.index;
|
|
149
|
+
input.$scope.limits = [...(task.frame.scope.limits ?? []), { id: task.frame.visitId, concurrency: Number(node.params.concurrency ?? frozen.policies?.concurrency ?? 1) }];
|
|
150
|
+
input.$item = { index: task.index, value: task.item };
|
|
151
|
+
input.$vars.item = task.item;
|
|
152
|
+
input.$vars.index = task.index;
|
|
153
|
+
const completed = await child.invoke(input, config);
|
|
154
|
+
if (!completed.$exit)
|
|
155
|
+
throw new EngineError('component_exit_missing', 'Map body did not produce an exit');
|
|
156
|
+
return { $branches: { [id]: { total: task.total, visitId: task.frame.visitId, transition: task.frame.transition,
|
|
157
|
+
results: [{ id: task.frame.visitId + ':' + task.index, transition: task.frame.transition, attempt: task.frame.attempt, ordinal: task.index,
|
|
158
|
+
index: task.index, outcome: completed.$exit.completion.ok ? 'next' : 'failed', output: boundPieceOutput({ outputs: completed.$outputs, completion: { ...completed.$exit.completion } }) }] } } };
|
|
159
|
+
}, { subgraphs: [child] });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Dynamic IDs are validated above; LangGraph's builder generic accumulates only literal IDs.
|
|
163
|
+
const edges = graph;
|
|
164
|
+
edges.addEdge(START, body.entry);
|
|
165
|
+
for (const [id, node] of Object.entries(body.nodes)) {
|
|
166
|
+
if (node.kind === 'end') {
|
|
167
|
+
edges.addEdge(id, END);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (node.kind === 'map') {
|
|
171
|
+
const branchName = '_core_map_' + id;
|
|
172
|
+
const join = node.ends.next;
|
|
173
|
+
edges.addConditionalEdges(id, state => {
|
|
174
|
+
if (state.$lastOutcome[id] !== 'next')
|
|
175
|
+
return END;
|
|
176
|
+
const plan = state.$maps[id];
|
|
177
|
+
if (!plan || !plan.items.length)
|
|
178
|
+
return join;
|
|
179
|
+
const seed = initialDefinitionState(state.$vars, state.$scope);
|
|
180
|
+
seed.$candidate = state.$candidate;
|
|
181
|
+
seed.$verified = state.$verified;
|
|
182
|
+
seed.$usage = state.$usage;
|
|
183
|
+
return plan.items.map((item, index) => new Send(branchName, { seed, frame: plan.frame, item, index, total: plan.items.length }));
|
|
184
|
+
}, [branchName, join, END]);
|
|
185
|
+
edges.addEdge(branchName, join);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const mapping = Object.fromEntries(Object.entries(node.ends).map(([outcome, target]) => [outcome, target ?? END]));
|
|
189
|
+
mapping.__engine_failure = END;
|
|
190
|
+
edges.addConditionalEdges(id, state => {
|
|
191
|
+
const outcome = state.$lastOutcome[id];
|
|
192
|
+
return Object.hasOwn(node.ends, outcome) ? outcome : '__engine_failure';
|
|
193
|
+
}, mapping);
|
|
194
|
+
}
|
|
195
|
+
return edges.compile(root ? { checkpointer: options.checkpointer } : {});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function resultPatch(id, frame, result, marker) {
|
|
199
|
+
return {
|
|
200
|
+
$outputs: { [id]: result.output ?? null }, $lastOutcome: { [id]: result.outcome },
|
|
201
|
+
$attempts: { [id]: frame.attempt }, $transitions: frame.transition, $commit: marker,
|
|
202
|
+
...(result.vars ? { $vars: result.vars } : {}), ...(result.history ? { $history: result.history } : {}),
|
|
203
|
+
...(result.session ? { $sessions: { [id]: result.session } } : {}), ...(result.usage ? { $usage: result.usage } : {}),
|
|
204
|
+
...(result.candidate ? { $candidate: result.candidate } : {}),
|
|
205
|
+
...(Object.hasOwn(result, 'verified') ? { $verified: result.verified ?? null } : {}),
|
|
206
|
+
...(result.answers ? { $answers: result.answers } : {}), ...(result.branches ? { $branches: result.branches } : {}),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function componentInput(state, frame, mapping, body) {
|
|
210
|
+
const vars = {};
|
|
211
|
+
for (const name of body.inputs ?? [])
|
|
212
|
+
if (Object.hasOwn(state.$vars, name))
|
|
213
|
+
vars[name] = state.$vars[name];
|
|
214
|
+
if (mapping && typeof mapping === 'object' && !Array.isArray(mapping))
|
|
215
|
+
for (const [key, value] of Object.entries(mapping)) {
|
|
216
|
+
if (typeof value === 'string' && value.startsWith('$')) {
|
|
217
|
+
const parts = value.split('.');
|
|
218
|
+
if (!['$vars', '$outputs', '$history', '$answers', '$candidate', '$verified', '$item'].includes(parts[0]))
|
|
219
|
+
throw new EngineError('invalid_component_input', `Unsupported input channel ${parts[0]}`);
|
|
220
|
+
let resolved = state;
|
|
221
|
+
for (const part of parts) {
|
|
222
|
+
if (['__proto__', 'constructor', 'prototype'].includes(part) || !resolved || typeof resolved !== 'object' || !Object.hasOwn(resolved, part))
|
|
223
|
+
throw new EngineError('run_var_missing', `Component input ${value} is missing`);
|
|
224
|
+
resolved = resolved[part];
|
|
225
|
+
}
|
|
226
|
+
vars[key] = resolved;
|
|
227
|
+
}
|
|
228
|
+
else
|
|
229
|
+
vars[key] = value;
|
|
230
|
+
}
|
|
231
|
+
const input = initialDefinitionState(vars, { id: frame.scope.id + '/' + frame.visitId, nodePathPrefix: frame.nodePath,
|
|
232
|
+
...(frame.scope.branchId ? { branchId: frame.scope.branchId } : {}), ...(frame.scope.limits ? { limits: frame.scope.limits } : {}) });
|
|
233
|
+
input.$candidate = state.$candidate;
|
|
234
|
+
input.$verified = state.$verified;
|
|
235
|
+
input.$usage = state.$usage;
|
|
236
|
+
return input;
|
|
237
|
+
}
|
|
238
|
+
function implicitCompletion(result, state) {
|
|
239
|
+
const candidate = result.candidate ?? state.$candidate;
|
|
240
|
+
const verified = Object.hasOwn(result, 'verified') ? result.verified : state.$verified;
|
|
241
|
+
return { ok: !result.status || result.status === 'succeeded' ? ['success', 'next', 'pass', 'stop', 'ok', 'true'].includes(result.outcome) : false,
|
|
242
|
+
reasons: result.error ? [result.error.code] : [], verified: Boolean(candidate && verified && candidate.hash === verified.candidateHash) };
|
|
243
|
+
}
|
|
244
|
+
function mapItems(over, state, collections) {
|
|
245
|
+
let items;
|
|
246
|
+
if (over === 'tickets' || over === 'repositories')
|
|
247
|
+
items = collections?.[over];
|
|
248
|
+
else if (over && typeof over === 'object' && !Array.isArray(over) && typeof over.outputsOf === 'string' && typeof over.path === 'string') {
|
|
249
|
+
items = Object.hasOwn(state.$outputs, over.outputsOf) ? state.$outputs[over.outputsOf] : undefined;
|
|
250
|
+
for (const part of over.path ? over.path.split('.') : []) {
|
|
251
|
+
if (['__proto__', 'constructor', 'prototype'].includes(part) || !items || typeof items !== 'object' || !Object.hasOwn(items, part))
|
|
252
|
+
throw new EngineError('map_input_missing', 'Map output path is missing');
|
|
253
|
+
items = items[part];
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (!Array.isArray(items))
|
|
257
|
+
throw new EngineError('map_input_missing', 'Map requires a frozen array collection');
|
|
258
|
+
if (items.length > 10_000)
|
|
259
|
+
throw new EngineError('map_input_limit', 'Map item count cannot exceed the global maximum transition bound');
|
|
260
|
+
return structuredClone(items);
|
|
261
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { RunDatabase } from './checkpoint/database.js';
|
|
2
|
+
import type { RunLedger } from './checkpoint/ledger.js';
|
|
3
|
+
import { SqliteRunSaver } from './checkpoint/saver.js';
|
|
4
|
+
import type { EngineEventStream } from './events.js';
|
|
5
|
+
import { DefinitionExecution } from './execution.js';
|
|
6
|
+
import { RunPieceDependencies } from './piece-dependencies.js';
|
|
7
|
+
import type { preflightDefinition } from './preflight.js';
|
|
8
|
+
export type DefinitionAdmission = Awaited<ReturnType<typeof preflightDefinition>>;
|
|
9
|
+
/** Infrastructure is bound once to the frozen project and the current execution lease. */
|
|
10
|
+
export declare function composeDefinitionRuntime(database: RunDatabase, ledger: RunLedger, admitted: DefinitionAdmission, stream: EngineEventStream, signal: AbortSignal): Promise<{
|
|
11
|
+
deps: RunPieceDependencies;
|
|
12
|
+
execution: DefinitionExecution;
|
|
13
|
+
saver: SqliteRunSaver;
|
|
14
|
+
graph: import("./compiler.js").CompiledDefinitionGraph;
|
|
15
|
+
close: () => void;
|
|
16
|
+
}>;
|