@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/testing/mocks.js
CHANGED
|
@@ -37,12 +37,16 @@ export function createMockSession(partial = {}) {
|
|
|
37
37
|
handoffHistory: partial.handoffHistory ?? [],
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
|
+
function isAsyncIterable(value) {
|
|
41
|
+
return Symbol.asyncIterator in value;
|
|
42
|
+
}
|
|
40
43
|
export function createMockRuntime(parts, options = {}) {
|
|
41
44
|
const sessions = options.sessions ?? new Map();
|
|
42
45
|
const run = (opts) => {
|
|
43
46
|
if (typeof parts === 'function') {
|
|
44
47
|
throw parts();
|
|
45
48
|
}
|
|
49
|
+
const eventSource = parts;
|
|
46
50
|
options.onRun?.({
|
|
47
51
|
sessionId: opts.sessionId,
|
|
48
52
|
input: opts.input,
|
|
@@ -50,13 +54,13 @@ export function createMockRuntime(parts, options = {}) {
|
|
|
50
54
|
seedMessages: opts.seedMessages,
|
|
51
55
|
});
|
|
52
56
|
async function* events() {
|
|
53
|
-
if (
|
|
54
|
-
for await (const part of
|
|
57
|
+
if (isAsyncIterable(eventSource)) {
|
|
58
|
+
for await (const part of eventSource) {
|
|
55
59
|
yield part;
|
|
56
60
|
}
|
|
57
61
|
return;
|
|
58
62
|
}
|
|
59
|
-
for (const part of
|
|
63
|
+
for (const part of eventSource) {
|
|
60
64
|
yield part;
|
|
61
65
|
}
|
|
62
66
|
}
|
|
@@ -13,3 +13,17 @@ export declare function toolErrorResult(error: unknown): {
|
|
|
13
13
|
error: true;
|
|
14
14
|
message: string;
|
|
15
15
|
};
|
|
16
|
+
export interface ToolDeniedResult {
|
|
17
|
+
__denied: true;
|
|
18
|
+
toolName: string;
|
|
19
|
+
deniedBy?: string;
|
|
20
|
+
message: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* What the model sees when a human declines a `needsApproval` tool it asked to run.
|
|
24
|
+
*
|
|
25
|
+
* A result rather than an error, so the agent can tell the user the request was declined
|
|
26
|
+
* instead of the turn dying. `__denied` (not `error: true`) keeps it distinguishable from a
|
|
27
|
+
* genuine failure — nothing malfunctioned.
|
|
28
|
+
*/
|
|
29
|
+
export declare function toolDeniedResult(toolName: string, deniedBy?: string): ToolDeniedResult;
|
|
@@ -16,3 +16,18 @@ export function toolErrorResult(error) {
|
|
|
16
16
|
const message = error instanceof Error ? error.message : String(error);
|
|
17
17
|
return { error: true, message };
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* What the model sees when a human declines a `needsApproval` tool it asked to run.
|
|
21
|
+
*
|
|
22
|
+
* A result rather than an error, so the agent can tell the user the request was declined
|
|
23
|
+
* instead of the turn dying. `__denied` (not `error: true`) keeps it distinguishable from a
|
|
24
|
+
* genuine failure — nothing malfunctioned.
|
|
25
|
+
*/
|
|
26
|
+
export function toolDeniedResult(toolName, deniedBy) {
|
|
27
|
+
return {
|
|
28
|
+
__denied: true,
|
|
29
|
+
toolName,
|
|
30
|
+
deniedBy,
|
|
31
|
+
message: `The "${toolName}" action was not approved${deniedBy ? ` by ${deniedBy}` : ''}. Tell the user it was declined; do not retry it.`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -9,6 +9,11 @@ export interface CoreToolExecutorConfig {
|
|
|
9
9
|
parallelExecution?: boolean;
|
|
10
10
|
agentId?: string;
|
|
11
11
|
onInterim?: (message: string, toolName: string) => void;
|
|
12
|
+
/**
|
|
13
|
+
* Called for each chunk an async-iterable tool yields, as it arrives. The aggregate is
|
|
14
|
+
* still the tool's result; this is progress, not output.
|
|
15
|
+
*/
|
|
16
|
+
onChunk?: (chunk: unknown, toolName: string, toolCallId: string) => void;
|
|
12
17
|
}
|
|
13
18
|
export interface CoreExecuteArgs {
|
|
14
19
|
name: string;
|
|
@@ -26,6 +31,7 @@ export declare class CoreToolExecutor implements EffectToolExecutor {
|
|
|
26
31
|
private readonly parallelExecution;
|
|
27
32
|
private readonly agentId;
|
|
28
33
|
private readonly onInterim?;
|
|
34
|
+
private readonly onChunk?;
|
|
29
35
|
private readonly pairing;
|
|
30
36
|
private executionGate;
|
|
31
37
|
private callHistory;
|
|
@@ -3,12 +3,44 @@ import { debug } from '../../debug.js';
|
|
|
3
3
|
import { cancelledPlaceholder, inProgressPlaceholder, PairingTracker, } from './pairing.js';
|
|
4
4
|
import { ToolTimeoutError } from './errors.js';
|
|
5
5
|
import { ToolValidationError, validateAndSanitize, validateOutput } from './schema.js';
|
|
6
|
+
import { isControlFlowSignal } from '../../runtime/controlFlowSignal.js';
|
|
7
|
+
/**
|
|
8
|
+
* Merges the caller's abort with a timeout so the tool sees one signal. `AbortSignal.any` is
|
|
9
|
+
* present on Node 20+, Bun, and workerd; the manual path keeps older runtimes working.
|
|
10
|
+
* Mirrors `composeSignals` in `@kuralle-agents/fs`.
|
|
11
|
+
*/
|
|
12
|
+
function composeSignals(signal, timeoutSignal) {
|
|
13
|
+
if (!signal)
|
|
14
|
+
return timeoutSignal;
|
|
15
|
+
if (!timeoutSignal)
|
|
16
|
+
return signal;
|
|
17
|
+
if (typeof AbortSignal.any === 'function') {
|
|
18
|
+
return AbortSignal.any([signal, timeoutSignal]);
|
|
19
|
+
}
|
|
20
|
+
const controller = new AbortController();
|
|
21
|
+
const onAbort = () => controller.abort();
|
|
22
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
23
|
+
timeoutSignal.addEventListener('abort', onAbort, { once: true });
|
|
24
|
+
if (signal.aborted || timeoutSignal.aborted)
|
|
25
|
+
onAbort();
|
|
26
|
+
return controller.signal;
|
|
27
|
+
}
|
|
28
|
+
function rejectOnAbort(signal, makeError) {
|
|
29
|
+
return new Promise((_, reject) => {
|
|
30
|
+
if (signal.aborted) {
|
|
31
|
+
reject(makeError());
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
signal.addEventListener('abort', () => reject(makeError()), { once: true });
|
|
35
|
+
});
|
|
36
|
+
}
|
|
6
37
|
export class CoreToolExecutor {
|
|
7
38
|
tools;
|
|
8
39
|
enforcer;
|
|
9
40
|
parallelExecution;
|
|
10
41
|
agentId;
|
|
11
42
|
onInterim;
|
|
43
|
+
onChunk;
|
|
12
44
|
pairing = new PairingTracker();
|
|
13
45
|
executionGate = Promise.resolve();
|
|
14
46
|
callHistory = [];
|
|
@@ -18,6 +50,7 @@ export class CoreToolExecutor {
|
|
|
18
50
|
this.parallelExecution = config.parallelExecution ?? false;
|
|
19
51
|
this.agentId = config.agentId ?? 'agent';
|
|
20
52
|
this.onInterim = config.onInterim;
|
|
53
|
+
this.onChunk = config.onChunk;
|
|
21
54
|
}
|
|
22
55
|
getPairs() {
|
|
23
56
|
return this.pairing.getAllPairs();
|
|
@@ -26,7 +59,10 @@ export class CoreToolExecutor {
|
|
|
26
59
|
return this.tools.get(name);
|
|
27
60
|
}
|
|
28
61
|
async execute(args) {
|
|
29
|
-
|
|
62
|
+
const registryDef = this.tools.get(args.name);
|
|
63
|
+
const def = args.def ?? registryDef;
|
|
64
|
+
const parallelSafe = def?.parallelSafe === true || def?.replay === false;
|
|
65
|
+
if (!this.parallelExecution && !parallelSafe) {
|
|
30
66
|
return this.withSerialGate(() => this.executeInner(args));
|
|
31
67
|
}
|
|
32
68
|
return this.executeInner(args);
|
|
@@ -94,19 +130,14 @@ export class CoreToolExecutor {
|
|
|
94
130
|
}
|
|
95
131
|
}
|
|
96
132
|
let interimTimer;
|
|
97
|
-
let timeoutTimer;
|
|
98
|
-
let interimSent = false;
|
|
99
133
|
const onAbort = () => {
|
|
100
134
|
if (interimTimer)
|
|
101
135
|
clearTimeout(interimTimer);
|
|
102
|
-
if (timeoutTimer)
|
|
103
|
-
clearTimeout(timeoutTimer);
|
|
104
136
|
};
|
|
105
137
|
abortSignal?.addEventListener('abort', onAbort, { once: true });
|
|
106
138
|
try {
|
|
107
139
|
if (def.interim && def.interimAfterMs != null && def.interimAfterMs >= 0) {
|
|
108
140
|
interimTimer = setTimeout(() => {
|
|
109
|
-
interimSent = true;
|
|
110
141
|
this.onInterim?.(def.interim, name);
|
|
111
142
|
this.pairing.closePair(requestId, 'in_progress', inProgressPlaceholder(requestId, name, def.interim));
|
|
112
143
|
}, def.interimAfterMs);
|
|
@@ -114,40 +145,36 @@ export class CoreToolExecutor {
|
|
|
114
145
|
interimTimer.unref();
|
|
115
146
|
}
|
|
116
147
|
}
|
|
148
|
+
const timeoutMs = def.timeoutMs;
|
|
149
|
+
// The tool receives the timeout as an abort, so a cooperative tool stops working when
|
|
150
|
+
// we stop waiting for it. Racing alone abandoned the promise while the work continued.
|
|
151
|
+
const timeoutSignal = timeoutMs != null && timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : undefined;
|
|
152
|
+
const effectiveSignal = composeSignals(abortSignal, timeoutSignal);
|
|
117
153
|
const executeCtx = toolCtx
|
|
118
|
-
? { ...toolCtx, abortSignal }
|
|
119
|
-
:
|
|
120
|
-
? { abortSignal }
|
|
154
|
+
? { ...toolCtx, abortSignal: effectiveSignal }
|
|
155
|
+
: effectiveSignal
|
|
156
|
+
? { abortSignal: effectiveSignal }
|
|
121
157
|
: undefined;
|
|
122
158
|
const executePromise = Promise.resolve(def.execute(sanitizedArgs, executeCtx)).then(async (result) => {
|
|
123
159
|
if (result && typeof result[Symbol.asyncIterator] === 'function') {
|
|
124
160
|
const chunks = [];
|
|
125
161
|
for await (const chunk of result) {
|
|
126
162
|
chunks.push(chunk);
|
|
163
|
+
// Surface progress as it arrives. Observational only — the journal records the
|
|
164
|
+
// aggregate below, so a replayed step emits nothing and stays deterministic.
|
|
165
|
+
this.onChunk?.(chunk, name, requestId);
|
|
127
166
|
}
|
|
128
167
|
return chunks.length === 1 ? chunks[0] : chunks;
|
|
129
168
|
}
|
|
130
169
|
return result;
|
|
131
170
|
});
|
|
171
|
+
// The signals above cancel a cooperative tool; these racers stop us waiting on one that
|
|
172
|
+
// ignores them. Both are needed — neither alone bounds the call.
|
|
132
173
|
const abortPromise = abortSignal && def.interruptible !== false
|
|
133
|
-
?
|
|
134
|
-
if (abortSignal.aborted) {
|
|
135
|
-
reject(new DOMException('Aborted', 'AbortError'));
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
abortSignal.addEventListener('abort', () => reject(new DOMException('Aborted', 'AbortError')), { once: true });
|
|
139
|
-
})
|
|
174
|
+
? rejectOnAbort(abortSignal, () => new DOMException('Aborted', 'AbortError'))
|
|
140
175
|
: null;
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
? new Promise((_, reject) => {
|
|
144
|
-
timeoutTimer = setTimeout(() => {
|
|
145
|
-
reject(new ToolTimeoutError(name, timeoutMs));
|
|
146
|
-
}, timeoutMs);
|
|
147
|
-
if (typeof timeoutTimer === 'object' && 'unref' in timeoutTimer) {
|
|
148
|
-
timeoutTimer.unref();
|
|
149
|
-
}
|
|
150
|
-
})
|
|
176
|
+
const timeoutPromise = timeoutSignal
|
|
177
|
+
? rejectOnAbort(timeoutSignal, () => new ToolTimeoutError(name, timeoutMs))
|
|
151
178
|
: null;
|
|
152
179
|
const racers = [executePromise];
|
|
153
180
|
if (abortPromise)
|
|
@@ -157,25 +184,16 @@ export class CoreToolExecutor {
|
|
|
157
184
|
const rawResult = racers.length > 1 ? await Promise.race(racers) : await executePromise;
|
|
158
185
|
if (interimTimer)
|
|
159
186
|
clearTimeout(interimTimer);
|
|
160
|
-
if (timeoutTimer)
|
|
161
|
-
clearTimeout(timeoutTimer);
|
|
162
187
|
const validated = await validateOutput(def.output, rawResult, name);
|
|
163
188
|
callRecord.result = validated;
|
|
164
189
|
callRecord.durationMs = Date.now() - callRecord.timestamp;
|
|
165
190
|
this.callHistory.push(callRecord);
|
|
166
|
-
|
|
167
|
-
this.pairing.closePair(requestId, 'completed', validated);
|
|
168
|
-
}
|
|
169
|
-
else {
|
|
170
|
-
this.pairing.closePair(requestId, 'completed', validated);
|
|
171
|
-
}
|
|
191
|
+
this.pairing.closePair(requestId, 'completed', validated);
|
|
172
192
|
return validated;
|
|
173
193
|
}
|
|
174
194
|
catch (error) {
|
|
175
195
|
if (interimTimer)
|
|
176
196
|
clearTimeout(interimTimer);
|
|
177
|
-
if (timeoutTimer)
|
|
178
|
-
clearTimeout(timeoutTimer);
|
|
179
197
|
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
180
198
|
const placeholder = cancelledPlaceholder(requestId, name);
|
|
181
199
|
callRecord.success = false;
|
|
@@ -189,6 +207,27 @@ export class CoreToolExecutor {
|
|
|
189
207
|
this.pairing.closePair(requestId, 'validation_failed', undefined, error.message);
|
|
190
208
|
throw error;
|
|
191
209
|
}
|
|
210
|
+
if (error instanceof ToolTimeoutError || isControlFlowSignal(error)) {
|
|
211
|
+
// A timeout and a suspend are decisions about the run, not results the tool may
|
|
212
|
+
// reinterpret. They reach the caller unchanged.
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
if (def.onError) {
|
|
216
|
+
// Recovery runs before the failure is recorded, so a handled error is journaled as
|
|
217
|
+
// the success it became. A throwing handler falls through to the generic path below.
|
|
218
|
+
try {
|
|
219
|
+
const recovered = await def.onError(error instanceof Error ? error : new Error(String(error)), sanitizedArgs);
|
|
220
|
+
const validatedRecovery = await validateOutput(def.output, recovered, name);
|
|
221
|
+
callRecord.result = validatedRecovery;
|
|
222
|
+
callRecord.durationMs = Date.now() - callRecord.timestamp;
|
|
223
|
+
this.callHistory.push(callRecord);
|
|
224
|
+
this.pairing.closePair(requestId, 'completed', validatedRecovery);
|
|
225
|
+
return validatedRecovery;
|
|
226
|
+
}
|
|
227
|
+
catch (recoveryError) {
|
|
228
|
+
error = recoveryError;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
192
231
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
193
232
|
callRecord.success = false;
|
|
194
233
|
callRecord.error = err;
|
|
@@ -19,7 +19,18 @@ export declare function defineTool<S extends z.ZodTypeAny | StandardSchemaV1 | u
|
|
|
19
19
|
estimatedDurationMs?: number;
|
|
20
20
|
timeoutMs?: number;
|
|
21
21
|
replay?: boolean;
|
|
22
|
+
parallelSafe?: boolean;
|
|
23
|
+
idempotencyKey?: (args: InferToolInput<S>) => string;
|
|
22
24
|
execute: (args: InferToolInput<S>, ctx?: ToolContext) => Promise<R> | AsyncIterable<R>;
|
|
25
|
+
/**
|
|
26
|
+
* Turns a thrown error into a result the model can act on, instead of a generic failure.
|
|
27
|
+
* Return a value to recover; rethrow (or omit this) to let the error propagate.
|
|
28
|
+
*
|
|
29
|
+
* Runs only for genuine failures — never for a timeout, an abort, an input/output schema
|
|
30
|
+
* violation, or a control-flow signal, all of which must stay distinguishable from a
|
|
31
|
+
* result the tool chose to return.
|
|
32
|
+
*/
|
|
33
|
+
onError?: (error: Error, args: InferToolInput<S>) => Promise<R> | R;
|
|
23
34
|
}): Tool<InferToolInput<S>, R>;
|
|
24
35
|
export declare function toolToAiSdk<TInput = unknown, TOutput = unknown>(def: Tool<TInput, TOutput>): AiTool<TInput, TOutput>;
|
|
25
36
|
export declare function buildToolSet(tools: Record<string, AnyTool>): ToolSet;
|
|
@@ -11,7 +11,10 @@ export function defineTool(config) {
|
|
|
11
11
|
interimAfterMs: config.interimAfterMs ?? config.estimatedDurationMs,
|
|
12
12
|
timeoutMs: config.timeoutMs,
|
|
13
13
|
replay: config.replay,
|
|
14
|
+
parallelSafe: config.parallelSafe,
|
|
15
|
+
idempotencyKey: config.idempotencyKey,
|
|
14
16
|
execute: config.execute,
|
|
17
|
+
onError: config.onError,
|
|
15
18
|
};
|
|
16
19
|
}
|
|
17
20
|
export function toolToAiSdk(def) {
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Thrown by `ctx.tool(...)` when a tool declared `needsApproval: true` is denied
|
|
3
|
-
* by a human (the `__approval` signal resolves with `approved: false`). Catch it
|
|
4
|
-
* inside the calling flow `action` node to route gracefully (e.g. escalate or end).
|
|
5
|
-
*/
|
|
1
|
+
/** Thrown when a tool execution exceeds its configured timeout. */
|
|
6
2
|
export declare class ToolTimeoutError extends Error {
|
|
7
3
|
readonly toolName: string;
|
|
8
4
|
readonly timeoutMs: number;
|
|
9
5
|
constructor(toolName: string, timeoutMs: number);
|
|
10
6
|
}
|
|
7
|
+
/** Thrown when a human denies a tool call that requires approval. */
|
|
11
8
|
export declare class ToolApprovalDeniedError extends Error {
|
|
12
9
|
readonly toolName: string;
|
|
13
10
|
readonly by?: string;
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Thrown by `ctx.tool(...)` when a tool declared `needsApproval: true` is denied
|
|
3
|
-
* by a human (the `__approval` signal resolves with `approved: false`). Catch it
|
|
4
|
-
* inside the calling flow `action` node to route gracefully (e.g. escalate or end).
|
|
5
|
-
*/
|
|
1
|
+
/** Thrown when a tool execution exceeds its configured timeout. */
|
|
6
2
|
export class ToolTimeoutError extends Error {
|
|
7
3
|
toolName;
|
|
8
4
|
timeoutMs;
|
|
@@ -13,6 +9,7 @@ export class ToolTimeoutError extends Error {
|
|
|
13
9
|
this.timeoutMs = timeoutMs;
|
|
14
10
|
}
|
|
15
11
|
}
|
|
12
|
+
/** Thrown when a human denies a tool call that requires approval. */
|
|
16
13
|
export class ToolApprovalDeniedError extends Error {
|
|
17
14
|
toolName;
|
|
18
15
|
by;
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -6,5 +6,4 @@ export { isFinalResult } from './final.js';
|
|
|
6
6
|
export type { FinalResult } from './final.js';
|
|
7
7
|
export { createHttpTool } from './http.js';
|
|
8
8
|
export type { HttpToolConfig, HttpToolResult, HttpParam, ParamType, HttpMethod, AuthConfig, } from './http.types.js';
|
|
9
|
-
export { withErrorHandling, executeWithRetry, createCircuitBreaker, withTimeout, isPermanentError, isCircuitOpenError, CircuitOpenError, ToolTimeoutError, type ToolErrorConfig, type ToolResult, } from './errorHandling.js';
|
|
10
9
|
export { DEFAULT_ERROR_MESSAGES, getUserFriendlyError, categorizeError, createErrorResponse, type ErrorCategory, } from './errorMessages.js';
|
package/dist/tools/index.js
CHANGED
|
@@ -2,5 +2,4 @@ export { createTool, createToolWithFiller } from './Tool.js';
|
|
|
2
2
|
export { createHandoffTool, isHandoffResult } from './handoff.js';
|
|
3
3
|
export { isFinalResult } from './final.js';
|
|
4
4
|
export { createHttpTool } from './http.js';
|
|
5
|
-
export { withErrorHandling, executeWithRetry, createCircuitBreaker, withTimeout, isPermanentError, isCircuitOpenError, CircuitOpenError, ToolTimeoutError, } from './errorHandling.js';
|
|
6
5
|
export { DEFAULT_ERROR_MESSAGES, getUserFriendlyError, categorizeError, createErrorResponse, } from './errorMessages.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AgentSpan, AgentTrace } from '../types/trace.js';
|
|
2
|
+
import { type TraceListWindow, type TraceStore } from './TraceStore.js';
|
|
3
|
+
export interface MemoryTraceStoreOptions {
|
|
4
|
+
retentionMs?: number;
|
|
5
|
+
}
|
|
6
|
+
export declare class MemoryTraceStore implements TraceStore {
|
|
7
|
+
private readonly options;
|
|
8
|
+
private readonly spans;
|
|
9
|
+
constructor(options?: MemoryTraceStoreOptions);
|
|
10
|
+
write(span: AgentSpan): void;
|
|
11
|
+
putSpan(span: AgentSpan): void;
|
|
12
|
+
getTrace(traceId: string): Promise<AgentTrace | null>;
|
|
13
|
+
listTraces(sessionId: string, window?: TraceListWindow): Promise<AgentTrace[]>;
|
|
14
|
+
cleanup(maxAgeMs: number): Promise<number>;
|
|
15
|
+
private cleanupSync;
|
|
16
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { cloneSpan, traceFromSpans, } from './TraceStore.js';
|
|
2
|
+
export class MemoryTraceStore {
|
|
3
|
+
options;
|
|
4
|
+
spans = new Map();
|
|
5
|
+
constructor(options = {}) {
|
|
6
|
+
this.options = options;
|
|
7
|
+
}
|
|
8
|
+
write(span) {
|
|
9
|
+
this.putSpan(span);
|
|
10
|
+
}
|
|
11
|
+
putSpan(span) {
|
|
12
|
+
let trace = this.spans.get(span.traceId);
|
|
13
|
+
if (!trace) {
|
|
14
|
+
trace = new Map();
|
|
15
|
+
this.spans.set(span.traceId, trace);
|
|
16
|
+
}
|
|
17
|
+
trace.set(span.spanId, cloneSpan(span));
|
|
18
|
+
if (this.options.retentionMs !== undefined)
|
|
19
|
+
this.cleanupSync(this.options.retentionMs);
|
|
20
|
+
}
|
|
21
|
+
async getTrace(traceId) {
|
|
22
|
+
const spans = this.spans.get(traceId);
|
|
23
|
+
return traceFromSpans(spans ? [...spans.values()] : []);
|
|
24
|
+
}
|
|
25
|
+
async listTraces(sessionId, window) {
|
|
26
|
+
const traces = [...this.spans.values()]
|
|
27
|
+
.map((spans) => traceFromSpans([...spans.values()]))
|
|
28
|
+
.filter((trace) => trace !== null)
|
|
29
|
+
.filter((trace) => trace.sessionId === sessionId)
|
|
30
|
+
.filter((trace) => inWindow(trace.startedAt, window))
|
|
31
|
+
.sort((a, b) => b.startedAt - a.startedAt);
|
|
32
|
+
return window?.limit === undefined ? traces : traces.slice(0, window.limit);
|
|
33
|
+
}
|
|
34
|
+
async cleanup(maxAgeMs) {
|
|
35
|
+
return this.cleanupSync(maxAgeMs);
|
|
36
|
+
}
|
|
37
|
+
cleanupSync(maxAgeMs) {
|
|
38
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
39
|
+
let removed = 0;
|
|
40
|
+
for (const [traceId, spans] of this.spans) {
|
|
41
|
+
const root = [...spans.values()].find((span) => span.kind === 'turn');
|
|
42
|
+
if ((root?.endTime ?? root?.startTime ?? 0) < cutoff) {
|
|
43
|
+
this.spans.delete(traceId);
|
|
44
|
+
removed += 1;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return removed;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function inWindow(timestamp, window) {
|
|
51
|
+
if (window?.from && timestamp < window.from.getTime())
|
|
52
|
+
return false;
|
|
53
|
+
if (window?.to && timestamp > window.to.getTime())
|
|
54
|
+
return false;
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { AgentSpan } from '../types/trace.js';
|
|
2
|
+
import type { TraceSink } from './TraceStore.js';
|
|
3
|
+
export interface OtelTraceSinkOptions {
|
|
4
|
+
endpoint: string;
|
|
5
|
+
headers?: Record<string, string>;
|
|
6
|
+
serviceName?: string;
|
|
7
|
+
batchSize?: number;
|
|
8
|
+
fetch?: typeof fetch;
|
|
9
|
+
}
|
|
10
|
+
export declare class OtelTraceSink implements TraceSink {
|
|
11
|
+
private readonly options;
|
|
12
|
+
private readonly pending;
|
|
13
|
+
private exporting?;
|
|
14
|
+
constructor(options: OtelTraceSinkOptions);
|
|
15
|
+
write(span: AgentSpan): Promise<void> | void;
|
|
16
|
+
flush(): Promise<void>;
|
|
17
|
+
private export;
|
|
18
|
+
}
|
|
19
|
+
export declare function otelSink(options: OtelTraceSinkOptions): OtelTraceSink;
|
|
20
|
+
export interface LangfuseSinkOptions {
|
|
21
|
+
endpoint?: string;
|
|
22
|
+
publicKey: string;
|
|
23
|
+
secretKey: string;
|
|
24
|
+
serviceName?: string;
|
|
25
|
+
batchSize?: number;
|
|
26
|
+
fetch?: typeof fetch;
|
|
27
|
+
}
|
|
28
|
+
export declare function langfuseSink(options: LangfuseSinkOptions): OtelTraceSink;
|
|
29
|
+
export declare function toOtlpPayload(spans: AgentSpan[], serviceName?: string): {
|
|
30
|
+
resourceSpans: {
|
|
31
|
+
resource: {
|
|
32
|
+
attributes: {
|
|
33
|
+
key: string;
|
|
34
|
+
value: {
|
|
35
|
+
stringValue: string;
|
|
36
|
+
};
|
|
37
|
+
}[];
|
|
38
|
+
};
|
|
39
|
+
scopeSpans: {
|
|
40
|
+
scope: {
|
|
41
|
+
name: string;
|
|
42
|
+
};
|
|
43
|
+
spans: {
|
|
44
|
+
name: string;
|
|
45
|
+
kind: number;
|
|
46
|
+
startTimeUnixNano: string;
|
|
47
|
+
endTimeUnixNano: string;
|
|
48
|
+
attributes: ({
|
|
49
|
+
key: string;
|
|
50
|
+
value: {
|
|
51
|
+
boolValue: boolean;
|
|
52
|
+
intValue?: undefined;
|
|
53
|
+
doubleValue?: undefined;
|
|
54
|
+
stringValue?: undefined;
|
|
55
|
+
};
|
|
56
|
+
} | {
|
|
57
|
+
key: string;
|
|
58
|
+
value: {
|
|
59
|
+
intValue: string;
|
|
60
|
+
boolValue?: undefined;
|
|
61
|
+
doubleValue?: undefined;
|
|
62
|
+
stringValue?: undefined;
|
|
63
|
+
};
|
|
64
|
+
} | {
|
|
65
|
+
key: string;
|
|
66
|
+
value: {
|
|
67
|
+
doubleValue: number;
|
|
68
|
+
boolValue?: undefined;
|
|
69
|
+
intValue?: undefined;
|
|
70
|
+
stringValue?: undefined;
|
|
71
|
+
};
|
|
72
|
+
} | {
|
|
73
|
+
key: string;
|
|
74
|
+
value: {
|
|
75
|
+
stringValue: string;
|
|
76
|
+
boolValue?: undefined;
|
|
77
|
+
intValue?: undefined;
|
|
78
|
+
doubleValue?: undefined;
|
|
79
|
+
};
|
|
80
|
+
})[];
|
|
81
|
+
events: {
|
|
82
|
+
name: string;
|
|
83
|
+
timeUnixNano: string;
|
|
84
|
+
attributes: ({
|
|
85
|
+
key: string;
|
|
86
|
+
value: {
|
|
87
|
+
boolValue: boolean;
|
|
88
|
+
intValue?: undefined;
|
|
89
|
+
doubleValue?: undefined;
|
|
90
|
+
stringValue?: undefined;
|
|
91
|
+
};
|
|
92
|
+
} | {
|
|
93
|
+
key: string;
|
|
94
|
+
value: {
|
|
95
|
+
intValue: string;
|
|
96
|
+
boolValue?: undefined;
|
|
97
|
+
doubleValue?: undefined;
|
|
98
|
+
stringValue?: undefined;
|
|
99
|
+
};
|
|
100
|
+
} | {
|
|
101
|
+
key: string;
|
|
102
|
+
value: {
|
|
103
|
+
doubleValue: number;
|
|
104
|
+
boolValue?: undefined;
|
|
105
|
+
intValue?: undefined;
|
|
106
|
+
stringValue?: undefined;
|
|
107
|
+
};
|
|
108
|
+
} | {
|
|
109
|
+
key: string;
|
|
110
|
+
value: {
|
|
111
|
+
stringValue: string;
|
|
112
|
+
boolValue?: undefined;
|
|
113
|
+
intValue?: undefined;
|
|
114
|
+
doubleValue?: undefined;
|
|
115
|
+
};
|
|
116
|
+
})[];
|
|
117
|
+
}[];
|
|
118
|
+
status: {
|
|
119
|
+
code: number;
|
|
120
|
+
};
|
|
121
|
+
parentSpanId?: string | undefined;
|
|
122
|
+
traceId: string;
|
|
123
|
+
spanId: string;
|
|
124
|
+
}[];
|
|
125
|
+
}[];
|
|
126
|
+
}[];
|
|
127
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export class OtelTraceSink {
|
|
2
|
+
options;
|
|
3
|
+
pending = [];
|
|
4
|
+
exporting;
|
|
5
|
+
constructor(options) {
|
|
6
|
+
this.options = options;
|
|
7
|
+
}
|
|
8
|
+
write(span) {
|
|
9
|
+
this.pending.push(structuredClone(span));
|
|
10
|
+
if (this.pending.length >= (this.options.batchSize ?? 32))
|
|
11
|
+
return this.flush();
|
|
12
|
+
}
|
|
13
|
+
async flush() {
|
|
14
|
+
if (this.exporting)
|
|
15
|
+
await this.exporting;
|
|
16
|
+
if (this.pending.length === 0)
|
|
17
|
+
return;
|
|
18
|
+
const spans = this.pending.splice(0);
|
|
19
|
+
this.exporting = this.export(spans).finally(() => { this.exporting = undefined; });
|
|
20
|
+
await this.exporting;
|
|
21
|
+
}
|
|
22
|
+
async export(spans) {
|
|
23
|
+
const transport = this.options.fetch ?? globalThis.fetch;
|
|
24
|
+
const response = await transport(otlpUrl(this.options.endpoint), {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: { 'content-type': 'application/json', ...this.options.headers },
|
|
27
|
+
body: JSON.stringify(toOtlpPayload(spans, this.options.serviceName)),
|
|
28
|
+
});
|
|
29
|
+
if (!response.ok)
|
|
30
|
+
throw new Error(`OTLP trace export failed: ${response.status}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function otelSink(options) {
|
|
34
|
+
return new OtelTraceSink(options);
|
|
35
|
+
}
|
|
36
|
+
export function langfuseSink(options) {
|
|
37
|
+
return new OtelTraceSink({
|
|
38
|
+
endpoint: options.endpoint ?? 'https://cloud.langfuse.com/api/public/otel',
|
|
39
|
+
headers: {
|
|
40
|
+
Authorization: `Basic ${btoa(`${options.publicKey}:${options.secretKey}`)}`,
|
|
41
|
+
'x-langfuse-ingestion-version': '4',
|
|
42
|
+
},
|
|
43
|
+
serviceName: options.serviceName,
|
|
44
|
+
batchSize: options.batchSize,
|
|
45
|
+
fetch: options.fetch,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
export function toOtlpPayload(spans, serviceName = 'kuralle-agent') {
|
|
49
|
+
return {
|
|
50
|
+
resourceSpans: [{
|
|
51
|
+
resource: { attributes: [{ key: 'service.name', value: { stringValue: serviceName } }] },
|
|
52
|
+
scopeSpans: [{
|
|
53
|
+
scope: { name: '@kuralle-agents/core' },
|
|
54
|
+
spans: spans.map(toOtlpSpan),
|
|
55
|
+
}],
|
|
56
|
+
}],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function toOtlpSpan(span) {
|
|
60
|
+
return {
|
|
61
|
+
traceId: normalizeHex(span.traceId, 32),
|
|
62
|
+
spanId: normalizeHex(span.spanId, 16),
|
|
63
|
+
...(span.parentSpanId ? { parentSpanId: normalizeHex(span.parentSpanId, 16) } : {}),
|
|
64
|
+
name: span.name,
|
|
65
|
+
kind: 1,
|
|
66
|
+
startTimeUnixNano: millisecondsToNanos(span.startTime),
|
|
67
|
+
endTimeUnixNano: millisecondsToNanos(span.endTime ?? span.startTime),
|
|
68
|
+
attributes: [
|
|
69
|
+
otlpAttribute('kuralle.kind', span.kind),
|
|
70
|
+
...Object.entries(span.attributes)
|
|
71
|
+
.filter(([, value]) => value !== undefined)
|
|
72
|
+
.map(([key, value]) => otlpAttribute(`kuralle.${key}`, value)),
|
|
73
|
+
],
|
|
74
|
+
events: (span.events ?? []).map((event) => ({
|
|
75
|
+
name: event.name,
|
|
76
|
+
timeUnixNano: millisecondsToNanos(event.time),
|
|
77
|
+
attributes: Object.entries(event.attributes ?? {}).map(([key, value]) => otlpAttribute(`kuralle.${key}`, value)),
|
|
78
|
+
})),
|
|
79
|
+
status: { code: span.status === 'error' ? 2 : 1 },
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function otlpAttribute(key, value) {
|
|
83
|
+
if (typeof value === 'boolean')
|
|
84
|
+
return { key, value: { boolValue: value } };
|
|
85
|
+
if (typeof value === 'number' && Number.isInteger(value))
|
|
86
|
+
return { key, value: { intValue: String(value) } };
|
|
87
|
+
if (typeof value === 'number')
|
|
88
|
+
return { key, value: { doubleValue: value } };
|
|
89
|
+
return { key, value: { stringValue: typeof value === 'string' ? value : JSON.stringify(value) } };
|
|
90
|
+
}
|
|
91
|
+
function normalizeHex(value, length) {
|
|
92
|
+
const hex = value.toLowerCase().replace(/[^0-9a-f]/g, '');
|
|
93
|
+
return hex.padStart(length, '0').slice(-length);
|
|
94
|
+
}
|
|
95
|
+
function millisecondsToNanos(value) {
|
|
96
|
+
return (BigInt(Math.trunc(value)) * 1000000n).toString();
|
|
97
|
+
}
|
|
98
|
+
function otlpUrl(endpoint) {
|
|
99
|
+
const trimmed = endpoint.replace(/\/$/, '');
|
|
100
|
+
return trimmed.endsWith('/v1/traces') ? trimmed : `${trimmed}/v1/traces`;
|
|
101
|
+
}
|