@kuralle-agents/core 0.12.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 +6 -1
- package/dist/index.js +4 -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/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 +110 -57
- 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/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 +4 -1
- package/dist/tools/effect/defineTool.d.ts +2 -0
- package/dist/tools/effect/defineTool.js +2 -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 +2 -1
- package/dist/types/channel.d.ts +14 -1
- package/dist/types/effectTool.d.ts +4 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/run-context.d.ts +14 -1
- package/dist/types/session.d.ts +10 -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 +13 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Session } from '../../types/index.js';
|
|
2
2
|
import type { AuditListOptions, ConversationAuditEntry } from '../../audit/types.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type SessionListWindow, type SessionStore } from '../SessionStore.js';
|
|
4
4
|
export declare class MemoryStore implements SessionStore {
|
|
5
5
|
private sessions;
|
|
6
6
|
get(id: string): Promise<Session | null>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { StaleWriteError } from '../SessionStore.js';
|
|
1
2
|
export class MemoryStore {
|
|
2
3
|
sessions = new Map();
|
|
3
4
|
async get(id) {
|
|
@@ -5,8 +6,21 @@ export class MemoryStore {
|
|
|
5
6
|
return session ? safeClone(session) : null;
|
|
6
7
|
}
|
|
7
8
|
async save(session) {
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
const existing = this.sessions.get(session.id);
|
|
10
|
+
const expected = session.version ?? 0;
|
|
11
|
+
if (existing !== undefined) {
|
|
12
|
+
const stored = existing.version ?? 0;
|
|
13
|
+
if (stored !== expected) {
|
|
14
|
+
throw new StaleWriteError(session.id, expected, stored);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
else if (expected !== 0) {
|
|
18
|
+
throw new StaleWriteError(session.id, expected, 0);
|
|
19
|
+
}
|
|
20
|
+
const toSave = safeClone(session);
|
|
21
|
+
toSave.updatedAt = new Date();
|
|
22
|
+
toSave.version = expected + 1;
|
|
23
|
+
this.sessions.set(session.id, toSave);
|
|
10
24
|
}
|
|
11
25
|
async delete(id) {
|
|
12
26
|
this.sessions.delete(id);
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type SessionStore } from './SessionStore.js';
|
|
2
2
|
export type SessionStoreFactory = () => SessionStore | Promise<SessionStore>;
|
|
3
3
|
/**
|
|
4
4
|
* Registers the shared SessionStore contract tests. Must be invoked at the
|
|
5
5
|
* top level of a bun test file.
|
|
6
6
|
*/
|
|
7
7
|
export declare function runSessionStoreContract(factory: SessionStoreFactory): void;
|
|
8
|
+
/**
|
|
9
|
+
* Registers optimistic-concurrency (CAS) contract tests for SessionStore adapters.
|
|
10
|
+
* Two concurrent saves with the same expected version: exactly one succeeds.
|
|
11
|
+
*/
|
|
12
|
+
export declare function runSessionStoreCasContract(factory: SessionStoreFactory): void;
|
package/dist/session/testing.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* `bun:test` into runtime bundles.
|
|
22
22
|
*/
|
|
23
23
|
import { describe, test, expect, beforeEach } from 'bun:test';
|
|
24
|
+
import { StaleWriteError } from './SessionStore.js';
|
|
24
25
|
const buildSession = (overrides = {}) => {
|
|
25
26
|
const now = new Date();
|
|
26
27
|
return {
|
|
@@ -120,3 +121,36 @@ export function runSessionStoreContract(factory) {
|
|
|
120
121
|
});
|
|
121
122
|
});
|
|
122
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Registers optimistic-concurrency (CAS) contract tests for SessionStore adapters.
|
|
126
|
+
* Two concurrent saves with the same expected version: exactly one succeeds.
|
|
127
|
+
*/
|
|
128
|
+
export function runSessionStoreCasContract(factory) {
|
|
129
|
+
describe('SessionStore CAS contract', () => {
|
|
130
|
+
let store;
|
|
131
|
+
beforeEach(async () => {
|
|
132
|
+
store = await factory();
|
|
133
|
+
});
|
|
134
|
+
test('concurrent save with same expected version rejects one writer with StaleWriteError', async () => {
|
|
135
|
+
const session = buildSession({ id: 'cas-sess' });
|
|
136
|
+
await store.save(session);
|
|
137
|
+
const firstRead = (await store.get('cas-sess'));
|
|
138
|
+
const secondRead = (await store.get('cas-sess'));
|
|
139
|
+
expect(firstRead.version).toBe(1);
|
|
140
|
+
expect(secondRead.version).toBe(1);
|
|
141
|
+
firstRead.workingMemory = { writer: 'a' };
|
|
142
|
+
secondRead.workingMemory = { writer: 'b' };
|
|
143
|
+
const results = await Promise.allSettled([
|
|
144
|
+
store.save(firstRead),
|
|
145
|
+
store.save(secondRead),
|
|
146
|
+
]);
|
|
147
|
+
const fulfilled = results.filter((r) => r.status === 'fulfilled');
|
|
148
|
+
const rejected = results.filter((r) => r.status === 'rejected');
|
|
149
|
+
expect(fulfilled).toHaveLength(1);
|
|
150
|
+
expect(rejected).toHaveLength(1);
|
|
151
|
+
expect(rejected[0].reason).toBeInstanceOf(StaleWriteError);
|
|
152
|
+
const final = await store.get('cas-sess');
|
|
153
|
+
expect(final?.version).toBe(2);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
}
|
package/dist/session/utils.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Session } from '../types/index.js';
|
|
2
|
+
import { type SessionStore } from './SessionStore.js';
|
|
2
3
|
/**
|
|
3
4
|
* Restores a Session from its serialized form.
|
|
4
5
|
*
|
|
@@ -17,3 +18,5 @@ import type { Session } from '../types/index.js';
|
|
|
17
18
|
* untouched in the key fields — callers are expected to have validated shape.
|
|
18
19
|
*/
|
|
19
20
|
export declare function reviveSession(raw: unknown): Session;
|
|
21
|
+
/** Apply a session mutation to the latest snapshot and retry it after CAS conflicts. */
|
|
22
|
+
export declare function mutateSessionWithRetry(store: SessionStore, sessionId: string, mutator: (session: Session) => void | Promise<void>): Promise<Session>;
|
package/dist/session/utils.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { StaleWriteError } from './SessionStore.js';
|
|
2
|
+
const CAS_RETRIES = 8;
|
|
1
3
|
/**
|
|
2
4
|
* Restores a Session from its serialized form.
|
|
3
5
|
*
|
|
@@ -45,3 +47,24 @@ export function reviveSession(raw) {
|
|
|
45
47
|
]));
|
|
46
48
|
return session;
|
|
47
49
|
}
|
|
50
|
+
/** Apply a session mutation to the latest snapshot and retry it after CAS conflicts. */
|
|
51
|
+
export async function mutateSessionWithRetry(store, sessionId, mutator) {
|
|
52
|
+
for (let attempt = 0; attempt < CAS_RETRIES; attempt++) {
|
|
53
|
+
const session = await store.get(sessionId);
|
|
54
|
+
if (!session) {
|
|
55
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
56
|
+
}
|
|
57
|
+
await mutator(session);
|
|
58
|
+
try {
|
|
59
|
+
await store.save(session);
|
|
60
|
+
return (await store.get(sessionId)) ?? session;
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (error instanceof StaleWriteError && attempt < CAS_RETRIES - 1) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
throw new Error(`CAS retry limit exceeded for session: ${sessionId}`);
|
|
70
|
+
}
|
|
@@ -26,7 +26,10 @@ export class CoreToolExecutor {
|
|
|
26
26
|
return this.tools.get(name);
|
|
27
27
|
}
|
|
28
28
|
async execute(args) {
|
|
29
|
-
|
|
29
|
+
const registryDef = this.tools.get(args.name);
|
|
30
|
+
const def = args.def ?? registryDef;
|
|
31
|
+
const parallelSafe = def?.parallelSafe === true || def?.replay === false;
|
|
32
|
+
if (!this.parallelExecution && !parallelSafe) {
|
|
30
33
|
return this.withSerialGate(() => this.executeInner(args));
|
|
31
34
|
}
|
|
32
35
|
return this.executeInner(args);
|
|
@@ -19,6 +19,8 @@ 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>;
|
|
23
25
|
}): Tool<InferToolInput<S>, R>;
|
|
24
26
|
export declare function toolToAiSdk<TInput = unknown, TOutput = unknown>(def: Tool<TInput, TOutput>): AiTool<TInput, TOutput>;
|
|
@@ -11,6 +11,8 @@ 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,
|
|
15
17
|
};
|
|
16
18
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AgentSpan, AgentTrace } from '../types/trace.js';
|
|
2
|
+
export interface TraceListWindow {
|
|
3
|
+
from?: Date;
|
|
4
|
+
to?: Date;
|
|
5
|
+
limit?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface TraceSink {
|
|
8
|
+
write(span: AgentSpan): void | Promise<void>;
|
|
9
|
+
flush?(): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export interface TraceStore extends TraceSink {
|
|
12
|
+
putSpan(span: AgentSpan): void | Promise<void>;
|
|
13
|
+
getTrace(traceId: string): Promise<AgentTrace | null>;
|
|
14
|
+
listTraces(sessionId: string, window?: TraceListWindow): Promise<AgentTrace[]>;
|
|
15
|
+
cleanup?(maxAgeMs: number): Promise<number>;
|
|
16
|
+
}
|
|
17
|
+
export declare function isTraceStore(sink: TraceSink): sink is TraceStore;
|
|
18
|
+
export declare function traceFromSpans(spans: AgentSpan[]): AgentTrace | null;
|
|
19
|
+
export declare function cloneSpan(span: AgentSpan): AgentSpan;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function isTraceStore(sink) {
|
|
2
|
+
const value = sink;
|
|
3
|
+
return typeof value.putSpan === 'function' &&
|
|
4
|
+
typeof value.getTrace === 'function' &&
|
|
5
|
+
typeof value.listTraces === 'function';
|
|
6
|
+
}
|
|
7
|
+
export function traceFromSpans(spans) {
|
|
8
|
+
if (spans.length === 0)
|
|
9
|
+
return null;
|
|
10
|
+
const ordered = spans.map(cloneSpan).sort((a, b) => a.startTime - b.startTime);
|
|
11
|
+
const root = ordered.find((span) => span.kind === 'turn') ?? ordered[0];
|
|
12
|
+
const tools = ordered.filter((span) => span.kind === 'tool');
|
|
13
|
+
return {
|
|
14
|
+
traceId: root.traceId,
|
|
15
|
+
sessionId: root.attributes.sessionId,
|
|
16
|
+
spans: ordered,
|
|
17
|
+
answer: typeof root.attributes.output === 'string' ? root.attributes.output : '',
|
|
18
|
+
usedTool: tools.length > 0,
|
|
19
|
+
toolCalls: tools.map((span) => ({
|
|
20
|
+
name: span.attributes.toolName ?? span.name,
|
|
21
|
+
args: span.attributes.input ?? null,
|
|
22
|
+
})),
|
|
23
|
+
toolResults: tools
|
|
24
|
+
.filter((span) => span.attributes.output !== undefined)
|
|
25
|
+
.map((span) => ({ name: span.attributes.toolName ?? span.name, result: span.attributes.output })),
|
|
26
|
+
startedAt: root.startTime,
|
|
27
|
+
...(root.endTime !== undefined ? { endedAt: root.endTime } : {}),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function cloneSpan(span) {
|
|
31
|
+
return structuredClone(span);
|
|
32
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { MemoryTraceStore, type MemoryTraceStoreOptions } from './MemoryTraceStore.js';
|
|
2
|
+
export { isTraceStore, traceFromSpans, type TraceListWindow, type TraceSink, type TraceStore, } from './TraceStore.js';
|
|
3
|
+
export { OtelTraceSink, langfuseSink, otelSink, toOtlpPayload, type LangfuseSinkOptions, type OtelTraceSinkOptions, } from './OtelTraceSink.js';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/// <reference types="bun-types" />
|
|
2
|
+
import { beforeEach, describe, expect, test } from 'bun:test';
|
|
3
|
+
export function runTraceStoreContract(factory) {
|
|
4
|
+
describe('TraceStore contract', () => {
|
|
5
|
+
let store;
|
|
6
|
+
beforeEach(async () => { store = await factory(); });
|
|
7
|
+
test('writes spans and reconstructs a JSON-safe trace', async () => {
|
|
8
|
+
await store.putSpan(span('trace-a', 'session-a', 'root', 10, 'turn'));
|
|
9
|
+
await store.putSpan(span('trace-a', 'session-a', 'tool', 11, 'tool'));
|
|
10
|
+
const trace = await store.getTrace('trace-a');
|
|
11
|
+
expect(trace?.spans.map((entry) => entry.spanId)).toEqual(['root', 'tool']);
|
|
12
|
+
expect(trace?.sessionId).toBe('session-a');
|
|
13
|
+
expect(() => JSON.stringify(trace)).not.toThrow();
|
|
14
|
+
});
|
|
15
|
+
test('returns null for a missing trace', async () => {
|
|
16
|
+
expect(await store.getTrace('missing')).toBeNull();
|
|
17
|
+
});
|
|
18
|
+
test('lists newest traces for only the requested session and window', async () => {
|
|
19
|
+
await store.putSpan(span('old', 'session-a', 'old-root', 10, 'turn'));
|
|
20
|
+
await store.putSpan(span('new', 'session-a', 'new-root', 30, 'turn'));
|
|
21
|
+
await store.putSpan(span('other', 'session-b', 'other-root', 40, 'turn'));
|
|
22
|
+
const traces = await store.listTraces('session-a', { from: new Date(20), limit: 1 });
|
|
23
|
+
expect(traces.map((trace) => trace.traceId)).toEqual(['new']);
|
|
24
|
+
});
|
|
25
|
+
test('upserts a span by trace and span id', async () => {
|
|
26
|
+
const root = span('trace-a', 'session-a', 'root', 10, 'turn');
|
|
27
|
+
await store.write(root);
|
|
28
|
+
await store.putSpan({ ...root, endTime: 25 });
|
|
29
|
+
expect((await store.getTrace('trace-a'))?.spans).toHaveLength(1);
|
|
30
|
+
expect((await store.getTrace('trace-a'))?.endedAt).toBe(25);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function span(traceId, sessionId, spanId, startTime, kind) {
|
|
35
|
+
return {
|
|
36
|
+
traceId,
|
|
37
|
+
spanId,
|
|
38
|
+
name: kind,
|
|
39
|
+
kind,
|
|
40
|
+
startTime,
|
|
41
|
+
endTime: startTime + 5,
|
|
42
|
+
status: 'ok',
|
|
43
|
+
attributes: { sessionId },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -50,7 +50,8 @@ export interface AgentConfig {
|
|
|
50
50
|
/** Pre-turn refinement policies. Default: none. */
|
|
51
51
|
refine?: RefinementCapability[];
|
|
52
52
|
experimental?: {
|
|
53
|
-
/** Flow reply nodes: silo flow-transition control tools + deterministic evaluator (ADR 0003 H1).
|
|
53
|
+
/** Flow reply nodes: silo flow-transition control tools + deterministic evaluator (ADR 0003 H1).
|
|
54
|
+
* Default ON when the agent declares `flows`; OFF for answering-only agents. Override explicitly to opt out. */
|
|
54
55
|
outOfBandControl?: boolean;
|
|
55
56
|
};
|
|
56
57
|
/** Portable workspace filesystem; auto-registers the durable `workspace` tool when set.
|
package/dist/types/channel.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ToolSet } from 'ai';
|
|
1
|
+
import type { ModelMessage, ToolSet } from 'ai';
|
|
2
2
|
import type { UserInputContent } from '../runtime/userInput.js';
|
|
3
3
|
import type { RunContext } from './run-context.js';
|
|
4
4
|
import type { FlowNode } from './flow.js';
|
|
@@ -34,6 +34,14 @@ export interface ChannelDriver {
|
|
|
34
34
|
* back to runAgentTurn, whose text the engine then discards. */
|
|
35
35
|
runExtraction?(node: ResolvedNode, ctx: RunContext): Promise<TurnResult>;
|
|
36
36
|
}
|
|
37
|
+
export interface TurnUsageSnapshot {
|
|
38
|
+
inputTokens: number;
|
|
39
|
+
outputTokens: number;
|
|
40
|
+
totalTokens: number;
|
|
41
|
+
cacheReadTokens?: number;
|
|
42
|
+
/** Input tokens on the final model call, i.e. current context-window occupancy. */
|
|
43
|
+
contextTokens?: number;
|
|
44
|
+
}
|
|
37
45
|
export interface TurnResult {
|
|
38
46
|
text: string;
|
|
39
47
|
toolResults: ToolResultRecord[];
|
|
@@ -43,6 +51,11 @@ export interface TurnResult {
|
|
|
43
51
|
confidence?: number;
|
|
44
52
|
/** Native realtime post-hoc gate: provider audio already played; gate is advisory only. */
|
|
45
53
|
gateScope?: 'advisory';
|
|
54
|
+
/** AI-SDK tool round-trip messages (assistant tool-call + tool-result) produced this turn,
|
|
55
|
+
* so the host loop can persist them to history in free conversation (G18). */
|
|
56
|
+
toolMessages?: ModelMessage[];
|
|
57
|
+
/** Real token usage from the AI SDK when the driver captured it. */
|
|
58
|
+
usage?: TurnUsageSnapshot;
|
|
46
59
|
}
|
|
47
60
|
export type UserSignal = {
|
|
48
61
|
type: 'message';
|
|
@@ -12,6 +12,10 @@ export interface Tool<TInput = unknown, TOutput = unknown> {
|
|
|
12
12
|
timeoutMs?: number;
|
|
13
13
|
/** When false, the durable journal always re-executes this tool instead of returning a cached step result — for observation/mutation tools (fs, shell) whose result must be fresh. Default true. */
|
|
14
14
|
replay?: boolean;
|
|
15
|
+
/** When true, the tool may run concurrently with other parallel-safe tools in the same model turn. */
|
|
16
|
+
parallelSafe?: boolean;
|
|
17
|
+
/** Override the auto-derived effect key (`idempotencyKey(logicalRunId, callsite, {name,args})`). Use when args are not a stable identity (e.g. a nonce). */
|
|
18
|
+
idempotencyKey?: (args: TInput) => string;
|
|
15
19
|
execute: (args: TInput, ctx?: ToolContext) => Promise<TOutput> | AsyncIterable<TOutput>;
|
|
16
20
|
}
|
|
17
21
|
export type AnyTool = Tool<any, any>;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export * from './tool.js';
|
|
|
10
10
|
export * from './effectTool.js';
|
|
11
11
|
export * from './voice.js';
|
|
12
12
|
export * from './runtime.js';
|
|
13
|
+
export * from './trace.js';
|
|
13
14
|
export type * from '../audit/types.js';
|
|
14
15
|
export type * from '../outcomes/index.js';
|
|
15
16
|
export type * from '../channels/index.js';
|
package/dist/types/index.js
CHANGED