@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/Runtime.js
CHANGED
|
@@ -7,10 +7,16 @@ import { createEventBus, createTurnHandle } from '../events/TurnHandle.js';
|
|
|
7
7
|
import { CoreToolExecutor } from '../tools/effect/index.js';
|
|
8
8
|
import { buildAgentToolSurface } from './buildAgentToolSurface.js';
|
|
9
9
|
import { hostLoop } from './hostLoop.js';
|
|
10
|
+
import { isHandoffOscillating } from './handoffOscillation.js';
|
|
11
|
+
import { applyHandoffContinuation } from './handoffContinuation.js';
|
|
10
12
|
import { isDegradableRuntimeError } from '../flow/degradableErrors.js';
|
|
11
13
|
import { SAFE_DEGRADED_MESSAGE } from '../flow/degrade.js';
|
|
12
14
|
import { adaptHostSelect } from './hostClassifyAdapter.js';
|
|
13
15
|
import { openRun, sessionDerivedRunId } from './openRun.js';
|
|
16
|
+
function resolveOutOfBandControl(agent) {
|
|
17
|
+
const hasFlows = (agent.flows?.length ?? 0) > 0;
|
|
18
|
+
return agent.experimental?.outOfBandControl ?? hasFlows;
|
|
19
|
+
}
|
|
14
20
|
import { closeRun } from './closeRun.js';
|
|
15
21
|
import { SessionRunStore } from './durable/SessionRunStore.js';
|
|
16
22
|
import { loadRecordedSteps } from './durable/replay.js';
|
|
@@ -19,8 +25,16 @@ import { resolveAgentPolicies } from './policies/resolvePolicies.js';
|
|
|
19
25
|
import { buildAutoRetrieveProvider, buildKnowledgeProvider, buildMemoryService, runMemoryIngest, } from './grounding/index.js';
|
|
20
26
|
import { SessionMutex } from './SessionMutex.js';
|
|
21
27
|
import { compactMessages } from './compaction.js';
|
|
28
|
+
import { readLastPromptTokens, readCumulativeUsage, computeTurnTraceUsage } from './turnTokenUsage.js';
|
|
22
29
|
import { isContextOverflowError, recoverFromContextOverflow } from './contextOverflow.js';
|
|
30
|
+
import { projectGoalsPromptFromState, updateGoalsFromTurn } from './goals.js';
|
|
23
31
|
import { buildEscalationRequest, recordEscalationOutcome, ESCALATION_NOTIFIED_KEY, } from '../escalation/escalation.js';
|
|
32
|
+
import { runOnce as recordRunOnce } from './TraceRecorder.js';
|
|
33
|
+
import { TraceRecorder } from './TraceRecorder.js';
|
|
34
|
+
import { MemoryTraceStore } from '../tracing/MemoryTraceStore.js';
|
|
35
|
+
import { mutateSessionWithRetry } from '../session/utils.js';
|
|
36
|
+
import { isTraceStore } from '../tracing/TraceStore.js';
|
|
37
|
+
import { runHookSafely } from './runHookSafely.js';
|
|
24
38
|
export class Runtime {
|
|
25
39
|
config;
|
|
26
40
|
agentsById;
|
|
@@ -31,6 +45,9 @@ export class Runtime {
|
|
|
31
45
|
hooks;
|
|
32
46
|
activeTurnAborts = new Map();
|
|
33
47
|
sessionMutex = new SessionMutex();
|
|
48
|
+
traceStore;
|
|
49
|
+
traceSinks;
|
|
50
|
+
pendingTraceWrites = new Set();
|
|
34
51
|
constructor(config) {
|
|
35
52
|
this.config = config;
|
|
36
53
|
this.agentsById = indexAgents(config.agents);
|
|
@@ -39,12 +56,28 @@ export class Runtime {
|
|
|
39
56
|
this.maxHandoffs = config.maxHandoffs ?? 5;
|
|
40
57
|
this.terminalHandoffTargets = new Set(config.terminalHandoffTargets ?? ['human']);
|
|
41
58
|
this.hooks = config.hooks;
|
|
59
|
+
const configuredSinks = config.tracing?.sinks ?? [];
|
|
60
|
+
const configuredStore = config.tracing?.store ?? configuredSinks.find(isTraceStore);
|
|
61
|
+
this.traceStore = config.tracing?.enabled === false
|
|
62
|
+
? undefined
|
|
63
|
+
: configuredStore ?? new MemoryTraceStore();
|
|
64
|
+
this.traceSinks = this.traceStore
|
|
65
|
+
? [this.traceStore, ...configuredSinks.filter((sink) => sink !== this.traceStore)]
|
|
66
|
+
: [];
|
|
42
67
|
}
|
|
43
68
|
run(opts) {
|
|
44
69
|
if (opts.wake && opts.input !== undefined) {
|
|
45
70
|
throw new Error('RunOptions.wake and RunOptions.input are mutually exclusive');
|
|
46
71
|
}
|
|
47
72
|
const sessionId = opts.sessionId || randomUUID();
|
|
73
|
+
const recorder = this.shouldTrace(sessionId, opts.input)
|
|
74
|
+
? new TraceRecorder({
|
|
75
|
+
sessionId,
|
|
76
|
+
agentId: opts.agentId ?? this.config.defaultAgentId,
|
|
77
|
+
input: opts.input,
|
|
78
|
+
onSpan: (span) => this.writeSpan(span),
|
|
79
|
+
})
|
|
80
|
+
: undefined;
|
|
48
81
|
const bus = createEventBus();
|
|
49
82
|
const abortController = new AbortController();
|
|
50
83
|
this.activeTurnAborts.set(sessionId, abortController);
|
|
@@ -54,8 +87,11 @@ export class Runtime {
|
|
|
54
87
|
const execute = async () => {
|
|
55
88
|
let runCtx;
|
|
56
89
|
const emit = (part) => {
|
|
90
|
+
recorder?.record(part);
|
|
91
|
+
if (part.type === 'done')
|
|
92
|
+
this.flushTraceSinks();
|
|
57
93
|
bus.emit(part);
|
|
58
|
-
void this.hooks?.onStreamPart?.(runCtx, part);
|
|
94
|
+
void runHookSafely('onStreamPart', () => this.hooks?.onStreamPart?.(runCtx, part));
|
|
59
95
|
};
|
|
60
96
|
const opened = await openRun(this.agentsById, {
|
|
61
97
|
sessionId,
|
|
@@ -67,10 +103,12 @@ export class Runtime {
|
|
|
67
103
|
seedMessages: opts.seedMessages,
|
|
68
104
|
historyDelta: opts.historyDelta,
|
|
69
105
|
signalDelivery: opts.signalDelivery,
|
|
106
|
+
idempotencyKey: opts.idempotencyKey,
|
|
70
107
|
transcriptionModel: this.config.transcriptionModel,
|
|
71
108
|
defaultAgentId: this.config.defaultAgentId,
|
|
72
109
|
sessionStore: this.sessionStore,
|
|
73
110
|
});
|
|
111
|
+
recorder?.setInitiatingAgent(opened.agent.id);
|
|
74
112
|
const policies = resolveAgentPolicies(opened.agent);
|
|
75
113
|
const knowledgeProvider = this.config.knowledge
|
|
76
114
|
? buildKnowledgeProvider(this.config.knowledge)
|
|
@@ -86,13 +124,24 @@ export class Runtime {
|
|
|
86
124
|
agentId: opened.agent.id,
|
|
87
125
|
onInterim: (message) => {
|
|
88
126
|
const id = crypto.randomUUID();
|
|
89
|
-
emit({ type: 'text-start', id });
|
|
90
|
-
emit({ type: 'text-delta', id, delta: message });
|
|
91
|
-
emit({ type: 'text-end', id });
|
|
127
|
+
emit({ channel: 'client', type: 'text-start', payload: { id } });
|
|
128
|
+
emit({ channel: 'client', type: 'text-delta', payload: { id, delta: message } });
|
|
129
|
+
emit({ channel: 'client', type: 'text-end', payload: { id } });
|
|
130
|
+
},
|
|
131
|
+
onChunk: (chunk, toolName, toolCallId) => {
|
|
132
|
+
emit({
|
|
133
|
+
channel: 'internal',
|
|
134
|
+
type: 'tool-result',
|
|
135
|
+
payload: { toolName, result: chunk, toolCallId, preliminary: true },
|
|
136
|
+
});
|
|
92
137
|
},
|
|
93
138
|
});
|
|
94
139
|
const steps = await loadRecordedSteps(opened.runStore, opened.runState.runId);
|
|
95
140
|
const freshRunState = (await opened.runStore.getRunState(opened.runState.runId)) ?? opened.runState;
|
|
141
|
+
// Snapshot cumulative token usage as this turn opens, so the trace can report
|
|
142
|
+
// THIS turn's consumption (delta), not the running session total (see the
|
|
143
|
+
// per-turn scope requirement in the observability guide).
|
|
144
|
+
const usageBaseline = readCumulativeUsage(freshRunState.state);
|
|
96
145
|
const model = opened.agent.model ?? this.defaultModel;
|
|
97
146
|
if (!model) {
|
|
98
147
|
throw new Error('Runtime requires agent.model or config.defaultModel');
|
|
@@ -120,16 +169,20 @@ export class Runtime {
|
|
|
120
169
|
: undefined,
|
|
121
170
|
fs: openingSurface.resolvedWorkspace?.fs,
|
|
122
171
|
});
|
|
172
|
+
// Session retrieval cache (G6): created once per run, persists across
|
|
173
|
+
// in-session handoffs (runCtx survives the handoff branch). RAG-only —
|
|
174
|
+
// a knowledge-less runtime leaves it undefined.
|
|
175
|
+
runCtx.retrievalCache = knowledgeProvider?.createSessionCache();
|
|
123
176
|
// Agent base layer (ADR 0001): composed into every node turn by the drivers.
|
|
124
177
|
runCtx.baseInstructions = opened.agent.instructions;
|
|
125
178
|
runCtx.globalTools = openingSurface.globalTools;
|
|
126
|
-
runCtx.outOfBandControl = opened.agent
|
|
179
|
+
runCtx.outOfBandControl = resolveOutOfBandControl(opened.agent);
|
|
127
180
|
runCtx.skillPrompt = openingSurface.skillPrompt;
|
|
128
|
-
runCtx.workingMemoryPrompt = openingSurface.workingMemoryPrompt;
|
|
181
|
+
runCtx.workingMemoryPrompt = appendGoalsPrompt(openingSurface.workingMemoryPrompt, opened.session.workingMemory);
|
|
129
182
|
runCtx.workingMemoryTools = openingSurface.workingMemoryTools;
|
|
130
|
-
await this.hooks?.onStart?.(runCtx);
|
|
183
|
+
await runHookSafely('onStart', () => this.hooks?.onStart?.(runCtx));
|
|
131
184
|
if (opts.wake) {
|
|
132
|
-
emit({ type: 'wake', reason: opts.wake.reason });
|
|
185
|
+
emit({ channel: 'internal', type: 'wake', payload: { reason: opts.wake.reason } });
|
|
133
186
|
}
|
|
134
187
|
const driver = opts.driver ?? new TextDriver();
|
|
135
188
|
let activeAgent = opened.agent;
|
|
@@ -159,7 +212,11 @@ export class Runtime {
|
|
|
159
212
|
}
|
|
160
213
|
if (loopResult.kind === 'handoff') {
|
|
161
214
|
if (this.terminalHandoffTargets.has(loopResult.to)) {
|
|
162
|
-
emit({
|
|
215
|
+
emit({
|
|
216
|
+
channel: 'internal',
|
|
217
|
+
type: 'handoff',
|
|
218
|
+
payload: { targetAgent: loopResult.to, reason: loopResult.reason },
|
|
219
|
+
});
|
|
163
220
|
runCtx.runState.status = 'paused';
|
|
164
221
|
await runCtx.runStore.putRunState(runCtx.runState);
|
|
165
222
|
await this.dispatchEscalation(runCtx, activeAgent, { reason: loopResult.reason ?? 'handoff_to_human', category: loopResult.category }, emit, { setLatch: false });
|
|
@@ -169,6 +226,14 @@ export class Runtime {
|
|
|
169
226
|
if (handoffCount > this.maxHandoffs) {
|
|
170
227
|
throw new Error(`maxHandoffs exceeded (${this.maxHandoffs})`);
|
|
171
228
|
}
|
|
229
|
+
// Cross-turn ping-pong safeguard (handoffCount resets each turn, so it
|
|
230
|
+
// can't catch A↔B oscillation spread across turns). This is a bound
|
|
231
|
+
// ABOVE maxHandoffs: within-run runaway is caught by the maxHandoffs
|
|
232
|
+
// check above; oscillation only fires for same-pair accumulation in the
|
|
233
|
+
// persisted handoffHistory beyond that, so it never pre-empts maxHandoffs.
|
|
234
|
+
if (isHandoffOscillating(opened.session.handoffHistory, runCtx.runState.activeAgentId, loopResult.to, this.maxHandoffs + 1)) {
|
|
235
|
+
throw new Error(`Handoff oscillation detected between "${runCtx.runState.activeAgentId}" and "${loopResult.to}"`);
|
|
236
|
+
}
|
|
172
237
|
const target = this.agentsById.get(loopResult.to);
|
|
173
238
|
if (!target) {
|
|
174
239
|
throw new Error(`Handoff target agent not found: ${loopResult.to}`);
|
|
@@ -179,6 +244,20 @@ export class Runtime {
|
|
|
179
244
|
reason: loopResult.reason ?? 'handoff',
|
|
180
245
|
timestamp: new Date(),
|
|
181
246
|
});
|
|
247
|
+
const handoffTarget = loopResult.to;
|
|
248
|
+
const routeFilter = activeAgent.routes?.find((r) => r.agent === handoffTarget)?.filter;
|
|
249
|
+
const inputFilter = routeFilter ?? this.config.handoffInputFilter;
|
|
250
|
+
if (inputFilter) {
|
|
251
|
+
const filtered = await inputFilter({
|
|
252
|
+
messages: runCtx.runState.messages,
|
|
253
|
+
workingMemory: runCtx.session.workingMemory,
|
|
254
|
+
sourceAgentId: runCtx.runState.activeAgentId,
|
|
255
|
+
targetAgentId: handoffTarget,
|
|
256
|
+
reason: loopResult.reason,
|
|
257
|
+
});
|
|
258
|
+
runCtx.runState.messages = filtered.messages;
|
|
259
|
+
runCtx.session.workingMemory = filtered.workingMemory;
|
|
260
|
+
}
|
|
182
261
|
runCtx.runState.activeAgentId = loopResult.to;
|
|
183
262
|
activeAgent = target;
|
|
184
263
|
const targetSurface = await buildAgentToolSurface(target, opened.session, {
|
|
@@ -191,12 +270,51 @@ export class Runtime {
|
|
|
191
270
|
: undefined;
|
|
192
271
|
runCtx.globalTools = targetSurface.globalTools;
|
|
193
272
|
runCtx.skillPrompt = targetSurface.skillPrompt;
|
|
194
|
-
runCtx.workingMemoryPrompt = targetSurface.workingMemoryPrompt;
|
|
273
|
+
runCtx.workingMemoryPrompt = appendGoalsPrompt(targetSurface.workingMemoryPrompt, runCtx.session.workingMemory);
|
|
195
274
|
runCtx.workingMemoryTools = targetSurface.workingMemoryTools;
|
|
196
275
|
runCtx.fs = targetSurface.resolvedWorkspace?.fs;
|
|
197
276
|
runCtx.memoryService = this.config.memoryService
|
|
198
277
|
? buildMemoryService(this.config.memoryService, target)
|
|
199
278
|
: undefined;
|
|
279
|
+
const targetPolicies = resolveAgentPolicies(target);
|
|
280
|
+
const targetModel = target.model ?? this.defaultModel;
|
|
281
|
+
if (!targetModel) {
|
|
282
|
+
throw new Error('Runtime requires agent.model or config.defaultModel');
|
|
283
|
+
}
|
|
284
|
+
runCtx.baseInstructions =
|
|
285
|
+
(this.config.silentHandoff ?? true)
|
|
286
|
+
? applyHandoffContinuation(target.instructions)
|
|
287
|
+
: target.instructions;
|
|
288
|
+
runCtx.model = targetModel;
|
|
289
|
+
runCtx.controlModel = target.controlModel ?? targetModel;
|
|
290
|
+
runCtx.outOfBandControl = resolveOutOfBandControl(target);
|
|
291
|
+
runCtx.limits = targetPolicies.limits;
|
|
292
|
+
runCtx.refinementPolicies = targetPolicies.refinementPolicies;
|
|
293
|
+
runCtx.validationPolicies = targetPolicies.validationPolicies;
|
|
294
|
+
runCtx.inputProcessors = targetPolicies.inputProcessors;
|
|
295
|
+
runCtx.outputProcessors = targetPolicies.outputProcessors;
|
|
296
|
+
runCtx.toolExecutor = new CoreToolExecutor({
|
|
297
|
+
tools: targetSurface.executorTools,
|
|
298
|
+
enforcer: targetPolicies.enforcer,
|
|
299
|
+
agentId: target.id,
|
|
300
|
+
onInterim: (message) => {
|
|
301
|
+
const id = crypto.randomUUID();
|
|
302
|
+
emit({ channel: 'client', type: 'text-start', payload: { id } });
|
|
303
|
+
emit({
|
|
304
|
+
channel: 'client',
|
|
305
|
+
type: 'text-delta',
|
|
306
|
+
payload: { id, delta: message },
|
|
307
|
+
});
|
|
308
|
+
emit({ channel: 'client', type: 'text-end', payload: { id } });
|
|
309
|
+
},
|
|
310
|
+
onChunk: (chunk, toolName, toolCallId) => {
|
|
311
|
+
emit({
|
|
312
|
+
channel: 'internal',
|
|
313
|
+
type: 'tool-result',
|
|
314
|
+
payload: { toolName, result: chunk, toolCallId, preliminary: true },
|
|
315
|
+
});
|
|
316
|
+
},
|
|
317
|
+
});
|
|
200
318
|
await runCtx.runStore.putRunState(runCtx.runState);
|
|
201
319
|
continue;
|
|
202
320
|
}
|
|
@@ -221,14 +339,18 @@ export class Runtime {
|
|
|
221
339
|
await this.applyCompaction(runCtx, activeAgent, emit, false);
|
|
222
340
|
}
|
|
223
341
|
catch (error) {
|
|
224
|
-
await this.hooks?.onError?.(runCtx, error);
|
|
342
|
+
await runHookSafely('onError', () => this.hooks?.onError?.(runCtx, error));
|
|
225
343
|
if (isDegradableRuntimeError(error)) {
|
|
226
344
|
const message = error instanceof Error ? error.message : String(error);
|
|
227
|
-
emit({ type: 'error', error: message });
|
|
345
|
+
emit({ channel: 'client', type: 'error', payload: { error: message } });
|
|
228
346
|
const degradedId = crypto.randomUUID();
|
|
229
|
-
emit({ type: 'text-start', id: degradedId });
|
|
230
|
-
emit({
|
|
231
|
-
|
|
347
|
+
emit({ channel: 'client', type: 'text-start', payload: { id: degradedId } });
|
|
348
|
+
emit({
|
|
349
|
+
channel: 'client',
|
|
350
|
+
type: 'text-delta',
|
|
351
|
+
payload: { id: degradedId, delta: SAFE_DEGRADED_MESSAGE },
|
|
352
|
+
});
|
|
353
|
+
emit({ channel: 'client', type: 'text-end', payload: { id: degradedId } });
|
|
232
354
|
runCtx.runState.messages = [
|
|
233
355
|
...runCtx.runState.messages,
|
|
234
356
|
{ role: 'assistant', content: SAFE_DEGRADED_MESSAGE },
|
|
@@ -254,10 +376,22 @@ export class Runtime {
|
|
|
254
376
|
outcomeReason: loopResult.kind === 'ended' ? loopResult.reason : undefined,
|
|
255
377
|
memoryIngest: async () => {
|
|
256
378
|
await runMemoryIngest(runCtx);
|
|
379
|
+
if (this.config.trackGoals) {
|
|
380
|
+
const controlModel = this.agentsById.get(runCtx.runState.activeAgentId)?.controlModel ??
|
|
381
|
+
runCtx.controlModel;
|
|
382
|
+
await updateGoalsFromTurn(runCtx, controlModel);
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
await runHookSafely('onEnd', () => this.hooks?.onEnd?.(runCtx));
|
|
387
|
+
emit({
|
|
388
|
+
channel: 'client',
|
|
389
|
+
type: 'done',
|
|
390
|
+
payload: {
|
|
391
|
+
sessionId: opened.session.id,
|
|
392
|
+
usage: computeTurnTraceUsage(usageBaseline, runCtx.runState.state),
|
|
257
393
|
},
|
|
258
394
|
});
|
|
259
|
-
await this.hooks?.onEnd?.(runCtx);
|
|
260
|
-
emit({ type: 'done', sessionId: opened.session.id });
|
|
261
395
|
}
|
|
262
396
|
return { text: collectAssistantText(runCtx.runState.messages), toolResults: [] };
|
|
263
397
|
};
|
|
@@ -276,12 +410,35 @@ export class Runtime {
|
|
|
276
410
|
run: gated,
|
|
277
411
|
});
|
|
278
412
|
}
|
|
413
|
+
async runOnce(opts) {
|
|
414
|
+
const existing = opts.sessionId ? await this.sessionStore.get(opts.sessionId) : null;
|
|
415
|
+
// Caller's explicit agentId wins, matching run() (`opts.agentId ?? defaultAgentId`).
|
|
416
|
+
// Persisted state is the fallback, not an override — otherwise the two public
|
|
417
|
+
// entry points disagree about which agent handles the same turn.
|
|
418
|
+
const agentId = opts.agentId ?? existing?.activeAgentId ?? existing?.currentAgent ?? this.config.defaultAgentId;
|
|
419
|
+
return recordRunOnce(this, { ...opts, agentId });
|
|
420
|
+
}
|
|
279
421
|
stream(opts) {
|
|
280
422
|
return this.run(opts);
|
|
281
423
|
}
|
|
282
424
|
async getSession(sessionId) {
|
|
283
425
|
return this.sessionStore.get(sessionId);
|
|
284
426
|
}
|
|
427
|
+
async getTrace(traceId) {
|
|
428
|
+
await this.settleTraceWrites();
|
|
429
|
+
return this.traceStore?.getTrace(traceId) ?? null;
|
|
430
|
+
}
|
|
431
|
+
async listTraces(sessionId) {
|
|
432
|
+
await this.settleTraceWrites();
|
|
433
|
+
return this.traceStore?.listTraces(sessionId) ?? [];
|
|
434
|
+
}
|
|
435
|
+
getTraceStore() {
|
|
436
|
+
return this.traceStore;
|
|
437
|
+
}
|
|
438
|
+
/** The agent used when neither the caller nor persisted state names one. */
|
|
439
|
+
getDefaultAgentId() {
|
|
440
|
+
return this.config.defaultAgentId;
|
|
441
|
+
}
|
|
285
442
|
getSessionStore() {
|
|
286
443
|
return this.sessionStore;
|
|
287
444
|
}
|
|
@@ -308,6 +465,65 @@ export class Runtime {
|
|
|
308
465
|
markedBy: opts?.markedBy ?? 'http',
|
|
309
466
|
});
|
|
310
467
|
}
|
|
468
|
+
shouldTrace(sessionId, input) {
|
|
469
|
+
if (this.traceSinks.length === 0)
|
|
470
|
+
return false;
|
|
471
|
+
const sampling = this.config.tracing?.sampling;
|
|
472
|
+
if (typeof sampling === 'function') {
|
|
473
|
+
try {
|
|
474
|
+
return sampling({ sessionId, input });
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (sampling === undefined)
|
|
481
|
+
return true;
|
|
482
|
+
return sampling > 0 && (sampling >= 1 || Math.random() < sampling);
|
|
483
|
+
}
|
|
484
|
+
writeSpan(original) {
|
|
485
|
+
let span = original;
|
|
486
|
+
try {
|
|
487
|
+
if (this.config.tracing?.redact) {
|
|
488
|
+
span = this.config.tracing.redact(structuredClone(original));
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (!span)
|
|
495
|
+
return;
|
|
496
|
+
for (const sink of this.traceSinks) {
|
|
497
|
+
try {
|
|
498
|
+
const result = sink.write(span);
|
|
499
|
+
if (result instanceof Promise) {
|
|
500
|
+
const pending = result.catch(() => { }).finally(() => this.pendingTraceWrites.delete(pending));
|
|
501
|
+
this.pendingTraceWrites.add(pending);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
// Traces are derived observability and never participate in run correctness.
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
async settleTraceWrites() {
|
|
510
|
+
await Promise.allSettled([...this.pendingTraceWrites]);
|
|
511
|
+
await Promise.allSettled(this.traceSinks.map((sink) => sink.flush?.()));
|
|
512
|
+
}
|
|
513
|
+
flushTraceSinks() {
|
|
514
|
+
for (const sink of this.traceSinks) {
|
|
515
|
+
try {
|
|
516
|
+
const result = sink.flush?.();
|
|
517
|
+
if (result) {
|
|
518
|
+
const pending = result.catch(() => { }).finally(() => this.pendingTraceWrites.delete(pending));
|
|
519
|
+
this.pendingTraceWrites.add(pending);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
// Export flushes are observational and never affect the run.
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
311
527
|
/**
|
|
312
528
|
* Compact `runState.messages` when over the configured trigger (or always,
|
|
313
529
|
* when `force`). Persists both the run state and the session message mirror.
|
|
@@ -327,27 +543,33 @@ export class Runtime {
|
|
|
327
543
|
model,
|
|
328
544
|
config,
|
|
329
545
|
force,
|
|
546
|
+
lastPromptTokens: readLastPromptTokens(runCtx.runState.state),
|
|
330
547
|
});
|
|
331
548
|
if (!result.compacted) {
|
|
332
549
|
if (force) {
|
|
333
|
-
emit({
|
|
550
|
+
emit({
|
|
551
|
+
channel: 'internal',
|
|
552
|
+
type: 'compaction-skipped',
|
|
553
|
+
payload: { reason: result.reason },
|
|
554
|
+
});
|
|
334
555
|
}
|
|
335
556
|
return false;
|
|
336
557
|
}
|
|
337
558
|
runCtx.runState.messages = result.messages;
|
|
338
559
|
runCtx.runState.updatedAt = Date.now();
|
|
339
560
|
await runCtx.runStore.putRunState(runCtx.runState);
|
|
340
|
-
|
|
341
|
-
if (latest) {
|
|
561
|
+
await mutateSessionWithRetry(this.sessionStore, runCtx.session.id, (latest) => {
|
|
342
562
|
latest.messages = [...result.messages];
|
|
343
|
-
|
|
344
|
-
}
|
|
563
|
+
});
|
|
345
564
|
runCtx.session.messages = [...result.messages];
|
|
346
565
|
emit({
|
|
566
|
+
channel: 'internal',
|
|
347
567
|
type: 'context-compacted',
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
568
|
+
payload: {
|
|
569
|
+
beforeTokens: result.beforeTokens,
|
|
570
|
+
afterTokens: result.afterTokens,
|
|
571
|
+
summarizedCount: result.summarizedCount,
|
|
572
|
+
},
|
|
351
573
|
});
|
|
352
574
|
return true;
|
|
353
575
|
}
|
|
@@ -362,9 +584,12 @@ export class Runtime {
|
|
|
362
584
|
runCtx.runState.messages = runCtx.session.messages;
|
|
363
585
|
const compacted = await this.applyCompaction(runCtx, agent, emit, true);
|
|
364
586
|
emit({
|
|
587
|
+
channel: 'internal',
|
|
365
588
|
type: 'context-overflow-recovered',
|
|
366
|
-
|
|
367
|
-
|
|
589
|
+
payload: {
|
|
590
|
+
strippedCount: recovery.strippedCount,
|
|
591
|
+
compacted,
|
|
592
|
+
},
|
|
368
593
|
});
|
|
369
594
|
}
|
|
370
595
|
async getConversationLength(sessionId) {
|
|
@@ -409,18 +634,23 @@ export class Runtime {
|
|
|
409
634
|
error: error instanceof Error ? error.message : String(error),
|
|
410
635
|
};
|
|
411
636
|
}
|
|
412
|
-
recordEscalationOutcome(runCtx.session, info.category ?? 'user-request', outcome);
|
|
413
637
|
if (opts.setLatch) {
|
|
414
638
|
runCtx.runState.state[ESCALATION_NOTIFIED_KEY] = true;
|
|
415
639
|
}
|
|
416
640
|
await runCtx.runStore.putRunState(runCtx.runState);
|
|
417
|
-
await this.sessionStore
|
|
641
|
+
const latestSession = await mutateSessionWithRetry(this.sessionStore, runCtx.session.id, (latest) => {
|
|
642
|
+
recordEscalationOutcome(latest, info.category ?? 'user-request', outcome);
|
|
643
|
+
});
|
|
644
|
+
runCtx.session.metadata = latestSession.metadata;
|
|
418
645
|
emit({
|
|
646
|
+
channel: 'internal',
|
|
419
647
|
type: 'escalation',
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
648
|
+
payload: {
|
|
649
|
+
reason: info.reason,
|
|
650
|
+
category: info.category,
|
|
651
|
+
outcome: outcome.status,
|
|
652
|
+
summary: request.summary,
|
|
653
|
+
},
|
|
424
654
|
});
|
|
425
655
|
}
|
|
426
656
|
/**
|
|
@@ -451,9 +681,9 @@ export class Runtime {
|
|
|
451
681
|
delete runState.state[ESCALATION_NOTIFIED_KEY];
|
|
452
682
|
runState.updatedAt = Date.now();
|
|
453
683
|
await runStore.putRunState(runState);
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
684
|
+
await mutateSessionWithRetry(this.sessionStore, sessionId, (latest) => {
|
|
685
|
+
latest.messages = [...runState.messages];
|
|
686
|
+
});
|
|
457
687
|
}
|
|
458
688
|
}
|
|
459
689
|
export function createRuntime(config) {
|
|
@@ -476,3 +706,10 @@ function collectAssistantText(messages) {
|
|
|
476
706
|
}
|
|
477
707
|
return '';
|
|
478
708
|
}
|
|
709
|
+
function appendGoalsPrompt(workingMemoryPrompt, workingMemory) {
|
|
710
|
+
const goalsPrompt = projectGoalsPromptFromState(workingMemory);
|
|
711
|
+
if (!goalsPrompt) {
|
|
712
|
+
return workingMemoryPrompt;
|
|
713
|
+
}
|
|
714
|
+
return [workingMemoryPrompt, goalsPrompt].filter((part) => part && part.trim()).join('\n\n');
|
|
715
|
+
}
|
|
@@ -12,6 +12,13 @@ export declare class TokenAccumulator {
|
|
|
12
12
|
private _peakUtil;
|
|
13
13
|
private _cumCacheRead;
|
|
14
14
|
constructor(contextWindow?: number | undefined);
|
|
15
|
+
/** Rehydrate cumulative totals from persisted session state (cross-turn). */
|
|
16
|
+
restoreCumulative(saved: {
|
|
17
|
+
inputTokens: number;
|
|
18
|
+
outputTokens: number;
|
|
19
|
+
totalTokens: number;
|
|
20
|
+
cacheReadTokens?: number;
|
|
21
|
+
}): void;
|
|
15
22
|
record(usage: TurnUsageInput): TurnUsage;
|
|
16
23
|
get cumulative(): {
|
|
17
24
|
inputTokens: number;
|
|
@@ -12,6 +12,13 @@ export class TokenAccumulator {
|
|
|
12
12
|
constructor(contextWindow) {
|
|
13
13
|
this.contextWindow = contextWindow;
|
|
14
14
|
}
|
|
15
|
+
/** Rehydrate cumulative totals from persisted session state (cross-turn). */
|
|
16
|
+
restoreCumulative(saved) {
|
|
17
|
+
this._cumInput = saved.inputTokens;
|
|
18
|
+
this._cumOutput = saved.outputTokens;
|
|
19
|
+
this._cumTotal = saved.totalTokens;
|
|
20
|
+
this._cumCacheRead = saved.cacheReadTokens ?? 0;
|
|
21
|
+
}
|
|
15
22
|
record(usage) {
|
|
16
23
|
this._cumInput += usage.inputTokens;
|
|
17
24
|
this._cumOutput += usage.outputTokens;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { TurnResult } from '../types/channel.js';
|
|
2
|
+
import type { StreamPart, TurnHandle } from '../types/stream.js';
|
|
3
|
+
import type { AgentSpan, AgentTrace } from '../types/trace.js';
|
|
4
|
+
import type { RunOptions } from './Runtime.js';
|
|
5
|
+
export interface TraceRecorderOptions {
|
|
6
|
+
sessionId?: string;
|
|
7
|
+
agentId?: string;
|
|
8
|
+
input?: unknown;
|
|
9
|
+
onSpan?: (span: AgentSpan) => void;
|
|
10
|
+
}
|
|
11
|
+
export declare class TraceRecorder {
|
|
12
|
+
private readonly trace;
|
|
13
|
+
private readonly root;
|
|
14
|
+
private currentFlow?;
|
|
15
|
+
private currentNode?;
|
|
16
|
+
private readonly openTools;
|
|
17
|
+
private readonly toolCallIds;
|
|
18
|
+
private readonly emitted;
|
|
19
|
+
private readonly onSpan?;
|
|
20
|
+
private currentAgentId?;
|
|
21
|
+
constructor(options?: TraceRecorderOptions);
|
|
22
|
+
setInitiatingAgent(agentId: string): void;
|
|
23
|
+
record(part: StreamPart): void;
|
|
24
|
+
finish(result: TurnResult): AgentTrace;
|
|
25
|
+
private openSpan;
|
|
26
|
+
private takeToolSpan;
|
|
27
|
+
private closeTools;
|
|
28
|
+
private closeNode;
|
|
29
|
+
private closeFlow;
|
|
30
|
+
private setSessionId;
|
|
31
|
+
private close;
|
|
32
|
+
private emitSpan;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* What the standalone {@link runOnce} needs from a runtime. `run` is required;
|
|
36
|
+
* the optional accessors let it resolve the same agent `Runtime.runOnce` would.
|
|
37
|
+
*
|
|
38
|
+
* It must NOT call `runtime.runOnce` — that method delegates here, so delegating
|
|
39
|
+
* back is infinite recursion.
|
|
40
|
+
*/
|
|
41
|
+
export interface RunOnceRuntime {
|
|
42
|
+
run(opts: RunOptions): TurnHandle;
|
|
43
|
+
getSessionStore?(): {
|
|
44
|
+
get(id: string): Promise<{
|
|
45
|
+
activeAgentId?: string;
|
|
46
|
+
currentAgent?: string;
|
|
47
|
+
} | null>;
|
|
48
|
+
};
|
|
49
|
+
getDefaultAgentId?(): string;
|
|
50
|
+
}
|
|
51
|
+
export declare function runOnce(runtime: RunOnceRuntime, opts: RunOptions): Promise<AgentTrace>;
|