@kuralle-agents/core 0.12.0 → 0.14.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 +89 -4
- package/dist/ai-sdk/uiMessageStream.d.ts +2 -2
- package/dist/ai-sdk/uiMessageStream.js +42 -29
- package/dist/capabilities/LivePromptAssembler.d.ts +1 -1
- package/dist/capabilities/LivePromptAssembler.js +2 -1
- package/dist/capabilities/index.d.ts +1 -1
- package/dist/eval/EvalRunner.js +4 -4
- package/dist/eval/simulation.js +4 -3
- package/dist/events/TurnHandle.d.ts +3 -3
- package/dist/flow/collectDigression.d.ts +5 -2
- package/dist/flow/collectDigression.js +48 -10
- package/dist/flow/collectUntilComplete.js +13 -5
- package/dist/flow/degrade.js +8 -4
- package/dist/flow/emitInteractive.d.ts +2 -2
- package/dist/flow/emitInteractive.js +6 -3
- package/dist/flow/extraction.d.ts +3 -2
- package/dist/flow/extraction.js +14 -4
- package/dist/flow/reduceTransition.d.ts +2 -2
- package/dist/flow/reduceTransition.js +7 -3
- package/dist/flow/runFlow.js +97 -17
- package/dist/foundation/AgentStateController.d.ts +1 -1
- package/dist/foundation/ToolExecutor.d.ts +2 -2
- package/dist/foundation/index.d.ts +0 -11
- package/dist/foundation/index.js +0 -5
- package/dist/index.d.ts +15 -15
- package/dist/index.js +9 -6
- package/dist/outcomes/streamPart.d.ts +2 -2
- package/dist/outcomes/streamPart.js +2 -3
- package/dist/prompts/PromptBuilder.d.ts +3 -3
- package/dist/prompts/PromptBuilder.js +13 -6
- package/dist/prompts/index.d.ts +1 -1
- package/dist/prompts/types.d.ts +9 -2
- package/dist/runtime/InMemoryRetrievalCache.d.ts +25 -0
- package/dist/runtime/InMemoryRetrievalCache.js +59 -0
- package/dist/runtime/KnowledgeProvider.d.ts +3 -2
- package/dist/runtime/KnowledgeProvider.js +60 -31
- package/dist/runtime/Runtime.d.ts +47 -1
- package/dist/runtime/Runtime.js +273 -36
- package/dist/runtime/TokenAccumulator.d.ts +7 -0
- package/dist/runtime/TokenAccumulator.js +7 -0
- package/dist/runtime/TraceRecorder.d.ts +51 -0
- package/dist/runtime/TraceRecorder.js +326 -0
- package/dist/runtime/buildAgentToolSurface.js +1 -1
- package/dist/runtime/channels/TextDriver.js +57 -31
- package/dist/runtime/channels/executeModelTool.d.ts +14 -1
- package/dist/runtime/channels/executeModelTool.js +132 -3
- package/dist/runtime/channels/extractionTurn.js +1 -1
- package/dist/runtime/channels/index.d.ts +0 -3
- package/dist/runtime/channels/index.js +0 -6
- package/dist/runtime/channels/inputBuffer.d.ts +1 -0
- package/dist/runtime/channels/inputBuffer.js +9 -0
- package/dist/runtime/channels/streaming/hostControlSpeak.js +5 -1
- package/dist/runtime/channels/streaming/speakGated.js +24 -12
- package/dist/runtime/citations/index.d.ts +3 -3
- package/dist/runtime/closeRun.js +14 -10
- package/dist/runtime/compaction.d.ts +2 -0
- package/dist/runtime/compaction.js +3 -2
- package/dist/runtime/controlFlowSignal.d.ts +24 -0
- package/dist/runtime/controlFlowSignal.js +29 -0
- package/dist/runtime/ctx.d.ts +3 -4
- package/dist/runtime/ctx.js +147 -60
- package/dist/runtime/durable/RunStore.d.ts +16 -0
- package/dist/runtime/durable/RunStore.js +7 -1
- 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/gather.d.ts +1 -1
- package/dist/runtime/grounding/inMemoryKnowledge.d.ts +1 -1
- package/dist/runtime/grounding/knowledge.d.ts +1 -1
- 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 +37 -6
- package/dist/runtime/index.d.ts +4 -3
- package/dist/runtime/index.js +2 -1
- package/dist/runtime/openRun.d.ts +2 -0
- package/dist/runtime/openRun.js +46 -17
- package/dist/runtime/outcomeMarking.d.ts +2 -2
- package/dist/runtime/outcomeMarking.js +2 -1
- package/dist/runtime/policies/agentTurn.d.ts +1 -1
- package/dist/runtime/policies/limits.d.ts +1 -0
- package/dist/runtime/policies/limits.js +3 -0
- package/dist/runtime/runHookSafely.d.ts +3 -0
- package/dist/runtime/runHookSafely.js +19 -0
- 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/scheduler/index.d.ts +2 -2
- package/dist/scheduler/index.js +1 -1
- package/dist/services/TracingService.js +2 -3
- 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/skills/collectSkills.d.ts +11 -3
- package/dist/skills/collectSkills.js +51 -8
- package/dist/skills/compositeSkillStore.d.ts +26 -0
- package/dist/skills/compositeSkillStore.js +54 -0
- package/dist/skills/defineSkill.d.ts +21 -0
- package/dist/skills/defineSkill.js +26 -0
- package/dist/skills/fsSkillStore.d.ts +3 -0
- package/dist/skills/fsSkillStore.js +79 -0
- package/dist/skills/index.d.ts +4 -0
- package/dist/skills/index.js +4 -0
- package/dist/skills/parseSkillFrontmatter.d.ts +14 -0
- package/dist/skills/parseSkillFrontmatter.js +183 -0
- package/dist/skills/wireAgentSkills.d.ts +2 -1
- package/dist/skills/wireAgentSkills.js +2 -2
- package/dist/testing/mocks.d.ts +3 -3
- package/dist/testing/mocks.js +7 -3
- package/dist/tools/controlResults.d.ts +14 -0
- package/dist/tools/controlResults.js +15 -0
- package/dist/tools/effect/ToolExecutor.d.ts +6 -0
- package/dist/tools/effect/ToolExecutor.js +75 -36
- package/dist/tools/effect/defineTool.d.ts +11 -0
- package/dist/tools/effect/defineTool.js +3 -0
- package/dist/tools/effect/errors.d.ts +2 -5
- package/dist/tools/effect/errors.js +2 -5
- package/dist/tools/index.d.ts +0 -1
- package/dist/tools/index.js +0 -1
- 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 +2 -1
- package/dist/types/channel.d.ts +14 -1
- package/dist/types/effectTool.d.ts +6 -0
- package/dist/types/flow.d.ts +1 -1
- package/dist/types/guardrails.d.ts +5 -0
- package/dist/types/hooks.d.ts +2 -2
- package/dist/types/index.d.ts +25 -15
- package/dist/types/index.js +8 -12
- package/dist/types/knowledge.d.ts +213 -0
- package/dist/types/processors.d.ts +0 -58
- package/dist/types/run-context.d.ts +16 -7
- package/dist/types/runtime.d.ts +3 -74
- package/dist/types/session.d.ts +11 -1
- package/dist/types/skills.d.ts +15 -1
- package/dist/types/stream.d.ts +138 -68
- package/dist/types/stream.js +34 -1
- package/dist/types/trace.d.ts +52 -0
- package/guides/AGENTS.md +3 -3
- package/guides/EXAMPLE_VERIFICATION.md +4 -4
- package/guides/FLOWS.md +1 -1
- package/guides/GETTING_STARTED.md +1 -1
- package/guides/RUNTIME.md +20 -15
- package/guides/TOOLS.md +1 -1
- package/package.json +13 -14
- package/dist/foundation/AgentDefinition.d.ts +0 -17
- package/dist/foundation/ConversationEventLog.d.ts +0 -71
- package/dist/foundation/ConversationState.d.ts +0 -33
- package/dist/foundation/ConversationState.js +0 -1
- package/dist/foundation/DefaultConversationEventLog.d.ts +0 -27
- package/dist/foundation/DefaultConversationEventLog.js +0 -205
- package/dist/foundation/DefaultConversationState.d.ts +0 -36
- package/dist/foundation/DefaultConversationState.js +0 -103
- package/dist/foundation/DefaultToolExecutor.d.ts +0 -57
- package/dist/foundation/DefaultToolExecutor.js +0 -132
- package/dist/foundation/createFoundation.d.ts +0 -32
- package/dist/foundation/createFoundation.js +0 -33
- package/dist/hooks/HookRunner.d.ts +0 -37
- package/dist/hooks/HookRunner.js +0 -111
- package/dist/hooks/builtin/logging.d.ts +0 -5
- package/dist/hooks/builtin/logging.js +0 -98
- package/dist/hooks/builtin/metrics.d.ts +0 -7
- package/dist/hooks/builtin/metrics.js +0 -60
- package/dist/hooks/builtin/observability.d.ts +0 -20
- package/dist/hooks/builtin/observability.js +0 -572
- package/dist/hooks/helpers.d.ts +0 -22
- package/dist/hooks/helpers.js +0 -183
- package/dist/hooks/index.d.ts +0 -8
- package/dist/hooks/index.js +0 -6
- package/dist/realtime/RealtimeAudioClient.d.ts +0 -166
- package/dist/realtime/RealtimeAudioClient.js +0 -14
- package/dist/realtime/RealtimeSessionHandle.d.ts +0 -6
- package/dist/realtime/RealtimeSessionHandle.js +0 -1
- package/dist/realtime/index.d.ts +0 -2
- package/dist/realtime/index.js +0 -1
- package/dist/runtime/channels/VoiceDriver.d.ts +0 -31
- package/dist/runtime/channels/VoiceDriver.js +0 -321
- package/dist/runtime/channels/voiceTools.d.ts +0 -9
- package/dist/runtime/channels/voiceTools.js +0 -51
- package/dist/tools/errorHandling.d.ts +0 -35
- package/dist/tools/errorHandling.js +0 -145
- package/dist/types/voice.d.ts +0 -517
- package/dist/types/voice.js +0 -14
- package/dist/utils/isRecord.d.ts +0 -1
- package/dist/utils/isRecord.js +0 -3
- package/dist/utils/messageNormalization.d.ts +0 -2
- package/dist/utils/messageNormalization.js +0 -137
- /package/dist/{foundation/AgentDefinition.js → types/knowledge.js} +0 -0
- /package/dist/{foundation/ConversationEventLog.js → types/trace.js} +0 -0
package/dist/runtime/hostLoop.js
CHANGED
|
@@ -9,6 +9,7 @@ import { hasHostControlTargets } from './hostControlTools.js';
|
|
|
9
9
|
import { isValidControl, resolveHostControl, startHostControlGuard, } from './hostControlGuard.js';
|
|
10
10
|
import { resolveDispatchMode, isAdvisoryDispatch } from './dispatchMode.js';
|
|
11
11
|
import { adaptHostSelect } from './hostClassifyAdapter.js';
|
|
12
|
+
import { persistTurnUsageFromTurn } from './turnTokenUsage.js';
|
|
12
13
|
export async function hostLoop(options) {
|
|
13
14
|
const { agent, run, driver, ctx } = options;
|
|
14
15
|
const classify = options.classify ??
|
|
@@ -35,7 +36,7 @@ export async function hostLoop(options) {
|
|
|
35
36
|
return { kind: 'paused' };
|
|
36
37
|
}
|
|
37
38
|
if (error instanceof LimitsExceededError) {
|
|
38
|
-
ctx.emit({ type: 'error', error: error.message });
|
|
39
|
+
ctx.emit({ channel: 'client', type: 'error', payload: { error: error.message } });
|
|
39
40
|
return { kind: 'ended', reason: error.message };
|
|
40
41
|
}
|
|
41
42
|
throw error;
|
|
@@ -67,6 +68,12 @@ async function runActiveFlow(flow, run, driver, ctx, agent) {
|
|
|
67
68
|
ctx.resetCallsites();
|
|
68
69
|
const result = await runFlow(flow, run, driver, ctx, agent);
|
|
69
70
|
if (result.kind === 'handoff') {
|
|
71
|
+
// A handoff fired from inside a flow: the source flow is abandoned for this turn.
|
|
72
|
+
// Clear the active-flow pointers so the target agent does not try (and fail) to
|
|
73
|
+
// resume a flow that belongs to the source agent (G17).
|
|
74
|
+
run.activeFlow = undefined;
|
|
75
|
+
run.activeNode = undefined;
|
|
76
|
+
await ctx.runStore.putRunState(run);
|
|
70
77
|
return { kind: 'handoff', to: result.to, reason: result.reason };
|
|
71
78
|
}
|
|
72
79
|
if (result.kind === 'awaitingUser') {
|
|
@@ -111,12 +118,16 @@ async function runFreeConversation(agent, run, driver, ctx, classify) {
|
|
|
111
118
|
resolved.hostControl = { dispatchMode, advisoryDispatch };
|
|
112
119
|
}
|
|
113
120
|
const turn = await driver.runAgentTurn(resolved, ctx);
|
|
121
|
+
await persistTurnUsageFromTurn(ctx, turn);
|
|
114
122
|
if (turn.control && isValidControl(turn.control, agent, run)) {
|
|
115
123
|
emitHostGuardTelemetry(ctx, { invoked: false, reason: 'main-control' });
|
|
116
124
|
return await executeHostControl(agent, run, driver, ctx, turn.control);
|
|
117
125
|
}
|
|
118
126
|
if (turn.text.trim()) {
|
|
119
127
|
emitHostGuardTelemetry(ctx, { invoked: false, reason: 'answered' });
|
|
128
|
+
if (turn.toolMessages?.length) {
|
|
129
|
+
run.messages = [...run.messages, ...turn.toolMessages];
|
|
130
|
+
}
|
|
120
131
|
const message = { role: 'assistant', content: turn.text };
|
|
121
132
|
run.messages = [...run.messages, message];
|
|
122
133
|
await ctx.runStore.putRunState(run);
|
|
@@ -149,7 +160,11 @@ function guardVerdictToTelemetryVerdict(verdict) {
|
|
|
149
160
|
return 'keep';
|
|
150
161
|
}
|
|
151
162
|
function emitHostGuardTelemetry(ctx, data) {
|
|
152
|
-
ctx.emit({
|
|
163
|
+
ctx.emit({
|
|
164
|
+
channel: 'internal',
|
|
165
|
+
type: 'custom',
|
|
166
|
+
payload: { name: 'host-guard', data },
|
|
167
|
+
});
|
|
153
168
|
}
|
|
154
169
|
function guardVerdictToControl(verdict, agent) {
|
|
155
170
|
const selection = verdictToSelection(verdict, agent);
|
|
@@ -163,7 +178,11 @@ function guardVerdictToControl(verdict, agent) {
|
|
|
163
178
|
}
|
|
164
179
|
async function executeHostControl(agent, run, driver, ctx, control) {
|
|
165
180
|
if (!control) {
|
|
166
|
-
ctx.emit({
|
|
181
|
+
ctx.emit({
|
|
182
|
+
channel: 'client',
|
|
183
|
+
type: 'error',
|
|
184
|
+
payload: { error: 'No valid host control target resolved' },
|
|
185
|
+
});
|
|
167
186
|
return { kind: 'ended', reason: 'dispatch_failed' };
|
|
168
187
|
}
|
|
169
188
|
if (control.type === 'enterFlow') {
|
|
@@ -171,18 +190,30 @@ async function executeHostControl(agent, run, driver, ctx, control) {
|
|
|
171
190
|
if (flow) {
|
|
172
191
|
return await runActiveFlow(flow, run, driver, ctx, agent);
|
|
173
192
|
}
|
|
174
|
-
ctx.emit({
|
|
193
|
+
ctx.emit({
|
|
194
|
+
channel: 'client',
|
|
195
|
+
type: 'error',
|
|
196
|
+
payload: { error: `Flow not found: ${control.flowName}` },
|
|
197
|
+
});
|
|
175
198
|
return { kind: 'ended', reason: 'flow_not_found' };
|
|
176
199
|
}
|
|
177
200
|
if (control.type === 'handoff') {
|
|
178
|
-
ctx.emit({
|
|
201
|
+
ctx.emit({
|
|
202
|
+
channel: 'internal',
|
|
203
|
+
type: 'handoff',
|
|
204
|
+
payload: { targetAgent: control.target, reason: control.reason },
|
|
205
|
+
});
|
|
179
206
|
return { kind: 'handoff', to: control.target, reason: control.reason };
|
|
180
207
|
}
|
|
181
208
|
if (control.type === 'end') {
|
|
182
209
|
return { kind: 'ended', reason: control.reason };
|
|
183
210
|
}
|
|
184
211
|
if (control.type === 'escalate') {
|
|
185
|
-
|
|
212
|
+
// No handoff part is emitted here. Runtime owns it: it emits one for any
|
|
213
|
+
// target in `terminalHandoffTargets` (default ['human']) on this exact
|
|
214
|
+
// return value. Emitting here too produced two handoff spans per escalation
|
|
215
|
+
// and a meaningless `human -> human` self-edge, which then mis-attributed
|
|
216
|
+
// every later span in the turn to `human`.
|
|
186
217
|
return { kind: 'handoff', to: 'human', reason: control.reason, category: control.category };
|
|
187
218
|
}
|
|
188
219
|
if (control.type === 'recover') {
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
export { createRuntime, Runtime, type HarnessConfig, type RunOptions, } from './Runtime.js';
|
|
1
|
+
export { createRuntime, Runtime, type HarnessConfig, type RunOptions, type TracingConfig, } from './Runtime.js';
|
|
2
2
|
export type { RuntimeLike } from './RuntimeLike.js';
|
|
3
|
+
export { TraceRecorder, runOnce, type TraceRecorderOptions } from './TraceRecorder.js';
|
|
4
|
+
export type { AgentSpan, AgentTrace, SpanKind } from '../types/trace.js';
|
|
3
5
|
export { SessionWorkingMemory } from './WorkingMemory.js';
|
|
4
|
-
export { TextDriver
|
|
5
|
-
export type { VoiceDriverConfig } from './channels/index.js';
|
|
6
|
+
export { TextDriver } from './channels/index.js';
|
|
6
7
|
export { setPendingUserInput, consumePendingUserInput, consumeAllPendingUserInput, peekPendingUserInput, hasPendingUserInput, } from './channels/index.js';
|
|
7
8
|
export { userInputToText, mergeUserInputContents, hasMediaParts, transcribeAudioParts, type UserInputContent, } from './userInput.js';
|
|
8
9
|
export { isAbortSignal, type InterruptionEvent, type AbortOptions, type CancellationReason, } from '../types/index.js';
|
package/dist/runtime/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { createRuntime, Runtime, } from './Runtime.js';
|
|
2
|
+
export { TraceRecorder, runOnce } from './TraceRecorder.js';
|
|
2
3
|
export { SessionWorkingMemory } from './WorkingMemory.js';
|
|
3
|
-
export { TextDriver
|
|
4
|
+
export { TextDriver } from './channels/index.js';
|
|
4
5
|
// Pending-input buffer helpers — required by custom ChannelDriver authors to
|
|
5
6
|
// implement awaitUser the same FIFO-aware way the built-in drivers do (the
|
|
6
7
|
// buffer is an ordered queue since 0.3.13/H3, not a single slot).
|
|
@@ -21,6 +21,8 @@ export interface OpenRunOptions {
|
|
|
21
21
|
seedMessages?: ModelMessage[];
|
|
22
22
|
historyDelta?: ModelMessage[];
|
|
23
23
|
signalDelivery?: SignalDelivery;
|
|
24
|
+
/** Stable key for this inbound user message; duplicate webhook retries are ignored (H2). */
|
|
25
|
+
idempotencyKey?: string;
|
|
24
26
|
transcriptionModel?: TranscriptionModel;
|
|
25
27
|
defaultAgentId: string;
|
|
26
28
|
sessionStore: SessionStore;
|
package/dist/runtime/openRun.js
CHANGED
|
@@ -3,6 +3,8 @@ import { transcribeAudioParts } from './userInput.js';
|
|
|
3
3
|
import { setPendingUserInput } from './channels/inputBuffer.js';
|
|
4
4
|
import { SessionRunStore } from './durable/SessionRunStore.js';
|
|
5
5
|
import { recordSignalDelivery } from './durable/replay.js';
|
|
6
|
+
import { resetTurnCount } from './policies/limits.js';
|
|
7
|
+
import { mutateSessionWithRetry } from '../session/utils.js';
|
|
6
8
|
export function sessionDerivedRunId(sessionId) {
|
|
7
9
|
return sessionId;
|
|
8
10
|
}
|
|
@@ -26,16 +28,18 @@ export async function openRun(agentsById, options) {
|
|
|
26
28
|
};
|
|
27
29
|
await runStore.initRun(runState);
|
|
28
30
|
if (initialMessages.length > 0) {
|
|
29
|
-
session.
|
|
30
|
-
|
|
31
|
+
await mutateSessionWithRetry(options.sessionStore, session.id, (latest) => {
|
|
32
|
+
latest.messages = [...initialMessages];
|
|
33
|
+
});
|
|
31
34
|
}
|
|
32
35
|
}
|
|
33
36
|
if (options.historyDelta?.length) {
|
|
34
37
|
runState.messages = [...runState.messages, ...options.historyDelta];
|
|
35
38
|
runState.updatedAt = Date.now();
|
|
36
39
|
await runStore.putRunState(runState);
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
await mutateSessionWithRetry(options.sessionStore, session.id, (latest) => {
|
|
41
|
+
latest.messages = [...latest.messages, ...options.historyDelta];
|
|
42
|
+
});
|
|
39
43
|
}
|
|
40
44
|
if (options.signalDelivery) {
|
|
41
45
|
await recordSignalDelivery(runStore, runState, options.signalDelivery);
|
|
@@ -53,22 +57,47 @@ export async function openRun(agentsById, options) {
|
|
|
53
57
|
const hasInput = typeof effectiveInput === 'string'
|
|
54
58
|
? effectiveInput.length > 0
|
|
55
59
|
: Array.isArray(effectiveInput) && effectiveInput.length > 0;
|
|
60
|
+
const isResume = Boolean(options.signalDelivery);
|
|
61
|
+
const isFlowContinuation = Boolean(runState.activeFlow);
|
|
62
|
+
const isFreshLogicalRun = (hasInput || Boolean(options.wake)) && !isResume && !isFlowContinuation;
|
|
63
|
+
if (isFreshLogicalRun) {
|
|
64
|
+
runState.runEpoch = (runState.runEpoch ?? 0) + 1;
|
|
65
|
+
await runStore.pruneStepsBeforeEpoch(runId, runState.runEpoch);
|
|
66
|
+
resetTurnCount(runState);
|
|
67
|
+
if (Array.isArray(runState.state.__completedFlows)) {
|
|
68
|
+
runState.state.__completedFlows = [];
|
|
69
|
+
}
|
|
70
|
+
runState.updatedAt = Date.now();
|
|
71
|
+
await runStore.putRunState(runState);
|
|
72
|
+
}
|
|
56
73
|
if (hasInput && effectiveInput !== undefined) {
|
|
74
|
+
if (options.idempotencyKey) {
|
|
75
|
+
const processed = runState.processedInboundKeys ?? [];
|
|
76
|
+
if (processed.includes(options.idempotencyKey)) {
|
|
77
|
+
const agent = agentsById.get(runState.activeAgentId);
|
|
78
|
+
if (!agent) {
|
|
79
|
+
throw new Error(`Unknown activeAgentId "${runState.activeAgentId}"`);
|
|
80
|
+
}
|
|
81
|
+
const latestSession = (await options.sessionStore.get(options.sessionId)) ?? session;
|
|
82
|
+
return { session: latestSession, runState, runStore, agent };
|
|
83
|
+
}
|
|
84
|
+
runState.processedInboundKeys = [...processed, options.idempotencyKey];
|
|
85
|
+
}
|
|
57
86
|
runState.updatedAt = Date.now();
|
|
58
87
|
if (runState.activeFlow) {
|
|
59
88
|
await runStore.putRunState(runState);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
89
|
+
await mutateSessionWithRetry(options.sessionStore, session.id, (latest) => {
|
|
90
|
+
setPendingUserInput(latest, effectiveInput);
|
|
91
|
+
});
|
|
63
92
|
}
|
|
64
93
|
else {
|
|
65
94
|
const userMessage = { role: 'user', content: effectiveInput };
|
|
66
95
|
runState.messages = [...runState.messages, userMessage];
|
|
67
96
|
runState.updatedAt = Date.now();
|
|
68
97
|
await runStore.putRunState(runState);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
98
|
+
await mutateSessionWithRetry(options.sessionStore, session.id, (latest) => {
|
|
99
|
+
latest.messages = [...latest.messages, userMessage];
|
|
100
|
+
});
|
|
72
101
|
}
|
|
73
102
|
}
|
|
74
103
|
if (options.wake && !hasInput) {
|
|
@@ -84,18 +113,18 @@ export async function openRun(agentsById, options) {
|
|
|
84
113
|
runState.messages = [...runState.messages, wakeMessage];
|
|
85
114
|
runState.updatedAt = Date.now();
|
|
86
115
|
await runStore.putRunState(runState);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
116
|
+
await mutateSessionWithRetry(options.sessionStore, session.id, (latest) => {
|
|
117
|
+
latest.messages = [...latest.messages, wakeMessage];
|
|
118
|
+
});
|
|
90
119
|
}
|
|
91
120
|
const agent = agentsById.get(runState.activeAgentId);
|
|
92
121
|
if (!agent) {
|
|
93
122
|
throw new Error(`Unknown activeAgentId "${runState.activeAgentId}"`);
|
|
94
123
|
}
|
|
95
|
-
const latestSession =
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
124
|
+
const latestSession = await mutateSessionWithRetry(options.sessionStore, session.id, (latest) => {
|
|
125
|
+
latest.currentAgent = runState.activeAgentId;
|
|
126
|
+
latest.activeAgentId = runState.activeAgentId;
|
|
127
|
+
});
|
|
99
128
|
return { session: latestSession, runState, runStore, agent };
|
|
100
129
|
}
|
|
101
130
|
async function loadOrCreateSession(options) {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { Session } from '../types/session.js';
|
|
2
2
|
import type { SessionStore } from '../session/SessionStore.js';
|
|
3
3
|
import type { ConversationOutcome, ConversationOutcomeMarkedBy, ConversationOutcomeRecord } from '../outcomes/types.js';
|
|
4
|
-
import type {
|
|
4
|
+
import type { StreamPart } from '../types/stream.js';
|
|
5
5
|
export declare function isTerminalOutcome(outcome: ConversationOutcome): boolean;
|
|
6
6
|
export declare function markSessionOutcome(sessionStore: SessionStore, session: Session, outcome: ConversationOutcome, opts?: {
|
|
7
7
|
reason?: string;
|
|
8
8
|
markedBy?: ConversationOutcomeMarkedBy;
|
|
9
|
-
}, emit?: (part:
|
|
9
|
+
}, emit?: (part: StreamPart) => void): Promise<ConversationOutcomeRecord>;
|
|
@@ -22,8 +22,9 @@ export async function markSessionOutcome(sessionStore, session, outcome, opts =
|
|
|
22
22
|
await sessionStore.save(session);
|
|
23
23
|
if (emit) {
|
|
24
24
|
emit({
|
|
25
|
+
channel: 'client',
|
|
25
26
|
type: 'conversation-outcome',
|
|
26
|
-
outcome: record.outcome,
|
|
27
|
+
payload: { outcome: record.outcome },
|
|
27
28
|
});
|
|
28
29
|
}
|
|
29
30
|
return record;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { TurnControl } from '../../types/channel.js';
|
|
2
2
|
import type { ToolCallRecord } from '../../types/session.js';
|
|
3
|
-
import type { SourceRef } from '../../types/
|
|
3
|
+
import type { SourceRef } from '../../types/knowledge.js';
|
|
4
4
|
import type { RunContext } from '../../types/run-context.js';
|
|
5
5
|
export interface PreTurnResult {
|
|
6
6
|
proceed: boolean;
|
|
@@ -4,5 +4,6 @@ export declare class LimitsExceededError extends Error {
|
|
|
4
4
|
constructor(message: string);
|
|
5
5
|
}
|
|
6
6
|
export declare function incrementTurnCount(run: RunState): number;
|
|
7
|
+
export declare function resetTurnCount(run: RunState): void;
|
|
7
8
|
export declare function assertWithinTurnLimit(run: RunState, limits?: Limits): void;
|
|
8
9
|
export declare function resolveMaxSteps(limits: Limits | undefined, fallback: number): number;
|
|
@@ -15,6 +15,9 @@ export function incrementTurnCount(run) {
|
|
|
15
15
|
run.updatedAt = Date.now();
|
|
16
16
|
return next;
|
|
17
17
|
}
|
|
18
|
+
export function resetTurnCount(run) {
|
|
19
|
+
run.state[TURN_COUNT_KEY] = 0;
|
|
20
|
+
}
|
|
18
21
|
export function assertWithinTurnLimit(run, limits) {
|
|
19
22
|
const maxTurns = limits?.maxTurns;
|
|
20
23
|
if (maxTurns == null) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function runHookSafely(name, callback) {
|
|
2
|
+
if (!callback)
|
|
3
|
+
return Promise.resolve();
|
|
4
|
+
try {
|
|
5
|
+
return Promise.resolve(callback()).catch((error) => reportHookError(name, error));
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
reportHookError(name, error);
|
|
9
|
+
return Promise.resolve();
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function reportHookError(name, error) {
|
|
13
|
+
try {
|
|
14
|
+
console.error(`Hook ${name} failed`, error);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// Hook failure reporting must not affect run correctness.
|
|
18
|
+
}
|
|
19
|
+
}
|
package/dist/runtime/select.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LanguageModel } from 'ai';
|
|
1
|
+
import type { LanguageModel, ModelMessage } from 'ai';
|
|
2
2
|
import type { AgentConfig } from '../types/agentConfig.js';
|
|
3
3
|
import type { Flow } from '../types/flow.js';
|
|
4
4
|
import type { RunState } from './durable/types.js';
|
|
@@ -20,12 +20,15 @@ export interface HostGuardVerdict {
|
|
|
20
20
|
reason?: string;
|
|
21
21
|
confidence?: number;
|
|
22
22
|
}
|
|
23
|
+
export declare const DEFAULT_CLASSIFIER_CONTEXT_MESSAGES = 6;
|
|
23
24
|
export interface ClassifyHostOptions {
|
|
24
25
|
agent: AgentConfig;
|
|
25
26
|
run: RunState;
|
|
26
27
|
model: LanguageModel;
|
|
27
28
|
allowKeep: boolean;
|
|
28
29
|
excludeFlowNames?: string[];
|
|
30
|
+
/** How many recent messages to include in the classifier prompt. Default: 6. */
|
|
31
|
+
contextMessageLimit?: number;
|
|
29
32
|
}
|
|
30
33
|
export declare function classifyHostTarget(options: ClassifyHostOptions): Promise<HostGuardVerdict>;
|
|
31
34
|
/** @deprecated Use classifyHostTarget — kept as alias for test injection. */
|
|
@@ -33,4 +36,5 @@ export declare function selectHostTarget(options: Omit<ClassifyHostOptions, 'all
|
|
|
33
36
|
alwaysRoute?: boolean;
|
|
34
37
|
}): Promise<HostSelection>;
|
|
35
38
|
export declare function verdictToSelection(verdict: HostGuardVerdict, agent: AgentConfig): HostSelection | undefined;
|
|
39
|
+
export declare function formatRecentConversation(messages: ModelMessage[], limit?: number): string;
|
|
36
40
|
export { availableHostFlows };
|
package/dist/runtime/select.js
CHANGED
|
@@ -14,11 +14,13 @@ const dispatcherSchema = z.object({
|
|
|
14
14
|
agentId: z.union([z.string(), z.null()]),
|
|
15
15
|
reason: z.union([z.string(), z.null()]),
|
|
16
16
|
});
|
|
17
|
+
export const DEFAULT_CLASSIFIER_CONTEXT_MESSAGES = 6;
|
|
17
18
|
export async function classifyHostTarget(options) {
|
|
18
19
|
const { agent, run, model, allowKeep } = options;
|
|
19
20
|
const flows = agent.flows ?? [];
|
|
20
21
|
const routes = agent.routes ?? [];
|
|
21
22
|
const latestUser = latestUserMessage(run.messages);
|
|
23
|
+
const recentContext = formatRecentConversation(run.messages, options.contextMessageLimit ?? DEFAULT_CLASSIFIER_CONTEXT_MESSAGES);
|
|
22
24
|
if (!latestUser) {
|
|
23
25
|
return { action: 'keep', confidence: 1 };
|
|
24
26
|
}
|
|
@@ -51,7 +53,9 @@ export async function classifyHostTarget(options) {
|
|
|
51
53
|
system: 'You are an internal routing classifier. Choose exactly one action. ' +
|
|
52
54
|
'Output schema fields only — never user-facing prose. ' +
|
|
53
55
|
'Reason over semantic descriptions only; never match keywords or substrings.',
|
|
54
|
-
prompt:
|
|
56
|
+
prompt: (recentContext
|
|
57
|
+
? `Recent conversation:\n${recentContext}\n\n`
|
|
58
|
+
: `User message:\n${latestUser}\n\n`) +
|
|
55
59
|
(completedFlows.length > 0 ? `Completed flows: ${completedFlows.join(', ')}\n\n` : '') +
|
|
56
60
|
(flowLines ? `Available flows:\n${flowLines}\n\n` : '') +
|
|
57
61
|
(routeLines ? `Routes:\n${routeLines}\n\n` : '') +
|
|
@@ -159,6 +163,31 @@ function formatRouteLine(route, index) {
|
|
|
159
163
|
const target = route.agent ? `agent "${route.agent}"` : route.flow ? `flow "${route.flow}"` : 'keep';
|
|
160
164
|
return `- route ${index + 1} → ${target} when: ${route.when}`;
|
|
161
165
|
}
|
|
166
|
+
function formatMessageContent(message) {
|
|
167
|
+
if (typeof message.content === 'string') {
|
|
168
|
+
return message.content;
|
|
169
|
+
}
|
|
170
|
+
if (Array.isArray(message.content)) {
|
|
171
|
+
return message.content
|
|
172
|
+
.filter((part) => part.type === 'text')
|
|
173
|
+
.map((part) => part.text)
|
|
174
|
+
.join('');
|
|
175
|
+
}
|
|
176
|
+
return '';
|
|
177
|
+
}
|
|
178
|
+
export function formatRecentConversation(messages, limit = DEFAULT_CLASSIFIER_CONTEXT_MESSAGES) {
|
|
179
|
+
const slice = messages.slice(-limit);
|
|
180
|
+
return slice
|
|
181
|
+
.map((message) => {
|
|
182
|
+
const content = formatMessageContent(message);
|
|
183
|
+
if (!content.trim()) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
return `${message.role}: ${content}`;
|
|
187
|
+
})
|
|
188
|
+
.filter((line) => line != null)
|
|
189
|
+
.join('\n');
|
|
190
|
+
}
|
|
162
191
|
function latestUserMessage(messages) {
|
|
163
192
|
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
164
193
|
const message = messages[i];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { TurnResult } from '../types/channel.js';
|
|
2
|
+
import type { RunContext } from '../types/run-context.js';
|
|
3
|
+
export declare const TOKEN_USAGE_STATE_KEY = "__tokenUsage";
|
|
4
|
+
export declare const LAST_PROMPT_TOKENS_KEY = "__lastPromptTokens";
|
|
5
|
+
export interface PersistedTokenUsage {
|
|
6
|
+
inputTokens: number;
|
|
7
|
+
outputTokens: number;
|
|
8
|
+
totalTokens: number;
|
|
9
|
+
cacheReadTokens?: number;
|
|
10
|
+
}
|
|
11
|
+
/** Record real turn usage onto run state (survives across turns via persistence). */
|
|
12
|
+
export declare function persistTurnUsageFromTurn(ctx: RunContext, turn: TurnResult): Promise<void>;
|
|
13
|
+
export declare function readLastPromptTokens(state: Record<string, unknown>): number | undefined;
|
|
14
|
+
/** The session-cumulative usage persisted on run state (or undefined before any turn). */
|
|
15
|
+
export declare function readCumulativeUsage(state: Record<string, unknown>): PersistedTokenUsage | undefined;
|
|
16
|
+
export interface TraceTurnUsage {
|
|
17
|
+
/** Input tokens consumed by THIS turn (cumulative delta since the turn opened). */
|
|
18
|
+
inputTokens?: number;
|
|
19
|
+
/** Output tokens generated by THIS turn (cumulative delta since the turn opened). */
|
|
20
|
+
outputTokens?: number;
|
|
21
|
+
/** Context-window occupancy — the last prompt's token count (not a per-turn delta). */
|
|
22
|
+
contextTokens?: number;
|
|
23
|
+
}
|
|
24
|
+
/** Per-turn token usage for a trace's `done` event. `baseline` is the cumulative
|
|
25
|
+
* usage captured when the turn OPENED, so input/output are strictly this turn's
|
|
26
|
+
* consumption (correct for cost attribution — never the running session total).
|
|
27
|
+
* `contextTokens` is the current window occupancy, which is inherently a snapshot. */
|
|
28
|
+
export declare function computeTurnTraceUsage(baseline: PersistedTokenUsage | undefined, state: Record<string, unknown>): TraceTurnUsage;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { TokenAccumulator } from './TokenAccumulator.js';
|
|
2
|
+
export const TOKEN_USAGE_STATE_KEY = '__tokenUsage';
|
|
3
|
+
export const LAST_PROMPT_TOKENS_KEY = '__lastPromptTokens';
|
|
4
|
+
function readPersistedUsage(state) {
|
|
5
|
+
const saved = state[TOKEN_USAGE_STATE_KEY];
|
|
6
|
+
if (!saved || typeof saved !== 'object') {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
const usage = saved;
|
|
10
|
+
if (typeof usage.inputTokens !== 'number' ||
|
|
11
|
+
typeof usage.outputTokens !== 'number' ||
|
|
12
|
+
typeof usage.totalTokens !== 'number') {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
return usage;
|
|
16
|
+
}
|
|
17
|
+
function hydrateAccumulator(state) {
|
|
18
|
+
const acc = new TokenAccumulator();
|
|
19
|
+
const saved = readPersistedUsage(state);
|
|
20
|
+
if (saved) {
|
|
21
|
+
acc.restoreCumulative(saved);
|
|
22
|
+
}
|
|
23
|
+
return acc;
|
|
24
|
+
}
|
|
25
|
+
/** Record real turn usage onto run state (survives across turns via persistence). */
|
|
26
|
+
export async function persistTurnUsageFromTurn(ctx, turn) {
|
|
27
|
+
if (!turn.usage) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const acc = hydrateAccumulator(ctx.runState.state);
|
|
31
|
+
const turnIndex = acc.turns.length + 1;
|
|
32
|
+
acc.record({
|
|
33
|
+
turn: turnIndex,
|
|
34
|
+
inputTokens: turn.usage.inputTokens,
|
|
35
|
+
outputTokens: turn.usage.outputTokens,
|
|
36
|
+
totalTokens: turn.usage.totalTokens,
|
|
37
|
+
cacheReadTokens: turn.usage.cacheReadTokens,
|
|
38
|
+
latencyMs: 0,
|
|
39
|
+
});
|
|
40
|
+
ctx.runState.state[TOKEN_USAGE_STATE_KEY] = acc.cumulative;
|
|
41
|
+
ctx.runState.state[LAST_PROMPT_TOKENS_KEY] =
|
|
42
|
+
turn.usage.contextTokens ?? turn.usage.inputTokens;
|
|
43
|
+
ctx.runState.updatedAt = Date.now();
|
|
44
|
+
await ctx.runStore.putRunState(ctx.runState);
|
|
45
|
+
}
|
|
46
|
+
export function readLastPromptTokens(state) {
|
|
47
|
+
const value = state[LAST_PROMPT_TOKENS_KEY];
|
|
48
|
+
return typeof value === 'number' ? value : undefined;
|
|
49
|
+
}
|
|
50
|
+
/** The session-cumulative usage persisted on run state (or undefined before any turn). */
|
|
51
|
+
export function readCumulativeUsage(state) {
|
|
52
|
+
return readPersistedUsage(state);
|
|
53
|
+
}
|
|
54
|
+
/** Per-turn token usage for a trace's `done` event. `baseline` is the cumulative
|
|
55
|
+
* usage captured when the turn OPENED, so input/output are strictly this turn's
|
|
56
|
+
* consumption (correct for cost attribution — never the running session total).
|
|
57
|
+
* `contextTokens` is the current window occupancy, which is inherently a snapshot. */
|
|
58
|
+
export function computeTurnTraceUsage(baseline, state) {
|
|
59
|
+
const contextTokens = readLastPromptTokens(state);
|
|
60
|
+
const now = readPersistedUsage(state);
|
|
61
|
+
if (!now) {
|
|
62
|
+
return contextTokens === undefined ? {} : { contextTokens };
|
|
63
|
+
}
|
|
64
|
+
const base = baseline ?? { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
65
|
+
return {
|
|
66
|
+
inputTokens: Math.max(0, now.inputTokens - base.inputTokens),
|
|
67
|
+
outputTokens: Math.max(0, now.outputTokens - base.outputTokens),
|
|
68
|
+
...(contextTokens === undefined ? {} : { contextTokens }),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { Tool } from '../types/effectTool.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type { StreamPart, TurnHandle } from '../types/stream.js';
|
|
4
4
|
/**
|
|
5
5
|
* Deferred-work scheduling for proactive (agent-initiated) turns.
|
|
6
6
|
*
|
|
@@ -51,7 +51,7 @@ export interface WakeDelivery {
|
|
|
51
51
|
reason: string;
|
|
52
52
|
payload?: Record<string, unknown>;
|
|
53
53
|
/** Full stream of the wake turn (text, tool events, interactive parts…). */
|
|
54
|
-
parts:
|
|
54
|
+
parts: StreamPart[];
|
|
55
55
|
/** Concatenated assistant text of the wake turn. */
|
|
56
56
|
text: string;
|
|
57
57
|
}
|
package/dist/scheduler/index.js
CHANGED
|
@@ -11,9 +11,8 @@ export class TracingService {
|
|
|
11
11
|
}
|
|
12
12
|
startSpan(name, attributes, parentSpanId) {
|
|
13
13
|
if (!this.config) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
throw new Error('Tracing not initialized. Call initTracing() first.');
|
|
14
|
+
throw new Error('TracingService not initialized. Construct it with a config — ' +
|
|
15
|
+
'new TracingService(config) — or call .init(config) before startSpan().');
|
|
17
16
|
}
|
|
18
17
|
const span = {
|
|
19
18
|
id: this.generateSpanId(),
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { Session } from '../types/index.js';
|
|
2
2
|
import type { AuditListOptions, ConversationAuditEntry } from '../audit/types.js';
|
|
3
|
+
export declare class StaleWriteError extends Error {
|
|
4
|
+
readonly sessionId: string;
|
|
5
|
+
readonly expectedVersion: number;
|
|
6
|
+
readonly actualVersion: number;
|
|
7
|
+
constructor(sessionId: string, expectedVersion: number, actualVersion: number);
|
|
8
|
+
}
|
|
3
9
|
export interface SessionListWindow {
|
|
4
10
|
from?: Date;
|
|
5
11
|
to?: Date;
|
|
@@ -1 +1,12 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export class StaleWriteError extends Error {
|
|
2
|
+
sessionId;
|
|
3
|
+
expectedVersion;
|
|
4
|
+
actualVersion;
|
|
5
|
+
constructor(sessionId, expectedVersion, actualVersion) {
|
|
6
|
+
super(`Stale write for session ${sessionId}: expected version ${expectedVersion}, stored version is ${actualVersion}`);
|
|
7
|
+
this.name = 'StaleWriteError';
|
|
8
|
+
this.sessionId = sessionId;
|
|
9
|
+
this.expectedVersion = expectedVersion;
|
|
10
|
+
this.actualVersion = actualVersion;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Session } from '../../types/index.js';
|
|
2
2
|
import type { AuditListOptions, ConversationAuditEntry } from '../../audit/types.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type SessionListWindow, type SessionStore } from '../SessionStore.js';
|
|
4
4
|
export declare class MemoryStore implements SessionStore {
|
|
5
5
|
private sessions;
|
|
6
6
|
get(id: string): Promise<Session | null>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { StaleWriteError } from '../SessionStore.js';
|
|
1
2
|
export class MemoryStore {
|
|
2
3
|
sessions = new Map();
|
|
3
4
|
async get(id) {
|
|
@@ -5,8 +6,21 @@ export class MemoryStore {
|
|
|
5
6
|
return session ? safeClone(session) : null;
|
|
6
7
|
}
|
|
7
8
|
async save(session) {
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
const existing = this.sessions.get(session.id);
|
|
10
|
+
const expected = session.version ?? 0;
|
|
11
|
+
if (existing !== undefined) {
|
|
12
|
+
const stored = existing.version ?? 0;
|
|
13
|
+
if (stored !== expected) {
|
|
14
|
+
throw new StaleWriteError(session.id, expected, stored);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
else if (expected !== 0) {
|
|
18
|
+
throw new StaleWriteError(session.id, expected, 0);
|
|
19
|
+
}
|
|
20
|
+
const toSave = safeClone(session);
|
|
21
|
+
toSave.updatedAt = new Date();
|
|
22
|
+
toSave.version = expected + 1;
|
|
23
|
+
this.sessions.set(session.id, toSave);
|
|
10
24
|
}
|
|
11
25
|
async delete(id) {
|
|
12
26
|
this.sessions.delete(id);
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type SessionStore } from './SessionStore.js';
|
|
2
2
|
export type SessionStoreFactory = () => SessionStore | Promise<SessionStore>;
|
|
3
3
|
/**
|
|
4
4
|
* Registers the shared SessionStore contract tests. Must be invoked at the
|
|
5
5
|
* top level of a bun test file.
|
|
6
6
|
*/
|
|
7
7
|
export declare function runSessionStoreContract(factory: SessionStoreFactory): void;
|
|
8
|
+
/**
|
|
9
|
+
* Registers optimistic-concurrency (CAS) contract tests for SessionStore adapters.
|
|
10
|
+
* Two concurrent saves with the same expected version: exactly one succeeds.
|
|
11
|
+
*/
|
|
12
|
+
export declare function runSessionStoreCasContract(factory: SessionStoreFactory): void;
|