@copilotkit/channels-core 0.4.1-canary.1785375021 → 0.4.1-canary.narrowinstall1
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/codec.d.ts +3 -2
- package/dist/codec.d.ts.map +1 -1
- package/dist/create-channel.d.ts.map +1 -1
- package/dist/create-channel.js +31 -35
- package/dist/create-channel.test.js +0 -75
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/platform-adapter.d.ts +9 -64
- package/dist/platform-adapter.d.ts.map +1 -1
- package/dist/run-loop.d.ts +6 -25
- package/dist/run-loop.d.ts.map +1 -1
- package/dist/run-loop.js +39 -280
- package/dist/thread.d.ts +0 -4
- package/dist/thread.d.ts.map +1 -1
- package/dist/thread.js +243 -319
- package/package.json +4 -4
- package/dist/canonical-run-loop.test.d.ts +0 -2
- package/dist/canonical-run-loop.test.d.ts.map +0 -1
- package/dist/canonical-run-loop.test.js +0 -393
- package/dist/managed-v1-await-choice-guard.test.d.ts +0 -2
- package/dist/managed-v1-await-choice-guard.test.d.ts.map +0 -1
- package/dist/managed-v1-await-choice-guard.test.js +0 -52
- package/dist/source-platform.test.d.ts +0 -2
- package/dist/source-platform.test.d.ts.map +0 -1
- package/dist/source-platform.test.js +0 -149
- package/dist/thread-promise-contract.test.d.ts +0 -2
- package/dist/thread-promise-contract.test.d.ts.map +0 -1
- package/dist/thread-promise-contract.test.js +0 -37
package/dist/thread.js
CHANGED
|
@@ -2,14 +2,6 @@ import { runAgentLoop } from "./run-loop.js";
|
|
|
2
2
|
import { errorClass, normalizePlatform } from "./telemetry/sanitize-error.js";
|
|
3
3
|
import { toAgentToolDescriptors } from "./tools.js";
|
|
4
4
|
import { validateSchema } from "./standard-schema.js";
|
|
5
|
-
/** Stable rejection for surfaces that cannot hold one run open for a choice. */
|
|
6
|
-
class ChannelAwaitChoiceNotSupportedError extends Error {
|
|
7
|
-
code = "channel_await_choice_not_supported";
|
|
8
|
-
constructor() {
|
|
9
|
-
super("Managed Channels v1 does not support Thread.awaitChoice(); post the picker in an onInterrupt handler and call Thread.resume() from the later interaction delivery.");
|
|
10
|
-
this.name = "ChannelAwaitChoiceNotSupportedError";
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
5
|
/** A concrete conversation thread: posts UI, runs the agent loop, and resolves HITL waiters. */
|
|
14
6
|
export class Thread {
|
|
15
7
|
deps;
|
|
@@ -19,10 +11,9 @@ export class Thread {
|
|
|
19
11
|
/** Mirrors the adapter's `supportsBlockingChoice` capability (see SurfaceCapabilities). */
|
|
20
12
|
supportsBlockingChoice;
|
|
21
13
|
store;
|
|
22
|
-
implicitInboundConsumed = false;
|
|
23
14
|
constructor(deps) {
|
|
24
15
|
this.deps = deps;
|
|
25
|
-
this.platform = deps.
|
|
16
|
+
this.platform = deps.adapter.platform;
|
|
26
17
|
this.conversationKey = deps.conversationKey;
|
|
27
18
|
this.supportsBlockingChoice =
|
|
28
19
|
deps.adapter.capabilities.supportsBlockingChoice;
|
|
@@ -31,17 +22,6 @@ export class Thread {
|
|
|
31
22
|
async bindForPost(ui) {
|
|
32
23
|
return this.deps.registry.bindRenderable(ui, this.deps.conversationKey);
|
|
33
24
|
}
|
|
34
|
-
trackOperation(operation) {
|
|
35
|
-
if (!this.deps.adapter.trackThreadOperation) {
|
|
36
|
-
try {
|
|
37
|
-
return operation();
|
|
38
|
-
}
|
|
39
|
-
catch (error) {
|
|
40
|
-
return Promise.reject(error);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return this.deps.adapter.trackThreadOperation(this.deps.replyTarget, operation);
|
|
44
|
-
}
|
|
45
25
|
/**
|
|
46
26
|
* Wire a posted message's `onReaction` to its returned id: cache it for this
|
|
47
27
|
* process and, when it came from a component, persist a durable snapshot so a
|
|
@@ -58,355 +38,299 @@ export class Thread {
|
|
|
58
38
|
});
|
|
59
39
|
}
|
|
60
40
|
}
|
|
61
|
-
post(ui) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return ref;
|
|
67
|
-
});
|
|
41
|
+
async post(ui) {
|
|
42
|
+
const bound = await this.bindForPost(ui);
|
|
43
|
+
const ref = await this.deps.adapter.post(this.deps.replyTarget, bound.root);
|
|
44
|
+
await this.bindReaction(ref.id, bound);
|
|
45
|
+
return ref;
|
|
68
46
|
}
|
|
69
|
-
update(ref, ui) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return ref;
|
|
75
|
-
});
|
|
47
|
+
async update(ref, ui) {
|
|
48
|
+
const bound = await this.bindForPost(ui);
|
|
49
|
+
await this.deps.adapter.update(ref, bound.root);
|
|
50
|
+
await this.bindReaction(ref.id, bound);
|
|
51
|
+
return ref;
|
|
76
52
|
}
|
|
77
|
-
delete(ref) {
|
|
78
|
-
|
|
53
|
+
async delete(ref) {
|
|
54
|
+
await this.deps.adapter.delete(ref);
|
|
79
55
|
}
|
|
80
|
-
stream(src) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return this.deps.adapter.stream(this.deps.replyTarget, iter);
|
|
88
|
-
});
|
|
56
|
+
async stream(src) {
|
|
57
|
+
const iter = typeof src === "string"
|
|
58
|
+
? (async function* () {
|
|
59
|
+
yield src;
|
|
60
|
+
})()
|
|
61
|
+
: src;
|
|
62
|
+
return this.deps.adapter.stream(this.deps.replyTarget, iter);
|
|
89
63
|
}
|
|
90
|
-
postFile(args) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return adapter.postFile(this.deps.replyTarget, args);
|
|
100
|
-
});
|
|
64
|
+
async postFile(args) {
|
|
65
|
+
const adapter = this.deps.adapter;
|
|
66
|
+
if (!adapter.postFile) {
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
error: `${this.platform} does not support file upload`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return adapter.postFile(this.deps.replyTarget, args);
|
|
101
73
|
}
|
|
102
74
|
/** Pin suggested prompts (returns `{ ok: false }` on surfaces without support). */
|
|
103
|
-
setSuggestedPrompts(prompts, opts) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return adapter.setSuggestedPrompts(this.deps.replyTarget, prompts, opts);
|
|
113
|
-
});
|
|
75
|
+
async setSuggestedPrompts(prompts, opts) {
|
|
76
|
+
const adapter = this.deps.adapter;
|
|
77
|
+
if (!adapter.setSuggestedPrompts) {
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
error: `${this.platform} does not support suggested prompts`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return adapter.setSuggestedPrompts(this.deps.replyTarget, prompts, opts);
|
|
114
84
|
}
|
|
115
85
|
/** Name this conversation (returns `{ ok: false }` on surfaces without support). */
|
|
116
|
-
setTitle(title) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return adapter.setThreadTitle(this.deps.replyTarget, title);
|
|
126
|
-
});
|
|
86
|
+
async setTitle(title) {
|
|
87
|
+
const adapter = this.deps.adapter;
|
|
88
|
+
if (!adapter.setThreadTitle) {
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
error: `${this.platform} does not support thread titles`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return adapter.setThreadTitle(this.deps.replyTarget, title);
|
|
127
95
|
}
|
|
128
96
|
/** Add an emoji reaction to a message (capability-gated; `{ ok: false }` on surfaces without support). */
|
|
129
|
-
react(messageRef, emoji) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
return adapter.addReaction(this.deps.replyTarget, messageRef, emoji);
|
|
139
|
-
});
|
|
97
|
+
async react(messageRef, emoji) {
|
|
98
|
+
const adapter = this.deps.adapter;
|
|
99
|
+
if (!adapter.addReaction) {
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
error: `${this.platform} does not support reactions`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return adapter.addReaction(this.deps.replyTarget, messageRef, emoji);
|
|
140
106
|
}
|
|
141
107
|
/** Remove the channel's emoji reaction from a message (capability-gated). */
|
|
142
|
-
unreact(messageRef, emoji) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return adapter.removeReaction(this.deps.replyTarget, messageRef, emoji);
|
|
152
|
-
});
|
|
108
|
+
async unreact(messageRef, emoji) {
|
|
109
|
+
const adapter = this.deps.adapter;
|
|
110
|
+
if (!adapter.removeReaction) {
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
error: `${this.platform} does not support reactions`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return adapter.removeReaction(this.deps.replyTarget, messageRef, emoji);
|
|
153
117
|
}
|
|
154
118
|
/**
|
|
155
119
|
* Post a message only `user` can see. `fallbackToDM` is required:
|
|
156
120
|
* `true` → DM the user when native ephemeral is unsupported; `false` →
|
|
157
121
|
* resolve to `null` when native ephemeral is unsupported.
|
|
158
122
|
*/
|
|
159
|
-
postEphemeral(user, ui, opts) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return adapter.postEphemeral(this.deps.replyTarget, user, root, opts);
|
|
172
|
-
});
|
|
123
|
+
async postEphemeral(user, ui, opts) {
|
|
124
|
+
const adapter = this.deps.adapter;
|
|
125
|
+
if (!adapter.postEphemeral) {
|
|
126
|
+
return {
|
|
127
|
+
ok: false,
|
|
128
|
+
error: `${this.platform} does not support ephemeral messages`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
// Ephemeral messages can't be reacted to, so any `onReaction` is dropped
|
|
132
|
+
// (stripped by bindForPost) rather than registered.
|
|
133
|
+
const { root } = await this.bindForPost(ui);
|
|
134
|
+
return adapter.postEphemeral(this.deps.replyTarget, user, root, opts);
|
|
173
135
|
}
|
|
174
136
|
// Subscription STORAGE lands here; subscription ROUTING (onSubscribedMessage) is deferred.
|
|
175
137
|
/** Record this conversation as subscribed (persisted in state). Proactive delivery to subscribed conversations is not yet wired. */
|
|
176
|
-
subscribe() {
|
|
177
|
-
|
|
138
|
+
async subscribe() {
|
|
139
|
+
await this.store.kv.set(`sub:${this.deps.conversationKey}`, true);
|
|
178
140
|
}
|
|
179
141
|
/** Remove the subscription for this conversation. */
|
|
180
|
-
unsubscribe() {
|
|
181
|
-
|
|
142
|
+
async unsubscribe() {
|
|
143
|
+
await this.store.kv.delete(`sub:${this.deps.conversationKey}`);
|
|
182
144
|
}
|
|
183
145
|
/** Returns true if this conversation is currently subscribed. */
|
|
184
|
-
isSubscribed() {
|
|
185
|
-
return
|
|
146
|
+
async isSubscribed() {
|
|
147
|
+
return ((await this.store.kv.get(`sub:${this.deps.conversationKey}`)) ===
|
|
148
|
+
true);
|
|
186
149
|
}
|
|
187
150
|
/** Persist arbitrary per-thread state (e.g. workflow step). */
|
|
188
|
-
setState(v) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
await this.store.kv.set(`threadstate:${this.deps.conversationKey}`, value);
|
|
198
|
-
});
|
|
151
|
+
async setState(v) {
|
|
152
|
+
let value = v;
|
|
153
|
+
if (this.deps.stateSchema) {
|
|
154
|
+
const r = await validateSchema(this.deps.stateSchema, v);
|
|
155
|
+
if (!r.ok)
|
|
156
|
+
throw new Error(`thread.setState: invalid state — ${r.error}`);
|
|
157
|
+
value = r.value;
|
|
158
|
+
}
|
|
159
|
+
await this.store.kv.set(`threadstate:${this.deps.conversationKey}`, value);
|
|
199
160
|
}
|
|
200
161
|
/** Read back per-thread state previously written with `setState`. */
|
|
201
|
-
state() {
|
|
202
|
-
return this.
|
|
162
|
+
async state() {
|
|
163
|
+
return this.store.kv.get(`threadstate:${this.deps.conversationKey}`);
|
|
203
164
|
}
|
|
204
165
|
/** Read the conversation's messages (returns `[]` when the adapter can't read history). */
|
|
205
|
-
getMessages() {
|
|
206
|
-
return
|
|
166
|
+
async getMessages() {
|
|
167
|
+
return (await this.deps.adapter.getMessages?.(this.deps.replyTarget)) ?? [];
|
|
207
168
|
}
|
|
208
169
|
/** Resolve a platform user by free-form query (returns `undefined` when unsupported). */
|
|
209
|
-
lookupUser(query) {
|
|
210
|
-
return this.
|
|
170
|
+
async lookupUser(query) {
|
|
171
|
+
return this.deps.adapter.lookupUser?.({ query });
|
|
211
172
|
}
|
|
212
173
|
/** Post a picker and wait until an interaction in this conversation resolves it. */
|
|
213
|
-
awaitChoice(ui) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
return this.trackOperation(async () => {
|
|
218
|
-
const p = new Promise((resolve) => this.deps.registerWaiter(this.deps.conversationKey, resolve));
|
|
219
|
-
await this.post(ui);
|
|
220
|
-
return p;
|
|
221
|
-
});
|
|
174
|
+
async awaitChoice(ui) {
|
|
175
|
+
const p = new Promise((resolve) => this.deps.registerWaiter(this.deps.conversationKey, resolve));
|
|
176
|
+
await this.post(ui);
|
|
177
|
+
return p;
|
|
222
178
|
}
|
|
223
|
-
runAgent(input) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
: message?.text;
|
|
235
|
-
const implicitPrompt = input?.prompt === undefined &&
|
|
236
|
-
!this.deps.adapter.conversationStore.seedsInboundTurn &&
|
|
237
|
-
(!this.deps.adapter.injectInboundTurnOnce ||
|
|
238
|
-
!this.implicitInboundConsumed);
|
|
239
|
-
if (implicitPrompt && defaultPrompt) {
|
|
240
|
-
this.implicitInboundConsumed = true;
|
|
241
|
-
}
|
|
242
|
-
return this.run(undefined, {
|
|
243
|
-
...input,
|
|
244
|
-
prompt: input?.prompt ?? (!implicitPrompt ? undefined : defaultPrompt),
|
|
245
|
-
});
|
|
179
|
+
async runAgent(input) {
|
|
180
|
+
const message = this.deps.message;
|
|
181
|
+
const defaultPrompt = message?.contentParts && message.contentParts.length > 0
|
|
182
|
+
? message.contentParts
|
|
183
|
+
: message?.text;
|
|
184
|
+
return this.run(undefined, {
|
|
185
|
+
...input,
|
|
186
|
+
prompt: input?.prompt ??
|
|
187
|
+
(this.deps.adapter.conversationStore.seedsInboundTurn
|
|
188
|
+
? undefined
|
|
189
|
+
: defaultPrompt),
|
|
246
190
|
});
|
|
247
191
|
}
|
|
248
|
-
resume(value) {
|
|
249
|
-
return this.
|
|
192
|
+
async resume(value) {
|
|
193
|
+
return this.run({ resume: value });
|
|
250
194
|
}
|
|
251
195
|
async run(initialResume, extra) {
|
|
252
196
|
const session = await this.deps.adapter.conversationStore.getOrCreate(this.deps.conversationKey, this.deps.replyTarget, this.deps.agentFactory);
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
if (
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
if (this.deps.message) {
|
|
295
|
-
await transcripts.append(this, this.deps.message, { userKey });
|
|
296
|
-
}
|
|
197
|
+
// Inject an explicit user message when the input isn't in the adapter's
|
|
198
|
+
// reconstructed history (e.g. a slash command's args, or inbound image/file
|
|
199
|
+
// attachments built into multimodal content parts). A non-empty array is
|
|
200
|
+
// truthy, so this guard also admits multimodal prompts.
|
|
201
|
+
if (extra?.prompt) {
|
|
202
|
+
session.agent.addMessage({
|
|
203
|
+
id: globalThis.crypto.randomUUID(),
|
|
204
|
+
role: "user",
|
|
205
|
+
// AG-UI types `content` as `string`, but multimodal works at runtime by
|
|
206
|
+
// setting it to an `AgentContentPart[]` — the runtime's LLM adapter
|
|
207
|
+
// converts the parts to the provider's multimodal format. We cast to
|
|
208
|
+
// satisfy the string-typed field (channels-slack parity — it does the same
|
|
209
|
+
// when assigning multimodal `content` to its reconstructed messages).
|
|
210
|
+
content: extra.prompt,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
const renderer = this.deps.adapter.createRunRenderer(this.deps.replyTarget);
|
|
214
|
+
// Transcript auto-bridge (step 1 + 2): inject prior cross-platform history
|
|
215
|
+
// as a context entry, then append the current user turn. This flag owns the
|
|
216
|
+
// bridge — see `runAgent`'s `transcript` doc. No-ops with one warning when
|
|
217
|
+
// identity/transcripts aren't configured.
|
|
218
|
+
const transcripts = this.deps.transcripts;
|
|
219
|
+
const userKey = this.deps.userKey;
|
|
220
|
+
let transcriptContext;
|
|
221
|
+
if (extra?.transcript) {
|
|
222
|
+
if (transcripts && userKey) {
|
|
223
|
+
const limit = typeof extra.transcript === "object"
|
|
224
|
+
? (extra.transcript.limit ?? 20)
|
|
225
|
+
: 20;
|
|
226
|
+
// List BEFORE appending the current user turn so the current message
|
|
227
|
+
// isn't counted as its own "prior history".
|
|
228
|
+
const prior = await transcripts.list({ userKey, limit });
|
|
229
|
+
if (prior.length > 0) {
|
|
230
|
+
transcriptContext = {
|
|
231
|
+
description: `Prior cross-platform conversation history with this user. Current channel: ${this.platform}.`,
|
|
232
|
+
value: prior
|
|
233
|
+
.map((e) => `[${e.platform}] ${e.role}: ${e.text}`)
|
|
234
|
+
.join("\n"),
|
|
235
|
+
};
|
|
297
236
|
}
|
|
298
|
-
|
|
299
|
-
|
|
237
|
+
if (this.deps.message) {
|
|
238
|
+
await transcripts.append(this, this.deps.message, { userKey });
|
|
300
239
|
}
|
|
301
240
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
let tools = this.deps.tools;
|
|
305
|
-
let toolDescriptors = this.deps.toolDescriptors;
|
|
306
|
-
if (extraTools.length > 0) {
|
|
307
|
-
tools = new Map(this.deps.tools);
|
|
308
|
-
for (const t of extraTools)
|
|
309
|
-
tools.set(t.name, t);
|
|
310
|
-
toolDescriptors = [
|
|
311
|
-
...this.deps.toolDescriptors,
|
|
312
|
-
...toAgentToolDescriptors(extraTools),
|
|
313
|
-
];
|
|
241
|
+
else {
|
|
242
|
+
warnTranscriptIgnored();
|
|
314
243
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
244
|
+
}
|
|
245
|
+
// Merge per-run context/tools (this run only) on top of the channel-level deps.
|
|
246
|
+
const extraTools = extra?.tools ?? [];
|
|
247
|
+
let tools = this.deps.tools;
|
|
248
|
+
let toolDescriptors = this.deps.toolDescriptors;
|
|
249
|
+
if (extraTools.length > 0) {
|
|
250
|
+
tools = new Map(this.deps.tools);
|
|
251
|
+
for (const t of extraTools)
|
|
252
|
+
tools.set(t.name, t);
|
|
253
|
+
toolDescriptors = [
|
|
254
|
+
...this.deps.toolDescriptors,
|
|
255
|
+
...toAgentToolDescriptors(extraTools),
|
|
319
256
|
];
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const produced = session.agent.messages.slice(messagesBefore);
|
|
369
|
-
const text = produced
|
|
370
|
-
.filter((m) => m.role === "assistant" &&
|
|
371
|
-
typeof m.content === "string" &&
|
|
372
|
-
m.content.trim().length > 0)
|
|
373
|
-
.map((m) => m.content)
|
|
374
|
-
.join("\n\n");
|
|
375
|
-
if (text.length > 0) {
|
|
376
|
-
await transcripts.append(this, { role: "assistant", text }, { userKey });
|
|
377
|
-
}
|
|
257
|
+
}
|
|
258
|
+
const context = [
|
|
259
|
+
...this.deps.context,
|
|
260
|
+
...(transcriptContext ? [transcriptContext] : []),
|
|
261
|
+
...(extra?.context ?? []),
|
|
262
|
+
];
|
|
263
|
+
// Snapshot the message count BEFORE the loop so we can isolate the
|
|
264
|
+
// assistant messages this run produced (step 4).
|
|
265
|
+
const messagesBefore = session.agent.messages.length;
|
|
266
|
+
const startedAt = Date.now();
|
|
267
|
+
let loopResult;
|
|
268
|
+
// Telemetry stage: "agent" while the run loop runs, "finalize" for the
|
|
269
|
+
// transcript-append + renderer.finish() steps below. A throw in either is
|
|
270
|
+
// reported as agent_run_failed (with the right stage) instead of being
|
|
271
|
+
// hidden behind an already-sent success event.
|
|
272
|
+
let stage = "agent";
|
|
273
|
+
try {
|
|
274
|
+
loopResult = await runAgentLoop({
|
|
275
|
+
agent: session.agent,
|
|
276
|
+
renderer,
|
|
277
|
+
tools,
|
|
278
|
+
toolDescriptors,
|
|
279
|
+
context,
|
|
280
|
+
makeToolCtx: () => ({
|
|
281
|
+
thread: this,
|
|
282
|
+
platform: this.platform,
|
|
283
|
+
}),
|
|
284
|
+
handleInterrupt: async (interrupt) => {
|
|
285
|
+
const h = this.deps.interruptHandlers.get(interrupt.eventName);
|
|
286
|
+
if (h)
|
|
287
|
+
await h({ payload: interrupt.value, thread: this });
|
|
288
|
+
},
|
|
289
|
+
initialResume,
|
|
290
|
+
});
|
|
291
|
+
stage = "finalize";
|
|
292
|
+
// Transcript auto-bridge (step 4): capture the assistant text this run
|
|
293
|
+
// produced and append it. Only when the bridge actually applied (transcripts
|
|
294
|
+
// + userKey both present and `transcript` was requested).
|
|
295
|
+
if (extra?.transcript && transcripts && userKey) {
|
|
296
|
+
const produced = session.agent.messages.slice(messagesBefore);
|
|
297
|
+
const text = produced
|
|
298
|
+
.filter((m) => m.role === "assistant" &&
|
|
299
|
+
typeof m.content === "string" &&
|
|
300
|
+
m.content.trim().length > 0)
|
|
301
|
+
.map((m) => m.content)
|
|
302
|
+
.join("\n\n");
|
|
303
|
+
if (text.length > 0) {
|
|
304
|
+
await transcripts.append(this, { role: "assistant", text }, { userKey });
|
|
378
305
|
}
|
|
379
|
-
// Turn-end hook: lets a renderer finalize any turn-scoped resource it kept
|
|
380
|
-
// open across runAgent iterations (e.g. a native streaming message). A
|
|
381
|
-
// no-op for renderers whose per-message streams already self-terminate, and
|
|
382
|
-
// for runs that were interrupted (the renderer guards that internally).
|
|
383
|
-
await renderer.finish?.();
|
|
384
|
-
}
|
|
385
|
-
catch (err) {
|
|
386
|
-
// A throw is a run failure — in the agent loop (tool-handler errors are
|
|
387
|
-
// swallowed inside the loop, so a throw is agent-level) or in finalization.
|
|
388
|
-
// `stage` distinguishes the two.
|
|
389
|
-
this.deps.telemetry?.capture("oss.channel.agent_run_failed", {
|
|
390
|
-
platform: normalizePlatform(this.platform),
|
|
391
|
-
errorClass: errorClass(err),
|
|
392
|
-
stage,
|
|
393
|
-
});
|
|
394
|
-
throw err;
|
|
395
306
|
}
|
|
396
|
-
//
|
|
397
|
-
//
|
|
398
|
-
|
|
307
|
+
// Turn-end hook: lets a renderer finalize any turn-scoped resource it kept
|
|
308
|
+
// open across runAgent iterations (e.g. a native streaming message). A
|
|
309
|
+
// no-op for renderers whose per-message streams already self-terminate, and
|
|
310
|
+
// for runs that were interrupted (the renderer guards that internally).
|
|
311
|
+
await renderer.finish?.();
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
// A throw is a run failure — in the agent loop (tool-handler errors are
|
|
315
|
+
// swallowed inside the loop, so a throw is agent-level) or in finalization.
|
|
316
|
+
// `stage` distinguishes the two.
|
|
317
|
+
this.deps.telemetry?.capture("oss.channel.agent_run_failed", {
|
|
399
318
|
platform: normalizePlatform(this.platform),
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
iterations: loopResult.iterations,
|
|
403
|
-
interrupted: loopResult.interrupted,
|
|
319
|
+
errorClass: errorClass(err),
|
|
320
|
+
stage,
|
|
404
321
|
});
|
|
405
|
-
|
|
406
|
-
}
|
|
407
|
-
finally {
|
|
408
|
-
await session.release?.();
|
|
322
|
+
throw err;
|
|
409
323
|
}
|
|
324
|
+
// Emit success ONLY after the loop AND finalization both completed, so a
|
|
325
|
+
// late transcript/finish rejection can never follow a success event.
|
|
326
|
+
this.deps.telemetry?.capture("oss.channel.agent_run", {
|
|
327
|
+
platform: normalizePlatform(this.platform),
|
|
328
|
+
durationMs: Date.now() - startedAt,
|
|
329
|
+
toolCallCount: renderer.getCapturedToolCalls().length,
|
|
330
|
+
iterations: loopResult.iterations,
|
|
331
|
+
interrupted: loopResult.interrupted,
|
|
332
|
+
});
|
|
333
|
+
return undefined;
|
|
410
334
|
}
|
|
411
335
|
}
|
|
412
336
|
let transcriptWarned = false;
|