@kuralle-agents/cf-agent 0.7.1 → 0.8.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/dist/BridgeSessionStore.js +7 -1
- package/dist/KuralleAgent.d.ts +30 -2
- package/dist/KuralleAgent.js +79 -31
- package/dist/cfMessageInput.d.ts +11 -0
- package/dist/cfMessageInput.js +42 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/types.d.ts +8 -0
- package/package.json +8 -8
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DURABLE_RUNS_KEY } from '@kuralle-agents/core';
|
|
1
2
|
import { getToolName, isToolUIPart } from 'ai';
|
|
2
3
|
import { OrchestrationStore } from './OrchestrationStore.js';
|
|
3
4
|
/**
|
|
@@ -32,7 +33,7 @@ export class BridgeSessionStore {
|
|
|
32
33
|
const orchState = await this.orchestration.get(key);
|
|
33
34
|
// Convert CF UIMessages to Kuralle ModelMessages
|
|
34
35
|
const messages = convertUIMessagesToModelMessages(this.cfMessages);
|
|
35
|
-
|
|
36
|
+
const session = {
|
|
36
37
|
id: key,
|
|
37
38
|
conversationId: key,
|
|
38
39
|
channelId: 'web',
|
|
@@ -48,6 +49,10 @@ export class BridgeSessionStore {
|
|
|
48
49
|
})),
|
|
49
50
|
state: orchState?.state,
|
|
50
51
|
};
|
|
52
|
+
// Restore the durable run journal so durable tools / suspend-resume can find
|
|
53
|
+
// the run (SessionRunStore reads it off the Session object).
|
|
54
|
+
session[DURABLE_RUNS_KEY] = orchState?.durableRuns ?? {};
|
|
55
|
+
return session;
|
|
51
56
|
}
|
|
52
57
|
/**
|
|
53
58
|
* Save only orchestration state. CF handles message persistence.
|
|
@@ -66,6 +71,7 @@ export class BridgeSessionStore {
|
|
|
66
71
|
: String(h.timestamp),
|
|
67
72
|
})),
|
|
68
73
|
state: session.state,
|
|
74
|
+
durableRuns: session[DURABLE_RUNS_KEY],
|
|
69
75
|
};
|
|
70
76
|
await this.orchestration.save(session.id, state);
|
|
71
77
|
}
|
package/dist/KuralleAgent.d.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { AIChatAgent } from '@cloudflare/ai-chat';
|
|
22
22
|
import { type HarnessConfig } from '@kuralle-agents/core';
|
|
23
|
-
import type { PersistentMemoryStore } from '@kuralle-agents/core';
|
|
23
|
+
import type { PersistentMemoryStore, SignalDelivery } from '@kuralle-agents/core';
|
|
24
24
|
import type { StreamTextOnFinishCallback, ToolSet } from 'ai';
|
|
25
25
|
import type { OnChatMessageOptions } from '@cloudflare/ai-chat';
|
|
26
26
|
import type { StreamAdapterConfig } from './types.js';
|
|
@@ -79,6 +79,12 @@ export declare abstract class KuralleAgent<Env = unknown, State = unknown> exten
|
|
|
79
79
|
* Get the Durable Object ID as the session identifier.
|
|
80
80
|
*/
|
|
81
81
|
private getSessionId;
|
|
82
|
+
/**
|
|
83
|
+
* The hex Durable Object id for this instance. Subclasses use it to mint
|
|
84
|
+
* out-of-band callbacks (e.g. a payment link) that route back to this exact DO
|
|
85
|
+
* via `namespace.idFromString(...)`, then resume it through `resumeWithSignal`.
|
|
86
|
+
*/
|
|
87
|
+
protected getDurableObjectId(): string;
|
|
82
88
|
/**
|
|
83
89
|
* Called by CF when a chat message arrives.
|
|
84
90
|
*
|
|
@@ -102,7 +108,29 @@ export declare abstract class KuralleAgent<Env = unknown, State = unknown> exten
|
|
|
102
108
|
*/
|
|
103
109
|
onChatMessage(onFinish: StreamTextOnFinishCallback<ToolSet>, options?: OnChatMessageOptions): Promise<Response>;
|
|
104
110
|
/**
|
|
105
|
-
*
|
|
111
|
+
* Build a Kuralle runtime for this DO (fresh per request to pick up latest
|
|
112
|
+
* config). Bridges CF messages + DO-backed orchestration/working-memory state.
|
|
113
|
+
* Shared by the chat path and the durable resume path.
|
|
114
|
+
*/
|
|
115
|
+
private buildRuntime;
|
|
116
|
+
/**
|
|
117
|
+
* Resume a suspended run by delivering a durable signal — the server-side
|
|
118
|
+
* counterpart to a human/out-of-band event (e.g. a paid checkout link being
|
|
119
|
+
* hit). Drives the resumed turn to completion, then persists **and broadcasts**
|
|
120
|
+
* the resumed assistant reply through CF's machinery, so a live client sees it
|
|
121
|
+
* and a reconnecting client replays it from history.
|
|
122
|
+
*
|
|
123
|
+
* Idempotent at the durable layer: delivering the same `signal.signalId` twice
|
|
124
|
+
* is deduplicated by the effect log, so a double-clicked link is safe.
|
|
125
|
+
*
|
|
126
|
+
* @returns the assistant text produced by the resumed turn (may be empty).
|
|
127
|
+
*/
|
|
128
|
+
protected resumeWithSignal(signal: SignalDelivery): Promise<{
|
|
129
|
+
text: string;
|
|
130
|
+
}>;
|
|
131
|
+
/**
|
|
132
|
+
* Extract the last user turn from CF's messages as runtime input (multimodal).
|
|
133
|
+
* See `lastUserInputFromMessages`.
|
|
106
134
|
*/
|
|
107
135
|
private getLastUserInput;
|
|
108
136
|
/**
|
package/dist/KuralleAgent.js
CHANGED
|
@@ -26,6 +26,7 @@ import { SqlPersistentMemoryStore } from './SqlPersistentMemoryStore.js';
|
|
|
26
26
|
import { createSSEResponse } from './StreamAdapter.js';
|
|
27
27
|
import { DEFAULT_STREAM_CONFIG } from './types.js';
|
|
28
28
|
import { durableAgentSurface } from './durable-agent-surface.js';
|
|
29
|
+
import { lastUserInputFromMessages } from './cfMessageInput.js';
|
|
29
30
|
/**
|
|
30
31
|
* Abstract base class for running Kuralle agents on Cloudflare.
|
|
31
32
|
*
|
|
@@ -83,6 +84,14 @@ export class KuralleAgent extends AIChatAgent {
|
|
|
83
84
|
getSessionId() {
|
|
84
85
|
return durableAgentSurface(this).ctx.id.toString();
|
|
85
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* The hex Durable Object id for this instance. Subclasses use it to mint
|
|
89
|
+
* out-of-band callbacks (e.g. a payment link) that route back to this exact DO
|
|
90
|
+
* via `namespace.idFromString(...)`, then resume it through `resumeWithSignal`.
|
|
91
|
+
*/
|
|
92
|
+
getDurableObjectId() {
|
|
93
|
+
return this.getSessionId();
|
|
94
|
+
}
|
|
86
95
|
/**
|
|
87
96
|
* Called by CF when a chat message arrives.
|
|
88
97
|
*
|
|
@@ -110,19 +119,42 @@ export class KuralleAgent extends AIChatAgent {
|
|
|
110
119
|
if (!lastUserMessage) {
|
|
111
120
|
return new Response('No user message', { status: 400 });
|
|
112
121
|
}
|
|
122
|
+
const sessionId = this.getSessionId();
|
|
123
|
+
this.runtime = this.buildRuntime();
|
|
124
|
+
const handle = this.runtime.run({
|
|
125
|
+
input: lastUserMessage,
|
|
126
|
+
sessionId,
|
|
127
|
+
userId: options?.body?.userId,
|
|
128
|
+
abortSignal: options?.abortSignal,
|
|
129
|
+
});
|
|
130
|
+
const streamConfig = {
|
|
131
|
+
...DEFAULT_STREAM_CONFIG,
|
|
132
|
+
...this.getStreamConfig(),
|
|
133
|
+
};
|
|
134
|
+
async function* parts() {
|
|
135
|
+
for await (const part of handle.events) {
|
|
136
|
+
yield part;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return createSSEResponse(parts(), streamConfig);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Build a Kuralle runtime for this DO (fresh per request to pick up latest
|
|
143
|
+
* config). Bridges CF messages + DO-backed orchestration/working-memory state.
|
|
144
|
+
* Shared by the chat path and the durable resume path.
|
|
145
|
+
*/
|
|
146
|
+
buildRuntime() {
|
|
113
147
|
const sessionId = this.getSessionId();
|
|
114
148
|
const defaultAgentId = this.getDefaultAgentId();
|
|
115
|
-
// Build session store that bridges CF messages with Kuralle state
|
|
116
149
|
const sessionStore = new BridgeSessionStore({
|
|
117
150
|
sqlExecutor: this.getSql(),
|
|
118
151
|
cfMessages: this.messages,
|
|
119
152
|
sessionId,
|
|
120
153
|
defaultAgentId,
|
|
121
154
|
});
|
|
122
|
-
// Build runtime (fresh per request to pick up latest config)
|
|
123
155
|
const extraConfig = this.getRuntimeConfig();
|
|
124
156
|
const workingMemoryStore = this.getWorkingMemoryStore();
|
|
125
|
-
|
|
157
|
+
return createRuntime({
|
|
126
158
|
...extraConfig,
|
|
127
159
|
agents: this.getAgents(),
|
|
128
160
|
defaultAgentId,
|
|
@@ -131,39 +163,45 @@ export class KuralleAgent extends AIChatAgent {
|
|
|
131
163
|
? { defaultWorkingMemoryStore: workingMemoryStore }
|
|
132
164
|
: {}),
|
|
133
165
|
});
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Resume a suspended run by delivering a durable signal — the server-side
|
|
169
|
+
* counterpart to a human/out-of-band event (e.g. a paid checkout link being
|
|
170
|
+
* hit). Drives the resumed turn to completion, then persists **and broadcasts**
|
|
171
|
+
* the resumed assistant reply through CF's machinery, so a live client sees it
|
|
172
|
+
* and a reconnecting client replays it from history.
|
|
173
|
+
*
|
|
174
|
+
* Idempotent at the durable layer: delivering the same `signal.signalId` twice
|
|
175
|
+
* is deduplicated by the effect log, so a double-clicked link is safe.
|
|
176
|
+
*
|
|
177
|
+
* @returns the assistant text produced by the resumed turn (may be empty).
|
|
178
|
+
*/
|
|
179
|
+
async resumeWithSignal(signal) {
|
|
180
|
+
const sessionId = this.getSessionId();
|
|
181
|
+
const runtime = this.buildRuntime();
|
|
182
|
+
const handle = runtime.run({ sessionId, signalDelivery: signal });
|
|
183
|
+
let text = '';
|
|
184
|
+
for await (const part of handle.events) {
|
|
185
|
+
if (part.type === 'text-delta')
|
|
186
|
+
text += part.delta;
|
|
148
187
|
}
|
|
149
|
-
|
|
188
|
+
await handle;
|
|
189
|
+
if (text.trim()) {
|
|
190
|
+
const assistantMessage = {
|
|
191
|
+
id: crypto.randomUUID(),
|
|
192
|
+
role: 'assistant',
|
|
193
|
+
parts: [{ type: 'text', text }],
|
|
194
|
+
};
|
|
195
|
+
await this.persistMessages([...this.messages, assistantMessage]);
|
|
196
|
+
}
|
|
197
|
+
return { text };
|
|
150
198
|
}
|
|
151
199
|
/**
|
|
152
|
-
* Extract the
|
|
200
|
+
* Extract the last user turn from CF's messages as runtime input (multimodal).
|
|
201
|
+
* See `lastUserInputFromMessages`.
|
|
153
202
|
*/
|
|
154
203
|
getLastUserInput() {
|
|
155
|
-
|
|
156
|
-
const msg = this.messages[i];
|
|
157
|
-
if (msg.role !== 'user')
|
|
158
|
-
continue;
|
|
159
|
-
const text = msg.parts
|
|
160
|
-
?.filter((part) => part.type === 'text')
|
|
161
|
-
.map((part) => part.text)
|
|
162
|
-
.join('');
|
|
163
|
-
if (text?.trim())
|
|
164
|
-
return text;
|
|
165
|
-
}
|
|
166
|
-
return null;
|
|
204
|
+
return lastUserInputFromMessages(this.messages);
|
|
167
205
|
}
|
|
168
206
|
/**
|
|
169
207
|
* Delete orchestration rows older than `maxAgeMs`. Returns the number of
|
|
@@ -191,6 +229,16 @@ export class KuralleAgent extends AIChatAgent {
|
|
|
191
229
|
*/
|
|
192
230
|
async onRequest(request) {
|
|
193
231
|
const url = new URL(request.url);
|
|
232
|
+
// Durable resume: deliver a signal to a suspended run (e.g. a paid checkout
|
|
233
|
+
// link). Body: { signalId: string, name: string, payload?: unknown }.
|
|
234
|
+
if (request.method === 'POST' && url.pathname.endsWith('/resume')) {
|
|
235
|
+
const body = (await request.json().catch(() => null));
|
|
236
|
+
if (!body || typeof body.signalId !== 'string' || typeof body.name !== 'string') {
|
|
237
|
+
return Response.json({ error: 'signalId and name are required' }, { status: 400 });
|
|
238
|
+
}
|
|
239
|
+
const { text } = await this.resumeWithSignal(body);
|
|
240
|
+
return Response.json({ ok: true, text });
|
|
241
|
+
}
|
|
194
242
|
if (url.pathname.endsWith('/orchestration-state')) {
|
|
195
243
|
const store = new OrchestrationStore(this.getSql());
|
|
196
244
|
// OrchestrationStore is now keyed by sessionId (was a single `'default'`
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { UserInputContent } from '@kuralle-agents/core';
|
|
2
|
+
import type { UIMessage } from 'ai';
|
|
3
|
+
/**
|
|
4
|
+
* Map the last user turn in a CF `UIMessage[]` to runtime `UserInputContent`.
|
|
5
|
+
*
|
|
6
|
+
* Text parts → `TextPart`; file parts (images / documents / audio uploaded via the
|
|
7
|
+
* CF chat client) → `FilePart` carrying the part's URL, so multimodal input reaches
|
|
8
|
+
* the model instead of being dropped. A text-only turn collapses to a plain string.
|
|
9
|
+
* Returns `null` when the last user turn carries no usable content.
|
|
10
|
+
*/
|
|
11
|
+
export declare function lastUserInputFromMessages(messages: UIMessage[]): UserInputContent | null;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map the last user turn in a CF `UIMessage[]` to runtime `UserInputContent`.
|
|
3
|
+
*
|
|
4
|
+
* Text parts → `TextPart`; file parts (images / documents / audio uploaded via the
|
|
5
|
+
* CF chat client) → `FilePart` carrying the part's URL, so multimodal input reaches
|
|
6
|
+
* the model instead of being dropped. A text-only turn collapses to a plain string.
|
|
7
|
+
* Returns `null` when the last user turn carries no usable content.
|
|
8
|
+
*/
|
|
9
|
+
export function lastUserInputFromMessages(messages) {
|
|
10
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
11
|
+
const msg = messages[i];
|
|
12
|
+
if (!msg || msg.role !== 'user')
|
|
13
|
+
continue;
|
|
14
|
+
const content = [];
|
|
15
|
+
for (const part of msg.parts ?? []) {
|
|
16
|
+
if (part.type === 'text' && typeof part.text === 'string') {
|
|
17
|
+
content.push({ type: 'text', text: part.text });
|
|
18
|
+
}
|
|
19
|
+
else if (part.type === 'file' &&
|
|
20
|
+
typeof part.url === 'string' &&
|
|
21
|
+
typeof part.mediaType === 'string') {
|
|
22
|
+
content.push({
|
|
23
|
+
type: 'file',
|
|
24
|
+
data: part.url,
|
|
25
|
+
mediaType: part.mediaType,
|
|
26
|
+
filename: part.filename,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (content.length === 0)
|
|
31
|
+
continue;
|
|
32
|
+
const hasFile = content.some((p) => p.type === 'file');
|
|
33
|
+
if (!hasFile) {
|
|
34
|
+
const text = content.map((p) => (p.type === 'text' ? p.text : '')).join('');
|
|
35
|
+
if (text.trim())
|
|
36
|
+
return text;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
return content;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -29,6 +29,7 @@ export { OrchestrationStore } from './OrchestrationStore.js';
|
|
|
29
29
|
export { SqlPersistentMemoryStore } from './SqlPersistentMemoryStore.js';
|
|
30
30
|
export { createSqlExecutor } from './sqlExecutor.js';
|
|
31
31
|
export { createSSEResponse } from './StreamAdapter.js';
|
|
32
|
+
export { lastUserInputFromMessages } from './cfMessageInput.js';
|
|
32
33
|
export type { StreamAdapterConfig, OrchestrationState, SqlExecutor, } from './types.js';
|
|
33
34
|
export { DEFAULT_STREAM_CONFIG } from './types.js';
|
|
34
35
|
export type { HarnessConfig, HarnessHooks, HarnessStreamPart, Session, } from '@kuralle-agents/core';
|
package/dist/index.js
CHANGED
|
@@ -29,4 +29,5 @@ export { OrchestrationStore } from './OrchestrationStore.js';
|
|
|
29
29
|
export { SqlPersistentMemoryStore } from './SqlPersistentMemoryStore.js';
|
|
30
30
|
export { createSqlExecutor } from './sqlExecutor.js';
|
|
31
31
|
export { createSSEResponse } from './StreamAdapter.js';
|
|
32
|
+
export { lastUserInputFromMessages } from './cfMessageInput.js';
|
|
32
33
|
export { DEFAULT_STREAM_CONFIG } from './types.js';
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SessionDurableRuns } from '@kuralle-agents/core';
|
|
1
2
|
/**
|
|
2
3
|
* SQL executor type matching Cloudflare's Durable Object sql binding.
|
|
3
4
|
*
|
|
@@ -27,6 +28,13 @@ export interface OrchestrationState {
|
|
|
27
28
|
timestamp: string;
|
|
28
29
|
}>;
|
|
29
30
|
state?: Record<string, unknown>;
|
|
31
|
+
/**
|
|
32
|
+
* Kuralle durable run journal (effect log + run state) for this session.
|
|
33
|
+
* `SessionRunStore` keeps the run on the Session object, so the bridge must
|
|
34
|
+
* persist + restore it here — otherwise durable tools and suspend/resume fail
|
|
35
|
+
* with "Run not found" on CF.
|
|
36
|
+
*/
|
|
37
|
+
durableRuns?: SessionDurableRuns;
|
|
30
38
|
}
|
|
31
39
|
/**
|
|
32
40
|
* Configuration for the stream adapter.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kuralle-agents/cf-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Kuralle agent integration for Cloudflare Workers with AIChatAgent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -19,22 +19,22 @@
|
|
|
19
19
|
"dist"
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@cloudflare/ai-chat": "^0.
|
|
22
|
+
"@cloudflare/ai-chat": "^0.8.4",
|
|
23
23
|
"ai": "^6.0.90",
|
|
24
|
-
"@kuralle-agents/core": "0.
|
|
25
|
-
"@kuralle-agents/realtime-audio": "0.
|
|
24
|
+
"@kuralle-agents/core": "0.8.0",
|
|
25
|
+
"@kuralle-agents/realtime-audio": "0.8.0"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"agents": "
|
|
28
|
+
"agents": ">=0.14.0 <1.0.0",
|
|
29
29
|
"zod": "^4.0.0",
|
|
30
|
-
"@kuralle-agents/voice-protocol": "0.
|
|
30
|
+
"@kuralle-agents/voice-protocol": "0.8.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@cloudflare/vitest-pool-workers": "^0.12.7",
|
|
34
|
-
"agents": "^0.
|
|
34
|
+
"agents": "^0.15.0",
|
|
35
35
|
"typescript": "^5.7.0",
|
|
36
36
|
"vitest": "^3.2.4",
|
|
37
|
-
"@kuralle-agents/voice-protocol": "0.
|
|
37
|
+
"@kuralle-agents/voice-protocol": "0.8.0"
|
|
38
38
|
},
|
|
39
39
|
"engines": {
|
|
40
40
|
"node": ">=20"
|