@kuralle-agents/core 0.11.0 → 0.13.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 +85 -0
- package/dist/capabilities/LivePromptAssembler.js +1 -0
- package/dist/flow/collectDigression.d.ts +5 -2
- package/dist/flow/collectDigression.js +48 -10
- package/dist/flow/collectUntilComplete.js +9 -1
- package/dist/flow/extraction.d.ts +1 -0
- package/dist/flow/extraction.js +4 -0
- package/dist/flow/runFlow.js +63 -5
- package/dist/index.d.ts +9 -1
- package/dist/index.js +5 -0
- package/dist/prompts/PromptBuilder.js +7 -0
- package/dist/prompts/types.d.ts +2 -0
- package/dist/runtime/InMemoryRetrievalCache.d.ts +25 -0
- package/dist/runtime/InMemoryRetrievalCache.js +59 -0
- package/dist/runtime/KnowledgeProvider.js +6 -1
- package/dist/runtime/Runtime.d.ts +44 -0
- package/dist/runtime/Runtime.js +196 -13
- package/dist/runtime/TokenAccumulator.d.ts +7 -0
- package/dist/runtime/TokenAccumulator.js +7 -0
- package/dist/runtime/TraceRecorder.d.ts +33 -0
- package/dist/runtime/TraceRecorder.js +290 -0
- package/dist/runtime/buildAgentToolSurface.js +4 -0
- package/dist/runtime/channels/TextDriver.js +45 -22
- package/dist/runtime/channels/executeModelTool.d.ts +8 -1
- package/dist/runtime/channels/executeModelTool.js +70 -1
- package/dist/runtime/channels/inputBuffer.d.ts +1 -0
- package/dist/runtime/channels/inputBuffer.js +9 -0
- package/dist/runtime/closeRun.js +11 -8
- package/dist/runtime/compaction.d.ts +2 -0
- package/dist/runtime/compaction.js +3 -2
- package/dist/runtime/ctx.js +114 -36
- package/dist/runtime/durable/RunStore.d.ts +16 -0
- package/dist/runtime/durable/RunStore.js +6 -0
- package/dist/runtime/durable/SessionRunStore.d.ts +7 -2
- package/dist/runtime/durable/SessionRunStore.js +136 -34
- package/dist/runtime/durable/idempotency.d.ts +1 -0
- package/dist/runtime/durable/idempotency.js +3 -0
- package/dist/runtime/durable/replay.js +7 -2
- package/dist/runtime/durable/types.d.ts +11 -0
- package/dist/runtime/goals.d.ts +18 -0
- package/dist/runtime/goals.js +158 -0
- package/dist/runtime/grounding/knowledge.js +1 -1
- package/dist/runtime/handoffContinuation.d.ts +20 -0
- package/dist/runtime/handoffContinuation.js +30 -0
- package/dist/runtime/handoffOscillation.d.ts +6 -0
- package/dist/runtime/handoffOscillation.js +17 -0
- package/dist/runtime/hostLoop.js +11 -0
- package/dist/runtime/index.d.ts +3 -1
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/openRun.d.ts +2 -0
- package/dist/runtime/openRun.js +46 -17
- package/dist/runtime/policies/limits.d.ts +1 -0
- package/dist/runtime/policies/limits.js +3 -0
- package/dist/runtime/resolveAgentWorkspace.d.ts +3 -0
- package/dist/runtime/resolveAgentWorkspace.js +6 -2
- package/dist/runtime/select.d.ts +5 -1
- package/dist/runtime/select.js +30 -1
- package/dist/runtime/turnTokenUsage.d.ts +28 -0
- package/dist/runtime/turnTokenUsage.js +70 -0
- package/dist/session/SessionStore.d.ts +6 -0
- package/dist/session/SessionStore.js +12 -1
- package/dist/session/stores/MemoryStore.d.ts +1 -1
- package/dist/session/stores/MemoryStore.js +16 -2
- package/dist/session/testing.d.ts +6 -1
- package/dist/session/testing.js +34 -0
- package/dist/session/utils.d.ts +3 -0
- package/dist/session/utils.js +23 -0
- package/dist/tools/effect/ToolExecutor.js +10 -2
- package/dist/tools/effect/defineTool.d.ts +3 -0
- package/dist/tools/effect/defineTool.js +3 -0
- package/dist/tools/fs/caps.d.ts +21 -0
- package/dist/tools/fs/caps.js +59 -0
- package/dist/tools/fs/createFsTool.js +68 -7
- package/dist/tools/fs/createShellTool.d.ts +7 -0
- package/dist/tools/fs/createShellTool.js +90 -0
- package/dist/tracing/MemoryTraceStore.d.ts +16 -0
- package/dist/tracing/MemoryTraceStore.js +56 -0
- package/dist/tracing/OtelTraceSink.d.ts +127 -0
- package/dist/tracing/OtelTraceSink.js +101 -0
- package/dist/tracing/TraceStore.d.ts +19 -0
- package/dist/tracing/TraceStore.js +32 -0
- package/dist/tracing/index.d.ts +3 -0
- package/dist/tracing/index.js +3 -0
- package/dist/tracing/testing.d.ts +3 -0
- package/dist/tracing/testing.js +45 -0
- package/dist/types/agentConfig.d.ts +4 -1
- package/dist/types/channel.d.ts +14 -1
- package/dist/types/effectTool.d.ts +6 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/run-context.d.ts +15 -2
- package/dist/types/session.d.ts +10 -0
- package/dist/types/shell.d.ts +15 -0
- package/dist/types/shell.js +1 -0
- package/dist/types/stream.d.ts +8 -0
- package/dist/types/trace.d.ts +50 -0
- package/dist/types/trace.js +1 -0
- package/guides/EXAMPLE_VERIFICATION.md +4 -4
- package/package.json +14 -4
package/dist/runtime/ctx.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { SuspendError } from './durable/RunStore.js';
|
|
3
|
-
import { clockEffectKey,
|
|
3
|
+
import { clockEffectKey, idempotencyKey, logicalRunId, pauseEffectKey, } from './durable/idempotency.js';
|
|
4
4
|
import { findStepByKey } from './durable/replay.js';
|
|
5
5
|
import { ToolApprovalDeniedError } from '../tools/effect/errors.js';
|
|
6
6
|
const APPROVAL_SIGNAL = '__approval';
|
|
@@ -12,58 +12,114 @@ function makeCtx(deps) {
|
|
|
12
12
|
uuid: () => randomUUID(),
|
|
13
13
|
};
|
|
14
14
|
const emit = deps.emit ?? (() => { });
|
|
15
|
+
// Mutable holder: handoff reassigns runCtx.toolExecutor; ctx.tool must see the swap.
|
|
16
|
+
const toolExecutorHolder = { executor: deps.toolExecutor };
|
|
15
17
|
const consumeCallsite = () => {
|
|
16
18
|
const site = String(effectOrdinal);
|
|
17
19
|
effectOrdinal += 1;
|
|
18
20
|
return site;
|
|
19
21
|
};
|
|
20
|
-
const
|
|
22
|
+
const isFinishedStep = (hit) => {
|
|
23
|
+
if (hit.status === 'finished') {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
if (hit.status === 'running') {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
if (hit.status === 'error' || hit.error) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
return hit.result !== undefined;
|
|
33
|
+
};
|
|
34
|
+
const updateLocalStep = (key, patch) => {
|
|
35
|
+
const idx = steps.findIndex((step) => step.key === key);
|
|
36
|
+
if (idx >= 0) {
|
|
37
|
+
steps[idx] = { ...steps[idx], ...patch };
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const appendPendingStep = async (key, kind, name, index, signalId) => {
|
|
21
41
|
const startedAt = Date.now();
|
|
22
42
|
const record = {
|
|
23
|
-
index
|
|
43
|
+
index,
|
|
24
44
|
key,
|
|
25
45
|
kind,
|
|
26
46
|
name,
|
|
27
47
|
signalId,
|
|
28
|
-
|
|
48
|
+
status: 'running',
|
|
29
49
|
startedAt,
|
|
30
|
-
|
|
50
|
+
epoch: deps.runState.runEpoch ?? 0,
|
|
31
51
|
};
|
|
32
52
|
await deps.runStore.appendStep(deps.runState.runId, record);
|
|
33
|
-
steps.
|
|
34
|
-
|
|
35
|
-
|
|
53
|
+
const localIdx = steps.findIndex((step) => step.index === index);
|
|
54
|
+
if (localIdx >= 0) {
|
|
55
|
+
steps[localIdx] = record;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
while (steps.length < index) {
|
|
59
|
+
steps.push({
|
|
60
|
+
index: steps.length,
|
|
61
|
+
key: `__reserve:${deps.runState.runId}:${steps.length}`,
|
|
62
|
+
kind: 'tool',
|
|
63
|
+
name: '__reserve',
|
|
64
|
+
status: 'running',
|
|
65
|
+
startedAt,
|
|
66
|
+
epoch: deps.runState.runEpoch ?? 0,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (steps.length === index) {
|
|
70
|
+
steps.push(record);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
steps[index] = record;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
36
76
|
};
|
|
37
|
-
const replayOrExecute = async (key, kind, name, execute) => {
|
|
77
|
+
const replayOrExecute = async (key, kind, name, execute, options) => {
|
|
38
78
|
const hit = findStepByKey(steps, key);
|
|
39
79
|
if (hit) {
|
|
40
|
-
if (hit.error) {
|
|
80
|
+
if (hit.status === 'error' || hit.error) {
|
|
41
81
|
throw Object.assign(new Error(hit.error.message), { name: hit.error.name });
|
|
42
82
|
}
|
|
43
|
-
|
|
83
|
+
if (isFinishedStep(hit)) {
|
|
84
|
+
return hit.result;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const isRetry = hit?.status === 'running';
|
|
88
|
+
const stepIndex = options?.index ?? hit?.index ?? steps.length;
|
|
89
|
+
if (!isRetry) {
|
|
90
|
+
await appendPendingStep(key, kind, name, stepIndex);
|
|
44
91
|
}
|
|
92
|
+
let result;
|
|
45
93
|
try {
|
|
46
|
-
|
|
47
|
-
await appendLiveStep(key, kind, name, result);
|
|
48
|
-
return result;
|
|
94
|
+
result = await execute();
|
|
49
95
|
}
|
|
50
96
|
catch (error) {
|
|
51
97
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
98
|
+
const finishedAt = Date.now();
|
|
99
|
+
await deps.runStore.finalizeStep(deps.runState.runId, key, {
|
|
100
|
+
status: 'error',
|
|
101
|
+
error: { name: err.name, message: err.message },
|
|
102
|
+
finishedAt,
|
|
103
|
+
});
|
|
104
|
+
updateLocalStep(key, {
|
|
105
|
+
status: 'error',
|
|
58
106
|
error: { name: err.name, message: err.message },
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
};
|
|
62
|
-
await deps.runStore.appendStep(deps.runState.runId, record);
|
|
63
|
-
steps.push(record);
|
|
107
|
+
finishedAt,
|
|
108
|
+
});
|
|
64
109
|
throw err;
|
|
65
110
|
}
|
|
111
|
+
const finishedAt = Date.now();
|
|
112
|
+
await deps.runStore.finalizeStep(deps.runState.runId, key, {
|
|
113
|
+
status: 'finished',
|
|
114
|
+
result,
|
|
115
|
+
finishedAt,
|
|
116
|
+
});
|
|
117
|
+
updateLocalStep(key, { status: 'finished', result, finishedAt });
|
|
118
|
+
deps.runState.updatedAt = finishedAt;
|
|
119
|
+
await deps.runStore.putRunState(deps.runState);
|
|
120
|
+
return result;
|
|
66
121
|
};
|
|
122
|
+
const effectRunId = () => logicalRunId(deps.runState.runId, deps.runState.runEpoch);
|
|
67
123
|
const suspendForSignal = async (signalName, callsite, meta) => {
|
|
68
124
|
deps.runState.waitingFor = {
|
|
69
125
|
signalName,
|
|
@@ -80,13 +136,15 @@ function makeCtx(deps) {
|
|
|
80
136
|
};
|
|
81
137
|
const pauseEffect = async (signalName, meta) => {
|
|
82
138
|
const callsite = consumeCallsite();
|
|
83
|
-
const key = pauseEffectKey(
|
|
139
|
+
const key = pauseEffectKey(effectRunId(), callsite, signalName);
|
|
84
140
|
const hit = findStepByKey(steps, key);
|
|
85
141
|
if (hit) {
|
|
86
|
-
if (hit.error) {
|
|
142
|
+
if (hit.status === 'error' || hit.error) {
|
|
87
143
|
throw Object.assign(new Error(hit.error.message), { name: hit.error.name });
|
|
88
144
|
}
|
|
89
|
-
|
|
145
|
+
if (hit.status === 'finished' || hit.result !== undefined) {
|
|
146
|
+
return hit.result;
|
|
147
|
+
}
|
|
90
148
|
}
|
|
91
149
|
await suspendForSignal(signalName, callsite, meta);
|
|
92
150
|
throw new Error('unreachable');
|
|
@@ -96,7 +154,12 @@ function makeCtx(deps) {
|
|
|
96
154
|
runState: deps.runState,
|
|
97
155
|
runStore: deps.runStore,
|
|
98
156
|
emit,
|
|
99
|
-
toolExecutor
|
|
157
|
+
get toolExecutor() {
|
|
158
|
+
return toolExecutorHolder.executor;
|
|
159
|
+
},
|
|
160
|
+
set toolExecutor(executor) {
|
|
161
|
+
toolExecutorHolder.executor = executor;
|
|
162
|
+
},
|
|
100
163
|
hookRunner: deps.hookRunner ?? {},
|
|
101
164
|
model: deps.model,
|
|
102
165
|
controlModel: deps.controlModel ?? deps.model,
|
|
@@ -121,6 +184,13 @@ function makeCtx(deps) {
|
|
|
121
184
|
resetCallsites: () => {
|
|
122
185
|
effectOrdinal = 0;
|
|
123
186
|
},
|
|
187
|
+
reserveCallsites: (count) => {
|
|
188
|
+
const sites = [];
|
|
189
|
+
for (let i = 0; i < count; i++) {
|
|
190
|
+
sites.push(consumeCallsite());
|
|
191
|
+
}
|
|
192
|
+
return sites;
|
|
193
|
+
},
|
|
124
194
|
tool: async (name, args, options) => {
|
|
125
195
|
// needsApproval gate: a tool flagged `needsApproval` must be approved by a human
|
|
126
196
|
// before it runs. Approval is a durable pause (the `__approval` signal); on resume
|
|
@@ -129,7 +199,7 @@ function makeCtx(deps) {
|
|
|
129
199
|
// ordering is deterministic across replays. NOTE: the surrounding agent turn is not
|
|
130
200
|
// itself a replayable effect — this is fully deterministic for flow `action` tools;
|
|
131
201
|
// for model-issued tool calls, resume re-enters the agent turn.
|
|
132
|
-
const def = options?.def ??
|
|
202
|
+
const def = options?.def ?? toolExecutorHolder.executor.getTool?.(name);
|
|
133
203
|
if (def?.needsApproval) {
|
|
134
204
|
const decision = (await pauseEffect(APPROVAL_SIGNAL, {
|
|
135
205
|
approval: { title: `Approve tool: ${name}` },
|
|
@@ -138,9 +208,12 @@ function makeCtx(deps) {
|
|
|
138
208
|
throw new ToolApprovalDeniedError(name, decision.by);
|
|
139
209
|
}
|
|
140
210
|
}
|
|
141
|
-
const callsite = consumeCallsite();
|
|
142
|
-
const
|
|
143
|
-
|
|
211
|
+
const callsite = options?.callsite ?? consumeCallsite();
|
|
212
|
+
const logicalId = effectRunId();
|
|
213
|
+
const key = def?.idempotencyKey != null
|
|
214
|
+
? def.idempotencyKey(args)
|
|
215
|
+
: idempotencyKey(logicalId, callsite, { name, args });
|
|
216
|
+
const executeTool = () => toolExecutorHolder.executor.execute({
|
|
144
217
|
name,
|
|
145
218
|
args,
|
|
146
219
|
session: deps.session,
|
|
@@ -148,7 +221,12 @@ function makeCtx(deps) {
|
|
|
148
221
|
abortSignal: deps.bargeIn ?? deps.abortSignal,
|
|
149
222
|
def: options?.def,
|
|
150
223
|
toolCtx: options?.toolCtx,
|
|
151
|
-
})
|
|
224
|
+
});
|
|
225
|
+
if (def?.replay === false) {
|
|
226
|
+
const auditKey = `${key}:${steps.length}`;
|
|
227
|
+
return replayOrExecute(auditKey, 'tool', name, executeTool, { index: options?.index });
|
|
228
|
+
}
|
|
229
|
+
return replayOrExecute(key, 'tool', name, executeTool, { index: options?.index });
|
|
152
230
|
},
|
|
153
231
|
approve: async (req) => {
|
|
154
232
|
return pauseEffect(APPROVAL_SIGNAL, { approval: req });
|
|
@@ -161,12 +239,12 @@ function makeCtx(deps) {
|
|
|
161
239
|
},
|
|
162
240
|
now: async () => {
|
|
163
241
|
const callsite = consumeCallsite();
|
|
164
|
-
const key = clockEffectKey(
|
|
242
|
+
const key = clockEffectKey(effectRunId(), callsite, 'now');
|
|
165
243
|
return replayOrExecute(key, 'now', 'now', async () => clock.now());
|
|
166
244
|
},
|
|
167
245
|
uuid: async () => {
|
|
168
246
|
const callsite = consumeCallsite();
|
|
169
|
-
const key = clockEffectKey(
|
|
247
|
+
const key = clockEffectKey(effectRunId(), callsite, 'uuid');
|
|
170
248
|
return replayOrExecute(key, 'uuid', 'uuid', async () => clock.uuid());
|
|
171
249
|
},
|
|
172
250
|
};
|
|
@@ -12,9 +12,25 @@ export declare class SuspendError extends Error {
|
|
|
12
12
|
export declare class RunNotFoundError extends Error {
|
|
13
13
|
constructor(runId: string);
|
|
14
14
|
}
|
|
15
|
+
export declare class StepNotFoundError extends Error {
|
|
16
|
+
constructor(runId: string, key: string);
|
|
17
|
+
}
|
|
18
|
+
export interface StepFinalizePatch {
|
|
19
|
+
status: 'finished' | 'error';
|
|
20
|
+
result?: unknown;
|
|
21
|
+
error?: {
|
|
22
|
+
name: string;
|
|
23
|
+
message: string;
|
|
24
|
+
};
|
|
25
|
+
finishedAt?: number;
|
|
26
|
+
}
|
|
15
27
|
export interface RunStore {
|
|
16
28
|
appendStep(runId: string, record: StepRecord): Promise<void>;
|
|
29
|
+
finalizeStep(runId: string, key: string, patch: StepFinalizePatch): Promise<void>;
|
|
17
30
|
getSteps(runId: string): Promise<StepRecord[]>;
|
|
18
31
|
getRunState(runId: string): Promise<RunState | null>;
|
|
19
32
|
putRunState(state: RunState): Promise<void>;
|
|
33
|
+
initRun?(state: RunState): Promise<void>;
|
|
34
|
+
pruneStepsBeforeEpoch?(runId: string, keepEpoch: number): Promise<void>;
|
|
35
|
+
reserveSteps?(runId: string, count: number): Promise<number[]>;
|
|
20
36
|
}
|
|
@@ -24,3 +24,9 @@ export class RunNotFoundError extends Error {
|
|
|
24
24
|
this.name = 'RunNotFoundError';
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
+
export class StepNotFoundError extends Error {
|
|
28
|
+
constructor(runId, key) {
|
|
29
|
+
super(`Step not found for run ${runId}: ${key}`);
|
|
30
|
+
this.name = 'StepNotFoundError';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -1,14 +1,19 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type SessionStore } from '../../session/SessionStore.js';
|
|
2
2
|
import type { RunState, StepRecord } from './types.js';
|
|
3
|
-
import { type RunStore } from './RunStore.js';
|
|
3
|
+
import { type RunStore, type StepFinalizePatch } from './RunStore.js';
|
|
4
4
|
export declare class SessionRunStore implements RunStore {
|
|
5
5
|
private readonly sessionStore;
|
|
6
6
|
private readonly sessionId;
|
|
7
|
+
private static readonly CAS_RETRIES;
|
|
7
8
|
constructor(sessionStore: SessionStore, sessionId: string);
|
|
9
|
+
private mutateSession;
|
|
8
10
|
appendStep(runId: string, record: StepRecord): Promise<void>;
|
|
11
|
+
finalizeStep(runId: string, key: string, patch: StepFinalizePatch): Promise<void>;
|
|
12
|
+
reserveSteps(runId: string, count: number): Promise<number[]>;
|
|
9
13
|
getSteps(runId: string): Promise<StepRecord[]>;
|
|
10
14
|
getRunState(runId: string): Promise<RunState | null>;
|
|
11
15
|
putRunState(state: RunState): Promise<void>;
|
|
12
16
|
initRun(state: RunState): Promise<void>;
|
|
17
|
+
pruneStepsBeforeEpoch(runId: string, keepEpoch: number): Promise<void>;
|
|
13
18
|
private requireSession;
|
|
14
19
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { StaleWriteError } from '../../session/SessionStore.js';
|
|
1
2
|
import { DURABLE_RUNS_KEY } from './types.js';
|
|
2
|
-
import { LogConflictError, RunNotFoundError } from './RunStore.js';
|
|
3
|
+
import { LogConflictError, RunNotFoundError, StepNotFoundError, } from './RunStore.js';
|
|
3
4
|
function cloneSession(value) {
|
|
4
5
|
try {
|
|
5
6
|
return structuredClone(value);
|
|
@@ -21,28 +22,115 @@ function getPersistedRun(session, runId) {
|
|
|
21
22
|
export class SessionRunStore {
|
|
22
23
|
sessionStore;
|
|
23
24
|
sessionId;
|
|
25
|
+
static CAS_RETRIES = 8;
|
|
24
26
|
constructor(sessionStore, sessionId) {
|
|
25
27
|
this.sessionStore = sessionStore;
|
|
26
28
|
this.sessionId = sessionId;
|
|
27
29
|
}
|
|
28
|
-
async
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
async mutateSession(mutator) {
|
|
31
|
+
for (let attempt = 0; attempt < SessionRunStore.CAS_RETRIES; attempt++) {
|
|
32
|
+
const session = await this.requireSession();
|
|
33
|
+
await mutator(session);
|
|
34
|
+
try {
|
|
35
|
+
await this.sessionStore.save(session);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (error instanceof StaleWriteError && attempt < SessionRunStore.CAS_RETRIES - 1) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
34
44
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
45
|
+
}
|
|
46
|
+
async appendStep(runId, record) {
|
|
47
|
+
await this.mutateSession((session) => {
|
|
48
|
+
const runs = readRuns(session);
|
|
49
|
+
const persisted = runs[runId];
|
|
50
|
+
if (!persisted) {
|
|
51
|
+
throw new RunNotFoundError(runId);
|
|
52
|
+
}
|
|
53
|
+
const existingAtIndex = persisted.steps[record.index];
|
|
54
|
+
if (existingAtIndex?.name === '__reserve') {
|
|
55
|
+
if (persisted.steps.some((step) => step.key === record.key && step.index !== record.index)) {
|
|
56
|
+
throw new LogConflictError(runId, record.index, persisted.steps.length);
|
|
57
|
+
}
|
|
58
|
+
persisted.steps[record.index] = cloneSession(record);
|
|
59
|
+
}
|
|
60
|
+
else if (persisted.steps.length !== record.index) {
|
|
61
|
+
throw new LogConflictError(runId, record.index, persisted.steps.length);
|
|
62
|
+
}
|
|
63
|
+
else if (persisted.steps.some((step) => step.key === record.key)) {
|
|
64
|
+
throw new LogConflictError(runId, record.index, persisted.steps.length);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
persisted.steps.push(cloneSession(record));
|
|
68
|
+
}
|
|
69
|
+
persisted.runState.updatedAt = Date.now();
|
|
70
|
+
runs[runId] = persisted;
|
|
71
|
+
writeRuns(session, runs);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
async finalizeStep(runId, key, patch) {
|
|
75
|
+
await this.mutateSession((session) => {
|
|
76
|
+
const runs = readRuns(session);
|
|
77
|
+
const persisted = runs[runId];
|
|
78
|
+
if (!persisted) {
|
|
79
|
+
throw new RunNotFoundError(runId);
|
|
80
|
+
}
|
|
81
|
+
const stepIndex = persisted.steps.findIndex((step) => step.key === key);
|
|
82
|
+
if (stepIndex === -1) {
|
|
83
|
+
throw new StepNotFoundError(runId, key);
|
|
84
|
+
}
|
|
85
|
+
const existing = persisted.steps[stepIndex];
|
|
86
|
+
persisted.steps[stepIndex] = cloneSession({
|
|
87
|
+
...existing,
|
|
88
|
+
...patch,
|
|
89
|
+
index: existing.index,
|
|
90
|
+
key: existing.key,
|
|
91
|
+
kind: existing.kind,
|
|
92
|
+
name: existing.name,
|
|
93
|
+
startedAt: existing.startedAt,
|
|
94
|
+
epoch: existing.epoch,
|
|
95
|
+
});
|
|
96
|
+
persisted.runState.updatedAt = Date.now();
|
|
97
|
+
runs[runId] = persisted;
|
|
98
|
+
writeRuns(session, runs);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async reserveSteps(runId, count) {
|
|
102
|
+
if (count <= 0) {
|
|
103
|
+
return [];
|
|
40
104
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
105
|
+
let indices = [];
|
|
106
|
+
await this.mutateSession((session) => {
|
|
107
|
+
const runs = readRuns(session);
|
|
108
|
+
const persisted = runs[runId];
|
|
109
|
+
if (!persisted) {
|
|
110
|
+
throw new RunNotFoundError(runId);
|
|
111
|
+
}
|
|
112
|
+
const start = persisted.steps.length;
|
|
113
|
+
const now = Date.now();
|
|
114
|
+
const epoch = persisted.runState.runEpoch ?? 0;
|
|
115
|
+
indices = [];
|
|
116
|
+
for (let i = 0; i < count; i++) {
|
|
117
|
+
const index = start + i;
|
|
118
|
+
indices.push(index);
|
|
119
|
+
persisted.steps.push({
|
|
120
|
+
index,
|
|
121
|
+
key: `__reserve:${runId}:${index}`,
|
|
122
|
+
kind: 'tool',
|
|
123
|
+
name: '__reserve',
|
|
124
|
+
status: 'running',
|
|
125
|
+
startedAt: now,
|
|
126
|
+
epoch,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
persisted.runState.updatedAt = now;
|
|
130
|
+
runs[runId] = persisted;
|
|
131
|
+
writeRuns(session, runs);
|
|
132
|
+
});
|
|
133
|
+
return indices;
|
|
46
134
|
}
|
|
47
135
|
async getSteps(runId) {
|
|
48
136
|
const session = await this.requireSession();
|
|
@@ -58,25 +146,39 @@ export class SessionRunStore {
|
|
|
58
146
|
return persisted ? cloneSession(persisted.runState) : null;
|
|
59
147
|
}
|
|
60
148
|
async putRunState(state) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
149
|
+
await this.mutateSession((session) => {
|
|
150
|
+
const runs = readRuns(session);
|
|
151
|
+
const existing = runs[state.runId];
|
|
152
|
+
runs[state.runId] = {
|
|
153
|
+
runState: cloneSession({ ...state, updatedAt: Date.now() }),
|
|
154
|
+
steps: existing?.steps.map((step) => cloneSession(step)) ?? [],
|
|
155
|
+
};
|
|
156
|
+
writeRuns(session, runs);
|
|
157
|
+
});
|
|
70
158
|
}
|
|
71
159
|
async initRun(state) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
160
|
+
await this.mutateSession((session) => {
|
|
161
|
+
const runs = readRuns(session);
|
|
162
|
+
runs[state.runId] = {
|
|
163
|
+
runState: cloneSession(state),
|
|
164
|
+
steps: [],
|
|
165
|
+
};
|
|
166
|
+
writeRuns(session, runs);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
async pruneStepsBeforeEpoch(runId, keepEpoch) {
|
|
170
|
+
await this.mutateSession((session) => {
|
|
171
|
+
const runs = readRuns(session);
|
|
172
|
+
const persisted = runs[runId];
|
|
173
|
+
if (!persisted) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const kept = persisted.steps.filter((step) => step.epoch === undefined || step.epoch >= keepEpoch);
|
|
177
|
+
persisted.steps = kept.map((step, index) => ({ ...cloneSession(step), index }));
|
|
178
|
+
persisted.runState.updatedAt = Date.now();
|
|
179
|
+
runs[runId] = persisted;
|
|
180
|
+
writeRuns(session, runs);
|
|
181
|
+
});
|
|
80
182
|
}
|
|
81
183
|
async requireSession() {
|
|
82
184
|
const session = await this.sessionStore.get(this.sessionId);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export declare function logicalRunId(runId: string, runEpoch: number | undefined): string;
|
|
1
2
|
export declare function idempotencyKey(runId: string, callsite: string, payload: unknown): string;
|
|
2
3
|
export declare function toolEffectKey(runId: string, callsite: string, name: string, args: unknown): string;
|
|
3
4
|
export declare function pauseEffectKey(runId: string, callsite: string, name: string): string;
|
|
@@ -12,6 +12,9 @@ function stableStringify(value) {
|
|
|
12
12
|
return val;
|
|
13
13
|
});
|
|
14
14
|
}
|
|
15
|
+
export function logicalRunId(runId, runEpoch) {
|
|
16
|
+
return `${runId}#${runEpoch ?? 0}`;
|
|
17
|
+
}
|
|
15
18
|
export function idempotencyKey(runId, callsite, payload) {
|
|
16
19
|
const material = stableStringify({ runId, callsite, payload });
|
|
17
20
|
return createHash('sha256').update(material).digest('hex');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { pauseEffectKey } from './idempotency.js';
|
|
1
|
+
import { pauseEffectKey, logicalRunId } from './idempotency.js';
|
|
2
2
|
export function findStepByKey(steps, key) {
|
|
3
3
|
return steps.find((step) => step.key === key);
|
|
4
4
|
}
|
|
@@ -14,7 +14,10 @@ export async function recordSignalDelivery(runStore, runState, delivery) {
|
|
|
14
14
|
if (!waitingFor || waitingFor.signalName !== delivery.name) {
|
|
15
15
|
throw new Error(`Signal ${delivery.name} does not match waitingFor ${waitingFor?.signalName ?? 'none'}`);
|
|
16
16
|
}
|
|
17
|
-
|
|
17
|
+
// Must match the key pauseEffect used when it suspended: the keystone scopes the
|
|
18
|
+
// effect-key namespace by logical run (runId#epoch), so the resume-side delivery
|
|
19
|
+
// key has to use logicalRunId too, else the pause never finds its own decision.
|
|
20
|
+
const key = pauseEffectKey(logicalRunId(runState.runId, runState.runEpoch), waitingFor.callsite, delivery.name);
|
|
18
21
|
if (findStepByKey(steps, key)) {
|
|
19
22
|
return false;
|
|
20
23
|
}
|
|
@@ -25,9 +28,11 @@ export async function recordSignalDelivery(runStore, runState, delivery) {
|
|
|
25
28
|
kind: waitingFor.approval ? 'approval' : 'signal',
|
|
26
29
|
name: delivery.name,
|
|
27
30
|
signalId: delivery.signalId,
|
|
31
|
+
status: 'finished',
|
|
28
32
|
result: delivery.payload,
|
|
29
33
|
startedAt: now,
|
|
30
34
|
finishedAt: now,
|
|
35
|
+
epoch: runState.runEpoch ?? 0,
|
|
31
36
|
};
|
|
32
37
|
await runStore.appendStep(runState.runId, record);
|
|
33
38
|
runState.waitingFor = undefined;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ModelMessage } from 'ai';
|
|
2
2
|
export type StepKind = 'tool' | 'approval' | 'signal' | 'now' | 'uuid';
|
|
3
|
+
export type StepStatus = 'running' | 'paused' | 'finished' | 'error' | 'aborted';
|
|
3
4
|
export interface StepRecord {
|
|
4
5
|
index: number;
|
|
5
6
|
key: string;
|
|
@@ -11,8 +12,12 @@ export interface StepRecord {
|
|
|
11
12
|
name: string;
|
|
12
13
|
message: string;
|
|
13
14
|
};
|
|
15
|
+
/** Intent lifecycle: `running` = pending execute; `finished`/`error` = finalized. Legacy steps omit status. */
|
|
16
|
+
status?: StepStatus;
|
|
14
17
|
startedAt: number;
|
|
15
18
|
finishedAt?: number;
|
|
19
|
+
/** Logical-run epoch when this step was recorded. Absent on legacy steps → prune keeps them until superseded. */
|
|
20
|
+
epoch?: number;
|
|
16
21
|
}
|
|
17
22
|
interface WaitingFor {
|
|
18
23
|
signalName: string;
|
|
@@ -36,6 +41,12 @@ export interface RunState {
|
|
|
36
41
|
messages: ModelMessage[];
|
|
37
42
|
createdAt: number;
|
|
38
43
|
updatedAt: number;
|
|
44
|
+
/** Monotonic logical-run counter. Increments on each fresh turn; stable across suspend/resume.
|
|
45
|
+
* Scopes the durable effect-key namespace so a new turn re-executes rather than replaying a
|
|
46
|
+
* prior turn's cached result (F6/G8). Absent on legacy runs → treat as 0. */
|
|
47
|
+
runEpoch?: number;
|
|
48
|
+
/** Inbound message idempotency keys already accepted (H2 webhook-retry dedup). */
|
|
49
|
+
processedInboundKeys?: string[];
|
|
39
50
|
}
|
|
40
51
|
export interface SignalDelivery {
|
|
41
52
|
signalId: string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { LanguageModel } from 'ai';
|
|
2
|
+
import type { RunContext } from '../types/run-context.js';
|
|
3
|
+
export declare const GOALS_KEY = "__goals";
|
|
4
|
+
export type GoalStatus = 'open' | 'resolved';
|
|
5
|
+
export interface TrackedGoal {
|
|
6
|
+
topic: string;
|
|
7
|
+
status: GoalStatus;
|
|
8
|
+
lastTurn: number;
|
|
9
|
+
note?: string;
|
|
10
|
+
}
|
|
11
|
+
export type GoalsState = Record<string, unknown>;
|
|
12
|
+
export declare function getGoals(state: GoalsState): TrackedGoal[];
|
|
13
|
+
export declare function addGoal(state: GoalsState, topic: string, lastTurn?: number, note?: string): GoalsState;
|
|
14
|
+
export declare function resolveGoal(state: GoalsState, topic: string, lastTurn?: number): GoalsState;
|
|
15
|
+
export declare function listOpenGoals(state: GoalsState): string[];
|
|
16
|
+
export declare function projectGoalsPrompt(goals: TrackedGoal[]): string;
|
|
17
|
+
export declare function projectGoalsPromptFromState(state: GoalsState): string;
|
|
18
|
+
export declare function updateGoalsFromTurn(ctx: RunContext, model: LanguageModel): Promise<void>;
|