@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/README.md
CHANGED
|
@@ -60,6 +60,91 @@ for await (const part of handle.events) { // events is a property, not
|
|
|
60
60
|
await handle; // resolves to TurnResult once the stream is consumed
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
+
## Single-run trace / `runOnce`
|
|
64
|
+
|
|
65
|
+
Use `runOnce` when an evaluator needs one complete, JSON-serializable turn instead
|
|
66
|
+
of a live stream. The trace includes the answer, tool roll-up, and nested spans.
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
const trace = await runtime.runOnce({
|
|
70
|
+
sessionId: 'grounding-eval-42',
|
|
71
|
+
input: 'What was my last invoice total?',
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const judgeContext = {
|
|
75
|
+
answer: trace.answer,
|
|
76
|
+
evidence: trace.toolResults.map(({ name, result }) => ({ name, result })),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const verdict = await groundingJudge(judgeContext);
|
|
80
|
+
console.log(verdict, trace.usedTool, trace.traceId);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`runOnce` executes exactly one normal runtime turn and observes its existing event
|
|
84
|
+
stream. Runtime tracing also captures normal `run()` calls; trace persistence is
|
|
85
|
+
physically separate from the session store and durable effect journal.
|
|
86
|
+
|
|
87
|
+
## Observability
|
|
88
|
+
|
|
89
|
+
Tracing is enabled by default with an in-process `MemoryTraceStore`. Supply a native
|
|
90
|
+
store for durable reads and add any number of export sinks independently of
|
|
91
|
+
`sessionStore`:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { createRuntime } from '@kuralle-agents/core';
|
|
95
|
+
import { RedisTraceStore } from '@kuralle-agents/redis-store';
|
|
96
|
+
|
|
97
|
+
const traceStore = new RedisTraceStore({
|
|
98
|
+
client,
|
|
99
|
+
traceTtlSeconds: 7 * 24 * 60 * 60,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const runtime = createRuntime({
|
|
103
|
+
agents: [agent],
|
|
104
|
+
defaultAgentId: 'support',
|
|
105
|
+
tracing: {
|
|
106
|
+
store: traceStore,
|
|
107
|
+
sampling: 0.1,
|
|
108
|
+
sinks: [externalSink],
|
|
109
|
+
redact: (span) => ({
|
|
110
|
+
...span,
|
|
111
|
+
attributes: { ...span.attributes, input: undefined, output: undefined },
|
|
112
|
+
}),
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const latest = (await runtime.listTraces('session-42'))[0];
|
|
117
|
+
const trace = latest ? await runtime.getTrace(latest.traceId) : null;
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Sink failures are swallowed and never change the run result. Redaction is off by
|
|
121
|
+
default for useful local debugging; use the hook above before persisting sensitive
|
|
122
|
+
tool inputs or outputs. Set `tracing.enabled: false` to disable capture.
|
|
123
|
+
|
|
124
|
+
### OTLP and Langfuse
|
|
125
|
+
|
|
126
|
+
The built-in exporter uses HTTP/JSON over `fetch`, so it runs in Bun, Node.js, and
|
|
127
|
+
Cloudflare Workers without a Node OpenTelemetry SDK:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import { langfuseSink, otelSink } from '@kuralle-agents/core';
|
|
131
|
+
|
|
132
|
+
const runtime = createRuntime({
|
|
133
|
+
agents: [agent],
|
|
134
|
+
defaultAgentId: 'support',
|
|
135
|
+
tracing: {
|
|
136
|
+
store: traceStore,
|
|
137
|
+
sinks: [
|
|
138
|
+
otelSink({ endpoint: 'https://collector.example.com', headers: { Authorization: 'Bearer token' } }),
|
|
139
|
+
langfuseSink({ publicKey: env.LANGFUSE_PUBLIC_KEY, secretKey: env.LANGFUSE_SECRET_KEY }),
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Endpoints may include `/v1/traces`; otherwise the sink appends it. For self-hosted
|
|
146
|
+
Langfuse, pass its OTLP base URL as `endpoint`.
|
|
147
|
+
|
|
63
148
|
## Flows
|
|
64
149
|
|
|
65
150
|
A flow is a node graph that enforces a multi-step procedure without embedding a 600-line SOP in a system prompt.
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { AgentConfig } from '../types/agentConfig.js';
|
|
2
2
|
import type { ChannelDriver } from '../types/channel.js';
|
|
3
|
-
import type { CollectNode } from '../types/flow.js';
|
|
4
3
|
import type { RunContext } from '../types/run-context.js';
|
|
5
4
|
import type { RunState } from '../runtime/durable/types.js';
|
|
6
5
|
import type { NormalizedTransition } from './normalizeTransition.js';
|
|
@@ -9,11 +8,15 @@ export interface FlowPark {
|
|
|
9
8
|
flow: string;
|
|
10
9
|
node: string;
|
|
11
10
|
}
|
|
11
|
+
export declare function pushFlowPark(state: Record<string, unknown>, park: FlowPark): void;
|
|
12
|
+
export declare function popFlowPark(state: Record<string, unknown>): FlowPark | undefined;
|
|
12
13
|
export declare function getFlowPark(state: Record<string, unknown>): FlowPark | undefined;
|
|
13
14
|
export declare function looksLikeOffScriptQuestion(input: string): boolean;
|
|
14
15
|
export interface CollectDigressionOptions {
|
|
15
16
|
agent: AgentConfig;
|
|
16
|
-
node:
|
|
17
|
+
node: {
|
|
18
|
+
id: string;
|
|
19
|
+
};
|
|
17
20
|
activeFlowName: string;
|
|
18
21
|
run: RunState;
|
|
19
22
|
driver: ChannelDriver;
|
|
@@ -1,20 +1,57 @@
|
|
|
1
1
|
import { resolveReplyNode } from './nodeBuilders.js';
|
|
2
2
|
import { selectHostTarget } from '../runtime/select.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import { persistTurnUsageFromTurn } from '../runtime/turnTokenUsage.js';
|
|
4
|
+
const FLOW_PARK_STACK_KEY = '__flowParkStack';
|
|
5
|
+
const LEGACY_FLOW_PARK_KEY = '__flowPark';
|
|
6
|
+
const MAX_FLOW_PARK_DEPTH = 8;
|
|
7
|
+
function isFlowPark(raw) {
|
|
6
8
|
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
7
|
-
return
|
|
9
|
+
return false;
|
|
8
10
|
}
|
|
9
11
|
const flow = raw.flow;
|
|
10
12
|
const node = raw.node;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
return typeof flow === 'string' && typeof node === 'string';
|
|
14
|
+
}
|
|
15
|
+
function getFlowParkStack(state) {
|
|
16
|
+
const raw = state[FLOW_PARK_STACK_KEY];
|
|
17
|
+
if (!Array.isArray(raw)) {
|
|
18
|
+
return undefined;
|
|
13
19
|
}
|
|
14
|
-
|
|
20
|
+
const stack = raw.filter(isFlowPark);
|
|
21
|
+
return stack.length > 0 ? stack : undefined;
|
|
22
|
+
}
|
|
23
|
+
export function pushFlowPark(state, park) {
|
|
24
|
+
const stack = getFlowParkStack(state) ?? [];
|
|
25
|
+
stack.push(park);
|
|
26
|
+
while (stack.length > MAX_FLOW_PARK_DEPTH) {
|
|
27
|
+
stack.shift();
|
|
28
|
+
}
|
|
29
|
+
state[FLOW_PARK_STACK_KEY] = stack;
|
|
15
30
|
}
|
|
16
|
-
function
|
|
17
|
-
|
|
31
|
+
export function popFlowPark(state) {
|
|
32
|
+
const stack = getFlowParkStack(state);
|
|
33
|
+
if (!stack) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
const park = stack.pop();
|
|
37
|
+
if (stack.length === 0) {
|
|
38
|
+
delete state[FLOW_PARK_STACK_KEY];
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
state[FLOW_PARK_STACK_KEY] = stack;
|
|
42
|
+
}
|
|
43
|
+
return park;
|
|
44
|
+
}
|
|
45
|
+
export function getFlowPark(state) {
|
|
46
|
+
const stack = getFlowParkStack(state);
|
|
47
|
+
if (stack) {
|
|
48
|
+
return stack[stack.length - 1];
|
|
49
|
+
}
|
|
50
|
+
const legacy = state[LEGACY_FLOW_PARK_KEY];
|
|
51
|
+
if (isFlowPark(legacy)) {
|
|
52
|
+
return legacy;
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
18
55
|
}
|
|
19
56
|
function appendAssistantMessage(run, text) {
|
|
20
57
|
if (!text.trim()) {
|
|
@@ -53,7 +90,7 @@ export async function runCollectDigression(options) {
|
|
|
53
90
|
};
|
|
54
91
|
}
|
|
55
92
|
if (selection.kind === 'enterFlow') {
|
|
56
|
-
|
|
93
|
+
pushFlowPark(run.state, { flow: activeFlowName, node: node.id });
|
|
57
94
|
return {
|
|
58
95
|
kind: 'transition',
|
|
59
96
|
transition: {
|
|
@@ -74,6 +111,7 @@ export async function runCollectDigression(options) {
|
|
|
74
111
|
tools: ctx.globalTools,
|
|
75
112
|
};
|
|
76
113
|
const turn = await driver.runAgentTurn(resolveReplyNode(replyNode, run.state, { freeConversation: true }), ctx);
|
|
114
|
+
await persistTurnUsageFromTurn(ctx, turn);
|
|
77
115
|
if (turn.text.trim()) {
|
|
78
116
|
appendAssistantMessage(run, turn.text);
|
|
79
117
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { runCollectDigression } from './collectDigression.js';
|
|
2
|
+
import { persistTurnUsageFromTurn } from '../runtime/turnTokenUsage.js';
|
|
2
3
|
import { hasPendingUserInput } from '../runtime/channels/inputBuffer.js';
|
|
3
4
|
import { userInputToText } from '../runtime/userInput.js';
|
|
4
5
|
import { resolveCollectExtractionNode } from './nodeBuilders.js';
|
|
@@ -16,8 +17,13 @@ function appendUserMessage(run, input) {
|
|
|
16
17
|
run.messages = [...run.messages, message];
|
|
17
18
|
}
|
|
18
19
|
export async function collectUntilComplete(node, run, driver, ctx, options) {
|
|
20
|
+
// A satisfied collect defers completion for ONE pass when there is pending input,
|
|
21
|
+
// so a correction ("no, Tuesday") is re-extracted and overwrites before completing
|
|
22
|
+
// (G14). Bounded to a single pass so the loop cannot spin if a driver's awaitUser
|
|
23
|
+
// does not clear the pending buffer — completion no longer depends on that.
|
|
24
|
+
let pendingConsumed = false;
|
|
19
25
|
for (;;) {
|
|
20
|
-
if (schemaSatisfied(node, run.state)) {
|
|
26
|
+
if (schemaSatisfied(node, run.state) && (!hasPendingUserInput(ctx.session) || pendingConsumed)) {
|
|
21
27
|
const data = projectCollectData(node, run.state);
|
|
22
28
|
return normalizeTransition(await node.onComplete(data, run.state));
|
|
23
29
|
}
|
|
@@ -32,6 +38,7 @@ export async function collectUntilComplete(node, run, driver, ctx, options) {
|
|
|
32
38
|
if (hasPendingUserInput(ctx.session)) {
|
|
33
39
|
const signal = await driver.awaitUser(ctx);
|
|
34
40
|
appendUserMessage(run, signal.input);
|
|
41
|
+
pendingConsumed = true;
|
|
35
42
|
}
|
|
36
43
|
else if (ctx.turnInputConsumed) {
|
|
37
44
|
// No fresh input to extract this turn: ask (deterministically) for the
|
|
@@ -59,6 +66,7 @@ export async function collectUntilComplete(node, run, driver, ctx, options) {
|
|
|
59
66
|
const turn = await (driver.runExtraction
|
|
60
67
|
? driver.runExtraction(resolved, ctx)
|
|
61
68
|
: driver.runAgentTurn(resolved, ctx));
|
|
69
|
+
await persistTurnUsageFromTurn(ctx, turn);
|
|
62
70
|
mergeExtractionFromTurn(node, run, turn, ctx);
|
|
63
71
|
const missingAfter = computeMissingFields(node, getCollectData(run.state, node.id));
|
|
64
72
|
const advanced = missingAfter.length < missingBefore.length || schemaSatisfied(node, run.state);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CollectNode, FlowState } from '../types/flow.js';
|
|
2
2
|
import type { Tool } from '../types/effectTool.js';
|
|
3
3
|
import type { HarnessStreamPart } from '../types/stream.js';
|
|
4
|
+
export declare function resetCollect(state: FlowState, nodeId: string): void;
|
|
4
5
|
export declare function getCollectData(state: FlowState, nodeId: string): Record<string, unknown>;
|
|
5
6
|
export declare function incrementCollectTurns(state: FlowState, nodeId: string): number;
|
|
6
7
|
export declare function computeMissingFields(node: CollectNode, data: Record<string, unknown>): string[];
|
package/dist/flow/extraction.js
CHANGED
|
@@ -6,6 +6,10 @@ function collectDataKey(nodeId) {
|
|
|
6
6
|
function collectTurnsKey(nodeId) {
|
|
7
7
|
return `__collectTurns_${nodeId}`;
|
|
8
8
|
}
|
|
9
|
+
export function resetCollect(state, nodeId) {
|
|
10
|
+
delete state[collectDataKey(nodeId)];
|
|
11
|
+
delete state[collectTurnsKey(nodeId)];
|
|
12
|
+
}
|
|
9
13
|
export function getCollectData(state, nodeId) {
|
|
10
14
|
const raw = state[collectDataKey(nodeId)];
|
|
11
15
|
if (typeof raw === 'object' && raw !== null && !Array.isArray(raw)) {
|
package/dist/flow/runFlow.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { popFlowPark, runCollectDigression } from './collectDigression.js';
|
|
2
2
|
import { parseConfirmation } from './confirmParse.js';
|
|
3
|
-
import { hasPendingUserInput } from '../runtime/channels/inputBuffer.js';
|
|
3
|
+
import { hasPendingUserInput, setPendingUserInput } from '../runtime/channels/inputBuffer.js';
|
|
4
4
|
import { userInputToText } from '../runtime/userInput.js';
|
|
5
5
|
import { collectUntilComplete } from './collectUntilComplete.js';
|
|
6
6
|
import { isActionNode, isCollectNode, isDecideNode, isReplyNode, } from './nodeKinds.js';
|
|
@@ -10,6 +10,7 @@ import { resolveReplyNode } from './nodeBuilders.js';
|
|
|
10
10
|
import { evaluateReplyControl } from './controlEvaluator.js';
|
|
11
11
|
import { runNodeVerify, VerifyBlockedError } from './verify.js';
|
|
12
12
|
import { loadRecordedSteps } from '../runtime/durable/replay.js';
|
|
13
|
+
import { persistTurnUsageFromTurn } from '../runtime/turnTokenUsage.js';
|
|
13
14
|
import { SuspendError } from '../runtime/durable/RunStore.js';
|
|
14
15
|
import { ToolApprovalDeniedError } from '../tools/effect/errors.js';
|
|
15
16
|
import { emitInteractiveOnNodeEnter } from './emitInteractive.js';
|
|
@@ -68,16 +69,24 @@ async function dispatchConfirmGate(node, run, driver, ctx) {
|
|
|
68
69
|
return { kind: 'stay' };
|
|
69
70
|
}
|
|
70
71
|
let input = '';
|
|
72
|
+
let rawInput = '';
|
|
71
73
|
if (hasPendingUserInput(ctx.session)) {
|
|
72
74
|
const signal = await driver.awaitUser(ctx);
|
|
75
|
+
rawInput = signal.input;
|
|
73
76
|
input = userInputToText(signal.input);
|
|
74
77
|
appendUserMessage(run, signal.input);
|
|
75
78
|
}
|
|
76
79
|
else {
|
|
77
80
|
input = latestUserText(run);
|
|
81
|
+
rawInput = input;
|
|
78
82
|
}
|
|
79
|
-
ctx.turnInputConsumed = true;
|
|
80
83
|
const verdict = parseConfirmation(input);
|
|
84
|
+
if (verdict === 'decline') {
|
|
85
|
+
setPendingUserInput(ctx.session, rawInput);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
ctx.turnInputConsumed = true;
|
|
89
|
+
}
|
|
81
90
|
const branch = verdict === 'affirm'
|
|
82
91
|
? gate.onConfirm
|
|
83
92
|
: verdict === 'decline'
|
|
@@ -136,7 +145,40 @@ async function dispatchNode(node, run, driver, ctx, agent, flow) {
|
|
|
136
145
|
return normalizeTransition(await node.decide(structured, run.state));
|
|
137
146
|
}
|
|
138
147
|
if (isReplyNode(node)) {
|
|
148
|
+
// Consume input here ONLY to feed the out-of-band digression check below, and
|
|
149
|
+
// only for input this turn genuinely fresh to THIS reply — gated on both:
|
|
150
|
+
// !turnInputConsumed: a prior node (e.g. a collect) already took the turn's
|
|
151
|
+
// input, so a terminal reply like `done` (next→end) must not re-digress on
|
|
152
|
+
// the leftover — otherwise it returns `stay`, the main loop's stay+pending
|
|
153
|
+
// branch re-dispatches, and completion hangs on a driver clearing the buffer.
|
|
154
|
+
// outOfBandControl: with OOB off there is no digression, so the reply must not
|
|
155
|
+
// swallow input here — it returns its transition (`next: () => 'stay'`) and the
|
|
156
|
+
// main loop's stay-branch owns awaitUser.
|
|
157
|
+
let freshUserInput = false;
|
|
158
|
+
if (hasPendingUserInput(ctx.session) && !ctx.turnInputConsumed && ctx.outOfBandControl) {
|
|
159
|
+
const signal = await driver.awaitUser(ctx);
|
|
160
|
+
appendUserMessage(run, signal.input);
|
|
161
|
+
ctx.turnInputConsumed = true;
|
|
162
|
+
freshUserInput = true;
|
|
163
|
+
}
|
|
164
|
+
if (freshUserInput && agent) {
|
|
165
|
+
const digression = await runCollectDigression({
|
|
166
|
+
agent,
|
|
167
|
+
node,
|
|
168
|
+
activeFlowName: flow.name,
|
|
169
|
+
run,
|
|
170
|
+
driver,
|
|
171
|
+
ctx,
|
|
172
|
+
});
|
|
173
|
+
if (digression.kind === 'transition') {
|
|
174
|
+
return digression.transition;
|
|
175
|
+
}
|
|
176
|
+
if (digression.kind === 'answeredThenResume') {
|
|
177
|
+
return { kind: 'stay' };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
139
180
|
const turn = await driver.runAgentTurn(resolveReplyNode(node, run.state), ctx);
|
|
181
|
+
await persistTurnUsageFromTurn(ctx, turn);
|
|
140
182
|
if (ctx.outOfBandControl) {
|
|
141
183
|
const decision = await evaluateReplyControl({
|
|
142
184
|
node,
|
|
@@ -147,6 +189,23 @@ async function dispatchNode(node, run, driver, ctx, agent, flow) {
|
|
|
147
189
|
if (decision.kind === 'redispatch') {
|
|
148
190
|
const signal = await driver.awaitUser(ctx);
|
|
149
191
|
appendUserMessage(run, signal.input);
|
|
192
|
+
ctx.turnInputConsumed = true;
|
|
193
|
+
if (agent) {
|
|
194
|
+
const digression = await runCollectDigression({
|
|
195
|
+
agent,
|
|
196
|
+
node,
|
|
197
|
+
activeFlowName: flow.name,
|
|
198
|
+
run,
|
|
199
|
+
driver,
|
|
200
|
+
ctx,
|
|
201
|
+
});
|
|
202
|
+
if (digression.kind === 'transition') {
|
|
203
|
+
return digression.transition;
|
|
204
|
+
}
|
|
205
|
+
if (digression.kind === 'answeredThenResume') {
|
|
206
|
+
return { kind: 'stay' };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
150
209
|
return dispatchNode(node, run, driver, ctx, agent, flow);
|
|
151
210
|
}
|
|
152
211
|
appendAssistantMessage(run, turn.text);
|
|
@@ -232,9 +291,8 @@ export async function runFlow(flow, run, driver, ctx, agent) {
|
|
|
232
291
|
return runFlow(transition.flow, run, driver, ctx, agent);
|
|
233
292
|
}
|
|
234
293
|
if (transition.kind === 'end') {
|
|
235
|
-
const park =
|
|
294
|
+
const park = popFlowPark(run.state);
|
|
236
295
|
if (park && agent) {
|
|
237
|
-
delete run.state.__flowPark;
|
|
238
296
|
const parkedFlow = agent.flows?.find((candidate) => candidate.name === park.flow);
|
|
239
297
|
if (parkedFlow) {
|
|
240
298
|
run.activeFlow = park.flow;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { createFlowTransition, createFlowTransitionWithNode, isFlowTransition, createFlowUpdate, isFlowUpdate, } from './flows/index.js';
|
|
2
2
|
export { SessionManager } from './session/SessionManager.js';
|
|
3
3
|
export type { SessionStore } from './session/SessionStore.js';
|
|
4
|
+
export { StaleWriteError } from './session/SessionStore.js';
|
|
4
5
|
export { MemoryStore } from './session/stores/MemoryStore.js';
|
|
5
6
|
export { reviveSession } from './session/utils.js';
|
|
6
7
|
export { DEFAULT_CHANNEL_POLICIES, applyChannelPolicy, getDefaultChannelPolicy, resolveChannelPolicy, } from './channels/index.js';
|
|
@@ -94,6 +95,9 @@ export { defineTool } from './types/effectTool.js';
|
|
|
94
95
|
export { fsErrorCode } from './types/filesystem.js';
|
|
95
96
|
export { createFsTool } from './tools/fs/createFsTool.js';
|
|
96
97
|
export type { CreateFsToolOptions, GrepHit } from './tools/fs/createFsTool.js';
|
|
98
|
+
export { createShellTool } from './tools/fs/createShellTool.js';
|
|
99
|
+
export type { CreateShellToolOptions } from './tools/fs/createShellTool.js';
|
|
100
|
+
export type { Shell, ShellResult, ShellExecOptions } from './types/shell.js';
|
|
97
101
|
export { SkillsCapability, wireAgentSkills, collectRegisteredNames, validateSkillAllowedTools, prepareSkillStore, isSkillStore, InlineSkillStore, } from './skills/index.js';
|
|
98
102
|
export type { WiredAgentSkills, SkillWireAgent } from './skills/index.js';
|
|
99
103
|
export { buildToolSet, toolToAiSdk, wrapAiSdkTool, ToolApprovalDeniedError, ToolTimeoutError, } from './tools/effect/index.js';
|
|
@@ -106,6 +110,10 @@ export type { ConfirmVerdict } from './flow/confirmParse.js';
|
|
|
106
110
|
export type { Route } from './types/route.js';
|
|
107
111
|
export type { TurnHandle } from './types/stream.js';
|
|
108
112
|
export type { HarnessStreamPart } from './types/stream.js';
|
|
113
|
+
export type { AgentSpan, AgentTrace, SpanKind } from './types/trace.js';
|
|
114
|
+
export { MemoryTraceStore, isTraceStore, type MemoryTraceStoreOptions, type TraceListWindow, type TraceSink, type TraceStore, } from './tracing/index.js';
|
|
115
|
+
export { OtelTraceSink, langfuseSink, otelSink, toOtlpPayload, type LangfuseSinkOptions, type OtelTraceSinkOptions, } from './tracing/index.js';
|
|
116
|
+
export { TraceRecorder, runOnce, type TraceRecorderOptions } from './runtime/TraceRecorder.js';
|
|
109
117
|
export { harnessToUIMessageStream } from './ai-sdk/uiMessageStream.js';
|
|
110
118
|
export type { KuralleMetadata, KuralleDataParts, KuralleUIMessage, } from './ai-sdk/uiMessageStream.js';
|
|
111
119
|
export type { ChoiceOption, ResolvedSelection } from './types/selection.js';
|
|
@@ -116,7 +124,7 @@ export type { ChannelDriver, TextDriver } from './runtime/channels/index.js';
|
|
|
116
124
|
export type { TurnResult } from './types/channel.js';
|
|
117
125
|
export type { RunContext, ToolContext, ActionContext } from './types/run-context.js';
|
|
118
126
|
export type { AnyTool } from './types/effectTool.js';
|
|
119
|
-
export { createRuntime, Runtime, type HarnessConfig, type RunOptions, } from './runtime/Runtime.js';
|
|
127
|
+
export { createRuntime, Runtime, type HarnessConfig, type RunOptions, type TracingConfig, } from './runtime/Runtime.js';
|
|
120
128
|
export type { RuntimeLike } from './runtime/RuntimeLike.js';
|
|
121
129
|
export { userInputToText, mergeUserInputContents, hasMediaParts, transcribeAudioParts, type UserInputContent, } from './runtime/userInput.js';
|
|
122
130
|
export { setPendingUserInput, consumePendingUserInput, consumeAllPendingUserInput, peekPendingUserInput, hasPendingUserInput, } from './runtime/channels/inputBuffer.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { createFlowTransition, createFlowTransitionWithNode, isFlowTransition, createFlowUpdate, isFlowUpdate, } from './flows/index.js';
|
|
2
2
|
export { SessionManager } from './session/SessionManager.js';
|
|
3
|
+
export { StaleWriteError } from './session/SessionStore.js';
|
|
3
4
|
export { MemoryStore } from './session/stores/MemoryStore.js';
|
|
4
5
|
export { reviveSession } from './session/utils.js';
|
|
5
6
|
export { DEFAULT_CHANNEL_POLICIES, applyChannelPolicy, getDefaultChannelPolicy, resolveChannelPolicy, } from './channels/index.js';
|
|
@@ -53,9 +54,13 @@ export { defineAgent, defineFlow, reply, collect, action, decide, confirmGate, }
|
|
|
53
54
|
export { defineTool } from './types/effectTool.js';
|
|
54
55
|
export { fsErrorCode } from './types/filesystem.js';
|
|
55
56
|
export { createFsTool } from './tools/fs/createFsTool.js';
|
|
57
|
+
export { createShellTool } from './tools/fs/createShellTool.js';
|
|
56
58
|
export { SkillsCapability, wireAgentSkills, collectRegisteredNames, validateSkillAllowedTools, prepareSkillStore, isSkillStore, InlineSkillStore, } from './skills/index.js';
|
|
57
59
|
export { buildToolSet, toolToAiSdk, wrapAiSdkTool, ToolApprovalDeniedError, ToolTimeoutError, } from './tools/effect/index.js';
|
|
58
60
|
export { parseConfirmation } from './flow/confirmParse.js';
|
|
61
|
+
export { MemoryTraceStore, isTraceStore, } from './tracing/index.js';
|
|
62
|
+
export { OtelTraceSink, langfuseSink, otelSink, toOtlpPayload, } from './tracing/index.js';
|
|
63
|
+
export { TraceRecorder, runOnce } from './runtime/TraceRecorder.js';
|
|
59
64
|
export { harnessToUIMessageStream } from './ai-sdk/uiMessageStream.js';
|
|
60
65
|
export { DURABLE_RUNS_KEY } from './runtime/durable/types.js';
|
|
61
66
|
export { createRuntime, Runtime, } from './runtime/Runtime.js';
|
|
@@ -235,6 +235,13 @@ export class PromptBuilder {
|
|
|
235
235
|
priority: LAYER_PRIORITIES.CONVERSATION_SUMMARY,
|
|
236
236
|
});
|
|
237
237
|
}
|
|
238
|
+
if (memory.openGoalsPrompt) {
|
|
239
|
+
sections.push({
|
|
240
|
+
type: 'conversation_summary',
|
|
241
|
+
content: memory.openGoalsPrompt,
|
|
242
|
+
priority: LAYER_PRIORITIES.CONVERSATION_SUMMARY,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
238
245
|
return sections;
|
|
239
246
|
}
|
|
240
247
|
formatSection(section) {
|
package/dist/prompts/types.d.ts
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { KnowledgeRetrievalResult, RetrievalCacheAdapter } from '../types/voice.js';
|
|
2
|
+
/**
|
|
3
|
+
* Zero-config, in-process session retrieval cache (G6). Keyed by the query
|
|
4
|
+
* embedding: a lookup returns the cached results of the most-similar recent
|
|
5
|
+
* query (cosine ≥ threshold, within TTL). LRU-bounded. Needs only the embedder
|
|
6
|
+
* the KnowledgeProvider already requires — no external dependency. Apps that
|
|
7
|
+
* want a shared/vector-backed cache can inject their own `RetrievalCacheAdapter`
|
|
8
|
+
* via the KnowledgeProvider's `cacheFactory` instead.
|
|
9
|
+
*/
|
|
10
|
+
export interface InMemoryRetrievalCacheOptions {
|
|
11
|
+
maxEntries?: number;
|
|
12
|
+
ttlMs?: number;
|
|
13
|
+
similarityThreshold?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare class InMemoryRetrievalCache implements RetrievalCacheAdapter {
|
|
16
|
+
/** Ordered oldest → newest (LRU front, MRU back). */
|
|
17
|
+
private entries;
|
|
18
|
+
private readonly maxEntries;
|
|
19
|
+
private readonly ttlMs;
|
|
20
|
+
private readonly threshold;
|
|
21
|
+
constructor(options?: InMemoryRetrievalCacheOptions);
|
|
22
|
+
get size(): number;
|
|
23
|
+
lookup(queryEmbedding: readonly number[], topK?: number): KnowledgeRetrievalResult[];
|
|
24
|
+
populate(results: KnowledgeRetrievalResult[], queryEmbedding?: readonly number[]): void;
|
|
25
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
function cosineSimilarity(a, b) {
|
|
2
|
+
const n = Math.min(a.length, b.length);
|
|
3
|
+
if (n === 0)
|
|
4
|
+
return 0;
|
|
5
|
+
let dot = 0;
|
|
6
|
+
let na = 0;
|
|
7
|
+
let nb = 0;
|
|
8
|
+
for (let i = 0; i < n; i++) {
|
|
9
|
+
dot += a[i] * b[i];
|
|
10
|
+
na += a[i] * a[i];
|
|
11
|
+
nb += b[i] * b[i];
|
|
12
|
+
}
|
|
13
|
+
if (na === 0 || nb === 0)
|
|
14
|
+
return 0;
|
|
15
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
16
|
+
}
|
|
17
|
+
export class InMemoryRetrievalCache {
|
|
18
|
+
/** Ordered oldest → newest (LRU front, MRU back). */
|
|
19
|
+
entries = [];
|
|
20
|
+
maxEntries;
|
|
21
|
+
ttlMs;
|
|
22
|
+
threshold;
|
|
23
|
+
constructor(options = {}) {
|
|
24
|
+
this.maxEntries = options.maxEntries ?? 256;
|
|
25
|
+
this.ttlMs = options.ttlMs ?? 300_000;
|
|
26
|
+
this.threshold = options.similarityThreshold ?? 0.85;
|
|
27
|
+
}
|
|
28
|
+
get size() {
|
|
29
|
+
return this.entries.length;
|
|
30
|
+
}
|
|
31
|
+
lookup(queryEmbedding, topK) {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
let best;
|
|
34
|
+
let bestSim = this.threshold;
|
|
35
|
+
for (const entry of this.entries) {
|
|
36
|
+
if (now - entry.at > this.ttlMs)
|
|
37
|
+
continue;
|
|
38
|
+
const sim = cosineSimilarity(queryEmbedding, entry.embedding);
|
|
39
|
+
if (sim >= bestSim) {
|
|
40
|
+
bestSim = sim;
|
|
41
|
+
best = entry;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (!best)
|
|
45
|
+
return [];
|
|
46
|
+
// Touch: move to MRU.
|
|
47
|
+
this.entries = this.entries.filter((e) => e !== best);
|
|
48
|
+
this.entries.push(best);
|
|
49
|
+
const results = best.results;
|
|
50
|
+
return topK === undefined ? results : results.slice(0, topK);
|
|
51
|
+
}
|
|
52
|
+
populate(results, queryEmbedding) {
|
|
53
|
+
if (!queryEmbedding || queryEmbedding.length === 0 || results.length === 0)
|
|
54
|
+
return;
|
|
55
|
+
this.entries.push({ embedding: queryEmbedding, results: [...results], at: Date.now() });
|
|
56
|
+
while (this.entries.length > this.maxEntries)
|
|
57
|
+
this.entries.shift();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* The provider holds no mutable state itself — per-session cache state is
|
|
13
13
|
* stored on RunContext.retrievalCache so it survives agent handoffs.
|
|
14
14
|
*/
|
|
15
|
+
import { InMemoryRetrievalCache } from './InMemoryRetrievalCache.js';
|
|
15
16
|
export class KnowledgeProvider {
|
|
16
17
|
config;
|
|
17
18
|
retriever;
|
|
@@ -21,7 +22,11 @@ export class KnowledgeProvider {
|
|
|
21
22
|
this.config = options.config;
|
|
22
23
|
this.retriever = options.config.retriever;
|
|
23
24
|
this.embedder = options.config.embedder;
|
|
24
|
-
|
|
25
|
+
// Default to a zero-config in-process cache when an embedder is available
|
|
26
|
+
// (the cache is keyed by query embedding). Apps can inject their own adapter.
|
|
27
|
+
this.cacheFactory =
|
|
28
|
+
options.cacheFactory ??
|
|
29
|
+
(this.embedder ? () => new InMemoryRetrievalCache(options.config.cache) : undefined);
|
|
25
30
|
}
|
|
26
31
|
/**
|
|
27
32
|
* Create a new session-level cache instance. Called once per session
|
|
@@ -18,6 +18,19 @@ import type { PersistentMemoryStore } from '../memory/blocks/types.js';
|
|
|
18
18
|
import { type CompactionConfig } from './compaction.js';
|
|
19
19
|
import type { EscalationConfig } from '../escalation/types.js';
|
|
20
20
|
import type { WakeOptions } from '../scheduler/index.js';
|
|
21
|
+
import type { HandoffInputFilter } from './handoffFilters.js';
|
|
22
|
+
import type { AgentSpan, AgentTrace } from '../types/trace.js';
|
|
23
|
+
import { type TraceSink, type TraceStore } from '../tracing/TraceStore.js';
|
|
24
|
+
export interface TracingConfig {
|
|
25
|
+
enabled?: boolean;
|
|
26
|
+
store?: TraceStore;
|
|
27
|
+
sinks?: TraceSink[];
|
|
28
|
+
redact?: (span: AgentSpan) => AgentSpan | null;
|
|
29
|
+
sampling?: number | ((context: {
|
|
30
|
+
sessionId: string;
|
|
31
|
+
input?: unknown;
|
|
32
|
+
}) => boolean);
|
|
33
|
+
}
|
|
21
34
|
export interface HarnessConfig {
|
|
22
35
|
agents: AgentConfig[];
|
|
23
36
|
defaultAgentId: string;
|
|
@@ -56,6 +69,24 @@ export interface HarnessConfig {
|
|
|
56
69
|
* invokes the handler. Resume with `runtime.resumeFromEscalation()`.
|
|
57
70
|
*/
|
|
58
71
|
escalation?: EscalationConfig;
|
|
72
|
+
/** Default handoff input filter when a route does not define `filter`. */
|
|
73
|
+
handoffInputFilter?: HandoffInputFilter;
|
|
74
|
+
/**
|
|
75
|
+
* Silent handoff (default `true`). A transfer between agents reads as one
|
|
76
|
+
* continuous assistant: the transfer is a silent control tool call, and the
|
|
77
|
+
* target is given a continuation directive so it does not greet or
|
|
78
|
+
* re-introduce itself. Set `false` for an explicit visible transfer (the
|
|
79
|
+
* target follows its own instructions, e.g. "Bill here").
|
|
80
|
+
*/
|
|
81
|
+
silentHandoff?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Structured goal/thread tracking (G5). When enabled, a cheap control-model
|
|
84
|
+
* pass at turn end patches `session.workingMemory.__goals` and open threads
|
|
85
|
+
* are projected into the next turn's prompt. Default off — opt-in cost/latency.
|
|
86
|
+
*/
|
|
87
|
+
trackGoals?: boolean;
|
|
88
|
+
/** Read-only observability, configured independently from durable session state. */
|
|
89
|
+
tracing?: TracingConfig;
|
|
59
90
|
}
|
|
60
91
|
export interface RunOptions {
|
|
61
92
|
sessionId?: string;
|
|
@@ -75,6 +106,8 @@ export interface RunOptions {
|
|
|
75
106
|
historyDelta?: ModelMessage[];
|
|
76
107
|
driver?: ChannelDriver;
|
|
77
108
|
signalDelivery?: SignalDelivery;
|
|
109
|
+
/** Stable key for this inbound user message; duplicate webhook retries are ignored (H2). */
|
|
110
|
+
idempotencyKey?: string;
|
|
78
111
|
abortSignal?: AbortSignal;
|
|
79
112
|
}
|
|
80
113
|
export declare class Runtime {
|
|
@@ -87,10 +120,17 @@ export declare class Runtime {
|
|
|
87
120
|
private readonly hooks?;
|
|
88
121
|
private readonly activeTurnAborts;
|
|
89
122
|
private readonly sessionMutex;
|
|
123
|
+
private readonly traceStore?;
|
|
124
|
+
private readonly traceSinks;
|
|
125
|
+
private readonly pendingTraceWrites;
|
|
90
126
|
constructor(config: HarnessConfig);
|
|
91
127
|
run(opts: RunOptions): TurnHandle;
|
|
128
|
+
runOnce(opts: RunOptions): Promise<AgentTrace>;
|
|
92
129
|
stream(opts: RunOptions): TurnHandle;
|
|
93
130
|
getSession(sessionId: string): Promise<Session | null>;
|
|
131
|
+
getTrace(traceId: string): Promise<AgentTrace | null>;
|
|
132
|
+
listTraces(sessionId: string): Promise<AgentTrace[]>;
|
|
133
|
+
getTraceStore(): TraceStore | undefined;
|
|
94
134
|
getSessionStore(): SessionStore;
|
|
95
135
|
deleteSession(sessionId: string): Promise<void>;
|
|
96
136
|
abortSession(sessionId: string, reason?: string): void;
|
|
@@ -99,6 +139,10 @@ export declare class Runtime {
|
|
|
99
139
|
reason?: string;
|
|
100
140
|
markedBy?: ConversationOutcomeMarkedBy;
|
|
101
141
|
}): Promise<void>;
|
|
142
|
+
private shouldTrace;
|
|
143
|
+
private writeSpan;
|
|
144
|
+
private settleTraceWrites;
|
|
145
|
+
private flushTraceSinks;
|
|
102
146
|
/**
|
|
103
147
|
* Compact `runState.messages` when over the configured trigger (or always,
|
|
104
148
|
* when `force`). Persists both the run state and the session message mirror.
|