@kuralle-agents/core 0.4.1 → 0.5.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 +11 -2
- package/dist/ai-sdk/uiMessageStream.d.ts +51 -0
- package/dist/ai-sdk/uiMessageStream.js +164 -0
- package/dist/events/TurnHandle.js +7 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/testing/mocks.js +7 -0
- package/dist/types/stream.d.ts +3 -0
- package/guides/RUNTIME.md +18 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -130,9 +130,18 @@ const runtime = createRuntime({
|
|
|
130
130
|
});
|
|
131
131
|
```
|
|
132
132
|
|
|
133
|
-
## HTTP streaming
|
|
133
|
+
## HTTP streaming (web)
|
|
134
134
|
|
|
135
|
-
|
|
135
|
+
For React/web consumers, return a native AI SDK `UIMessageStream` — `useChat` works with no bridge:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
const handle = runtime.run({ input: 'Hello', sessionId: 'demo' });
|
|
139
|
+
return handle.toUIMessageStreamResponse({ sessionId: 'demo' });
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Kuralle orchestration events (flow telemetry, safety blocks, interactive choices) arrive as typed `data-kuralle-*` parts. Import `KuralleUIMessage` and `KuralleDataParts` for compile-time-safe `message.parts` and `useChat({ onData })` handlers.
|
|
143
|
+
|
|
144
|
+
For non-UI consumers (curl, custom transports), use `handle.toResponseStream('sse')` to emit raw `HarnessStreamPart` JSON-SSE. Or use `@kuralle-agents/hono-server` — `POST /api/chat/sse` defaults to native `UIMessageStream`; append `?format=raw` for the legacy wire.
|
|
136
145
|
|
|
137
146
|
## Related
|
|
138
147
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type UIMessage } from 'ai';
|
|
2
|
+
import type { ConversationOutcome } from '../outcomes/types.js';
|
|
3
|
+
import type { ChoiceOption } from '../types/selection.js';
|
|
4
|
+
import type { HarnessStreamPart } from '../types/stream.js';
|
|
5
|
+
export type KuralleMetadata = {
|
|
6
|
+
sessionId?: string;
|
|
7
|
+
};
|
|
8
|
+
export type KuralleDataParts = {
|
|
9
|
+
'kuralle-node': {
|
|
10
|
+
event: 'enter' | 'exit';
|
|
11
|
+
node: string;
|
|
12
|
+
};
|
|
13
|
+
'kuralle-flow': {
|
|
14
|
+
event: 'enter' | 'transition' | 'end';
|
|
15
|
+
flow?: string;
|
|
16
|
+
from?: string;
|
|
17
|
+
to?: string;
|
|
18
|
+
reason?: string;
|
|
19
|
+
};
|
|
20
|
+
'kuralle-handoff': {
|
|
21
|
+
targetAgent: string;
|
|
22
|
+
reason?: string;
|
|
23
|
+
};
|
|
24
|
+
'kuralle-interactive': {
|
|
25
|
+
nodeId: string;
|
|
26
|
+
prompt: string;
|
|
27
|
+
options: ChoiceOption[];
|
|
28
|
+
};
|
|
29
|
+
'kuralle-safety': {
|
|
30
|
+
kind: 'safety-blocked' | 'pipeline-validation-block';
|
|
31
|
+
moderator?: string;
|
|
32
|
+
rationale: string;
|
|
33
|
+
userFacingMessage?: string;
|
|
34
|
+
};
|
|
35
|
+
'kuralle-outcome': {
|
|
36
|
+
outcome: ConversationOutcome;
|
|
37
|
+
};
|
|
38
|
+
'kuralle-control': {
|
|
39
|
+
event: 'interrupted' | 'paused';
|
|
40
|
+
reason?: string;
|
|
41
|
+
waitingFor?: string;
|
|
42
|
+
};
|
|
43
|
+
'kuralle-custom': {
|
|
44
|
+
name: string;
|
|
45
|
+
data: unknown;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
export type KuralleUIMessage = UIMessage<KuralleMetadata, KuralleDataParts>;
|
|
49
|
+
export declare function harnessToUIMessageStream(source: AsyncIterable<HarnessStreamPart>, opts?: {
|
|
50
|
+
sessionId?: string;
|
|
51
|
+
}): ReadableStream;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { createUIMessageStream, generateId, } from 'ai';
|
|
2
|
+
function writeHarnessPart(part, writer) {
|
|
3
|
+
switch (part.type) {
|
|
4
|
+
case 'text-start':
|
|
5
|
+
writer.write({ type: 'text-start', id: part.id });
|
|
6
|
+
break;
|
|
7
|
+
case 'text-delta':
|
|
8
|
+
writer.write({ type: 'text-delta', id: part.id, delta: part.delta });
|
|
9
|
+
break;
|
|
10
|
+
case 'text-end':
|
|
11
|
+
writer.write({ type: 'text-end', id: part.id });
|
|
12
|
+
break;
|
|
13
|
+
case 'text-cancel':
|
|
14
|
+
writer.write({ type: 'text-end', id: part.id });
|
|
15
|
+
break;
|
|
16
|
+
case 'tool-call':
|
|
17
|
+
writer.write({
|
|
18
|
+
type: 'tool-input-available',
|
|
19
|
+
toolCallId: part.toolCallId ?? generateId(),
|
|
20
|
+
toolName: part.toolName,
|
|
21
|
+
input: part.args,
|
|
22
|
+
});
|
|
23
|
+
break;
|
|
24
|
+
case 'tool-result':
|
|
25
|
+
writer.write({
|
|
26
|
+
type: 'tool-output-available',
|
|
27
|
+
toolCallId: part.toolCallId ?? 'unknown',
|
|
28
|
+
output: part.result,
|
|
29
|
+
});
|
|
30
|
+
break;
|
|
31
|
+
case 'node-enter':
|
|
32
|
+
writer.write({
|
|
33
|
+
type: 'data-kuralle-node',
|
|
34
|
+
data: { event: 'enter', node: part.nodeName },
|
|
35
|
+
transient: true,
|
|
36
|
+
});
|
|
37
|
+
break;
|
|
38
|
+
case 'node-exit':
|
|
39
|
+
writer.write({
|
|
40
|
+
type: 'data-kuralle-node',
|
|
41
|
+
data: { event: 'exit', node: part.nodeName },
|
|
42
|
+
transient: true,
|
|
43
|
+
});
|
|
44
|
+
break;
|
|
45
|
+
case 'flow-enter':
|
|
46
|
+
writer.write({
|
|
47
|
+
type: 'data-kuralle-flow',
|
|
48
|
+
data: { event: 'enter', flow: part.flow },
|
|
49
|
+
transient: true,
|
|
50
|
+
});
|
|
51
|
+
break;
|
|
52
|
+
case 'flow-end':
|
|
53
|
+
writer.write({
|
|
54
|
+
type: 'data-kuralle-flow',
|
|
55
|
+
data: { event: 'end', flow: part.flow, reason: part.reason },
|
|
56
|
+
transient: true,
|
|
57
|
+
});
|
|
58
|
+
break;
|
|
59
|
+
case 'flow-transition':
|
|
60
|
+
writer.write({
|
|
61
|
+
type: 'data-kuralle-flow',
|
|
62
|
+
data: { event: 'transition', from: part.from, to: part.to },
|
|
63
|
+
transient: true,
|
|
64
|
+
});
|
|
65
|
+
break;
|
|
66
|
+
case 'handoff':
|
|
67
|
+
writer.write({
|
|
68
|
+
type: 'data-kuralle-handoff',
|
|
69
|
+
id: generateId(),
|
|
70
|
+
data: { targetAgent: part.targetAgent, reason: part.reason },
|
|
71
|
+
});
|
|
72
|
+
break;
|
|
73
|
+
case 'interactive':
|
|
74
|
+
writer.write({
|
|
75
|
+
type: 'data-kuralle-interactive',
|
|
76
|
+
id: part.nodeId,
|
|
77
|
+
data: { nodeId: part.nodeId, prompt: part.prompt, options: part.options },
|
|
78
|
+
});
|
|
79
|
+
break;
|
|
80
|
+
case 'safety-blocked':
|
|
81
|
+
writer.write({
|
|
82
|
+
type: 'data-kuralle-safety',
|
|
83
|
+
id: generateId(),
|
|
84
|
+
data: {
|
|
85
|
+
kind: 'safety-blocked',
|
|
86
|
+
moderator: part.moderator,
|
|
87
|
+
rationale: part.rationale,
|
|
88
|
+
userFacingMessage: part.userFacingMessage,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
break;
|
|
92
|
+
case 'pipeline-validation-block':
|
|
93
|
+
writer.write({
|
|
94
|
+
type: 'data-kuralle-safety',
|
|
95
|
+
id: generateId(),
|
|
96
|
+
data: {
|
|
97
|
+
kind: 'pipeline-validation-block',
|
|
98
|
+
rationale: part.rationale,
|
|
99
|
+
userFacingMessage: part.userFacingMessage,
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
break;
|
|
103
|
+
case 'conversation-outcome':
|
|
104
|
+
writer.write({
|
|
105
|
+
type: 'data-kuralle-outcome',
|
|
106
|
+
id: generateId(),
|
|
107
|
+
data: { outcome: part.outcome },
|
|
108
|
+
});
|
|
109
|
+
break;
|
|
110
|
+
case 'interrupted':
|
|
111
|
+
writer.write({
|
|
112
|
+
type: 'data-kuralle-control',
|
|
113
|
+
data: { event: 'interrupted', reason: part.reason },
|
|
114
|
+
transient: true,
|
|
115
|
+
});
|
|
116
|
+
break;
|
|
117
|
+
case 'paused':
|
|
118
|
+
writer.write({
|
|
119
|
+
type: 'data-kuralle-control',
|
|
120
|
+
data: { event: 'paused', waitingFor: part.waitingFor },
|
|
121
|
+
transient: true,
|
|
122
|
+
});
|
|
123
|
+
break;
|
|
124
|
+
case 'custom':
|
|
125
|
+
writer.write({
|
|
126
|
+
type: 'data-kuralle-custom',
|
|
127
|
+
data: { name: part.name, data: part.data },
|
|
128
|
+
transient: true,
|
|
129
|
+
});
|
|
130
|
+
break;
|
|
131
|
+
case 'error':
|
|
132
|
+
throw new Error(part.error);
|
|
133
|
+
case 'done':
|
|
134
|
+
case 'turn-end':
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
export function harnessToUIMessageStream(source, opts) {
|
|
139
|
+
return createUIMessageStream({
|
|
140
|
+
execute: async ({ writer }) => {
|
|
141
|
+
let doneSessionId = opts?.sessionId;
|
|
142
|
+
if (doneSessionId) {
|
|
143
|
+
writer.write({
|
|
144
|
+
type: 'start',
|
|
145
|
+
messageMetadata: { sessionId: doneSessionId },
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
for await (const part of source) {
|
|
149
|
+
if (part.type === 'done' && part.sessionId) {
|
|
150
|
+
doneSessionId = doneSessionId ?? part.sessionId;
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
writeHarnessPart(part, writer);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (doneSessionId) {
|
|
157
|
+
writer.write({
|
|
158
|
+
type: 'finish',
|
|
159
|
+
messageMetadata: { sessionId: doneSessionId },
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createUIMessageStreamResponse } from 'ai';
|
|
2
|
+
import { harnessToUIMessageStream } from '../ai-sdk/uiMessageStream.js';
|
|
1
3
|
export function createEventBus() {
|
|
2
4
|
const events = [];
|
|
3
5
|
const waiters = [];
|
|
@@ -48,6 +50,11 @@ export function createTurnHandle(options) {
|
|
|
48
50
|
toResponseStream(format = 'sse') {
|
|
49
51
|
return createResponseStream(bus.events(), format);
|
|
50
52
|
},
|
|
53
|
+
toUIMessageStreamResponse(opts) {
|
|
54
|
+
return createUIMessageStreamResponse({
|
|
55
|
+
stream: harnessToUIMessageStream(bus.events(), opts),
|
|
56
|
+
});
|
|
57
|
+
},
|
|
51
58
|
cancel(reason) {
|
|
52
59
|
abortController.abort(reason);
|
|
53
60
|
bus.close();
|
package/dist/index.d.ts
CHANGED
|
@@ -77,6 +77,8 @@ export type { ConfirmVerdict } from './flow/confirmParse.js';
|
|
|
77
77
|
export type { Route } from './types/route.js';
|
|
78
78
|
export type { TurnHandle } from './types/stream.js';
|
|
79
79
|
export type { HarnessStreamPart } from './types/stream.js';
|
|
80
|
+
export { harnessToUIMessageStream } from './ai-sdk/uiMessageStream.js';
|
|
81
|
+
export type { KuralleMetadata, KuralleDataParts, KuralleUIMessage, } from './ai-sdk/uiMessageStream.js';
|
|
80
82
|
export type { ChoiceOption, ResolvedSelection } from './types/selection.js';
|
|
81
83
|
export type { RunState, StepRecord } from './runtime/durable/types.js';
|
|
82
84
|
export type { RunStore } from './runtime/durable/RunStore.js';
|
package/dist/index.js
CHANGED
|
@@ -40,4 +40,5 @@ export { defineAgent, defineFlow, reply, collect, action, decide, confirmGate, }
|
|
|
40
40
|
export { defineTool } from './types/effectTool.js';
|
|
41
41
|
export { buildToolSet, toolToAiSdk, ToolApprovalDeniedError, ToolTimeoutError, } from './tools/effect/index.js';
|
|
42
42
|
export { parseConfirmation } from './flow/confirmParse.js';
|
|
43
|
+
export { harnessToUIMessageStream } from './ai-sdk/uiMessageStream.js';
|
|
43
44
|
export { createRuntime, Runtime, } from './runtime/Runtime.js';
|
package/dist/testing/mocks.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
+
import { createUIMessageStreamResponse } from 'ai';
|
|
2
|
+
import { harnessToUIMessageStream } from '../ai-sdk/uiMessageStream.js';
|
|
1
3
|
export function createMockTurnHandle(events, settled = { text: '', toolResults: [] }) {
|
|
2
4
|
return Object.assign(Promise.resolve(settled), {
|
|
3
5
|
events,
|
|
4
6
|
toResponseStream: () => new ReadableStream(),
|
|
7
|
+
toUIMessageStreamResponse(opts) {
|
|
8
|
+
return createUIMessageStreamResponse({
|
|
9
|
+
stream: harnessToUIMessageStream(events, opts),
|
|
10
|
+
});
|
|
11
|
+
},
|
|
5
12
|
cancel: () => { },
|
|
6
13
|
});
|
|
7
14
|
}
|
package/dist/types/stream.d.ts
CHANGED
|
@@ -91,5 +91,8 @@ export type HarnessStreamPart = {
|
|
|
91
91
|
export interface TurnHandle extends Promise<import('./channel.js').TurnResult> {
|
|
92
92
|
readonly events: AsyncIterable<HarnessStreamPart>;
|
|
93
93
|
toResponseStream(format?: 'sse' | 'ndjson'): ReadableStream;
|
|
94
|
+
toUIMessageStreamResponse(opts?: {
|
|
95
|
+
sessionId?: string;
|
|
96
|
+
}): Response;
|
|
94
97
|
cancel(reason?: string): void;
|
|
95
98
|
}
|
package/guides/RUNTIME.md
CHANGED
|
@@ -11,7 +11,24 @@
|
|
|
11
11
|
|
|
12
12
|
## Stream Events
|
|
13
13
|
|
|
14
|
-
`runtime.run()` returns a `TurnHandle` with an `.events` async iterable of `HarnessStreamPart` items.
|
|
14
|
+
`runtime.run()` returns a `TurnHandle` with an `.events` async iterable of `HarnessStreamPart` items.
|
|
15
|
+
|
|
16
|
+
### Web UI (AI SDK native, 0.5.0+)
|
|
17
|
+
|
|
18
|
+
For React/web consumers, return a native `UIMessageStream` — `useChat` works with no bridge:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
const handle = runtime.run({ input, sessionId });
|
|
22
|
+
return handle.toUIMessageStreamResponse({ sessionId });
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Kuralle orchestration events map to typed `data-kuralle-*` parts (see `docs/adr/0005-ai-sdk-native-uimessage-default.md`). Import `KuralleUIMessage` for compile-time-safe `message.parts`.
|
|
26
|
+
|
|
27
|
+
With `@kuralle-agents/hono-server`, `POST /api/chat/sse` defaults to this native wire. Append `?format=raw` for legacy `HarnessStreamPart` JSON-SSE.
|
|
28
|
+
|
|
29
|
+
### Direct `HarnessStreamPart` consumption
|
|
30
|
+
|
|
31
|
+
For CLI scripts, cascaded voice, messaging, or custom transports, iterate `handle.events` directly. Typical usage renders `text-delta` chunks via `part.delta`.
|
|
15
32
|
|
|
16
33
|
Common types:
|
|
17
34
|
- `text-delta`
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"url": "git+https://github.com/kuralle/kuralle-agents.git",
|
|
7
7
|
"directory": "packages/kuralle-core"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.5.0",
|
|
10
10
|
"description": "A framework for structured conversational AI agents",
|
|
11
11
|
"publishConfig": {
|
|
12
12
|
"access": "public"
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"dotenv": "^16.4.0",
|
|
98
98
|
"typescript": "^5.3.0",
|
|
99
99
|
"zod": "^3.23.0",
|
|
100
|
-
"@kuralle-agents/realtime-audio": "0.
|
|
100
|
+
"@kuralle-agents/realtime-audio": "0.5.0"
|
|
101
101
|
},
|
|
102
102
|
"dependencies": {
|
|
103
103
|
"chrono-node": "^2.6.0",
|