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