@nocobase/app-plugin-agents 1.0.0-beta.26 → 1.0.0-beta.28
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/CHANGELOG.md +58 -0
- package/README.md +40 -22
- package/dist/client/chat/api.d.ts +18 -1
- package/dist/client/chat/api.d.ts.map +1 -1
- package/dist/client/chat/api.js +37 -0
- package/dist/client/chat/api.js.map +1 -1
- package/dist/client/chat/coalesced-invalidation.d.ts +15 -0
- package/dist/client/chat/coalesced-invalidation.d.ts.map +1 -0
- package/dist/client/chat/coalesced-invalidation.js +29 -0
- package/dist/client/chat/coalesced-invalidation.js.map +1 -0
- package/dist/client/chat/controller.d.ts +8 -1
- package/dist/client/chat/controller.d.ts.map +1 -1
- package/dist/client/chat/controller.js +21 -0
- package/dist/client/chat/controller.js.map +1 -1
- package/dist/client/chat/message-model.d.ts.map +1 -1
- package/dist/client/chat/message-model.js +4 -0
- package/dist/client/chat/message-model.js.map +1 -1
- package/dist/client/chat/submissions.d.ts +3 -0
- package/dist/client/chat/submissions.d.ts.map +1 -1
- package/dist/client/chat/submissions.js +1 -0
- package/dist/client/chat/submissions.js.map +1 -1
- package/dist/client/chat/use-chat-scroll.d.ts +8 -1
- package/dist/client/chat/use-chat-scroll.d.ts.map +1 -1
- package/dist/client/chat/use-chat-scroll.js +29 -5
- package/dist/client/chat/use-chat-scroll.js.map +1 -1
- package/dist/client/chat/use-chat.d.ts +2 -0
- package/dist/client/chat/use-chat.d.ts.map +1 -1
- package/dist/client/chat/use-chat.js +17 -7
- package/dist/client/chat/use-chat.js.map +1 -1
- package/dist/client/components/agent-prompt-callout.d.ts +19 -0
- package/dist/client/components/agent-prompt-callout.d.ts.map +1 -0
- package/dist/client/components/agent-prompt-callout.js +28 -0
- package/dist/client/components/agent-prompt-callout.js.map +1 -0
- package/dist/client/locales/chat.en-US.d.ts +1 -0
- package/dist/client/locales/chat.en-US.d.ts.map +1 -1
- package/dist/client/locales/chat.en-US.js +1 -0
- package/dist/client/locales/chat.en-US.js.map +1 -1
- package/dist/client/locales/chat.zh-CN.d.ts.map +1 -1
- package/dist/client/locales/chat.zh-CN.js +1 -0
- package/dist/client/locales/chat.zh-CN.js.map +1 -1
- package/dist/client/locales/de-DE.d.ts.map +1 -1
- package/dist/client/locales/de-DE.js +1 -0
- package/dist/client/locales/de-DE.js.map +1 -1
- package/dist/client/locales/es-ES.d.ts.map +1 -1
- package/dist/client/locales/es-ES.js +1 -0
- package/dist/client/locales/es-ES.js.map +1 -1
- package/dist/client/locales/fr-FR.d.ts.map +1 -1
- package/dist/client/locales/fr-FR.js +1 -0
- package/dist/client/locales/fr-FR.js.map +1 -1
- package/dist/client/locales/id-ID.d.ts.map +1 -1
- package/dist/client/locales/id-ID.js +1 -0
- package/dist/client/locales/id-ID.js.map +1 -1
- package/dist/client/locales/ja-JP.d.ts.map +1 -1
- package/dist/client/locales/ja-JP.js +1 -0
- package/dist/client/locales/ja-JP.js.map +1 -1
- package/dist/client/locales/pt-BR.d.ts.map +1 -1
- package/dist/client/locales/pt-BR.js +1 -0
- package/dist/client/locales/pt-BR.js.map +1 -1
- package/dist/client/locales/runtimes.en-US.d.ts +4 -1
- package/dist/client/locales/runtimes.en-US.d.ts.map +1 -1
- package/dist/client/locales/runtimes.en-US.js +6 -3
- package/dist/client/locales/runtimes.en-US.js.map +1 -1
- package/dist/client/locales/runtimes.zh-CN.d.ts.map +1 -1
- package/dist/client/locales/runtimes.zh-CN.js +6 -3
- package/dist/client/locales/runtimes.zh-CN.js.map +1 -1
- package/dist/client/locales/vi-VN.d.ts.map +1 -1
- package/dist/client/locales/vi-VN.js +1 -0
- package/dist/client/locales/vi-VN.js.map +1 -1
- package/dist/client/locales/zh-TW.d.ts.map +1 -1
- package/dist/client/locales/zh-TW.js +1 -0
- package/dist/client/locales/zh-TW.js.map +1 -1
- package/dist/client/pages/runtimes/connect.d.ts.map +1 -1
- package/dist/client/pages/runtimes/connect.js +16 -10
- package/dist/client/pages/runtimes/connect.js.map +1 -1
- package/dist/client/pages/skills/index.js +2 -2
- package/dist/client/pages/skills/index.js.map +1 -1
- package/dist/database/migrations/.manifest.json +8 -0
- package/dist/database/migrations/202611010001_ag_conversation_runner_attention.d.ts +4 -0
- package/dist/database/migrations/202611010001_ag_conversation_runner_attention.d.ts.map +1 -0
- package/dist/database/migrations/202611010001_ag_conversation_runner_attention.js +30 -0
- package/dist/database/migrations/202611010001_ag_conversation_runner_attention.js.map +1 -0
- package/dist/database/migrations/202611010002_ag_create_conversation_links.d.ts +4 -0
- package/dist/database/migrations/202611010002_ag_create_conversation_links.d.ts.map +1 -0
- package/dist/database/migrations/202611010002_ag_create_conversation_links.js +30 -0
- package/dist/database/migrations/202611010002_ag_create_conversation_links.js.map +1 -0
- package/dist/package.json +1 -1
- package/dist/server/composition.d.ts.map +1 -1
- package/dist/server/composition.js +1 -0
- package/dist/server/composition.js.map +1 -1
- package/dist/server/core/conversations/attachments.d.ts +5 -0
- package/dist/server/core/conversations/attachments.d.ts.map +1 -1
- package/dist/server/core/conversations/attachments.js +10 -0
- package/dist/server/core/conversations/attachments.js.map +1 -1
- package/dist/server/core/conversations/conversation.service.d.ts +3 -1
- package/dist/server/core/conversations/conversation.service.d.ts.map +1 -1
- package/dist/server/core/conversations/conversation.service.js +693 -32
- package/dist/server/core/conversations/conversation.service.js.map +1 -1
- package/dist/server/core/conversations/conversation.store.d.ts +24 -0
- package/dist/server/core/conversations/conversation.store.d.ts.map +1 -1
- package/dist/server/core/conversations/conversation.store.js +10 -0
- package/dist/server/core/conversations/conversation.store.js.map +1 -1
- package/dist/server/core/conversations/index.d.ts +1 -1
- package/dist/server/core/conversations/index.d.ts.map +1 -1
- package/dist/server/core/conversations/index.js.map +1 -1
- package/dist/server/core/conversations/links.d.ts +27 -0
- package/dist/server/core/conversations/links.d.ts.map +1 -0
- package/dist/server/core/conversations/links.js +188 -0
- package/dist/server/core/conversations/links.js.map +1 -0
- package/dist/server/core/conversations/ports.d.ts +107 -3
- package/dist/server/core/conversations/ports.d.ts.map +1 -1
- package/dist/server/core/runs/claim.d.ts.map +1 -1
- package/dist/server/core/runs/claim.js +47 -3
- package/dist/server/core/runs/claim.js.map +1 -1
- package/dist/server/core/runs/lifecycle.d.ts.map +1 -1
- package/dist/server/core/runs/lifecycle.js +28 -3
- package/dist/server/core/runs/lifecycle.js.map +1 -1
- package/dist/server/core/runs/ports.d.ts +15 -0
- package/dist/server/core/runs/ports.d.ts.map +1 -1
- package/dist/server/core/runs/ports.js.map +1 -1
- package/dist/server/core/runs/run-data-purge.d.ts +7 -0
- package/dist/server/core/runs/run-data-purge.d.ts.map +1 -1
- package/dist/server/core/runs/run-data-purge.js +13 -0
- package/dist/server/core/runs/run-data-purge.js.map +1 -1
- package/dist/server/core/runs/run.store.d.ts +6 -2
- package/dist/server/core/runs/run.store.d.ts.map +1 -1
- package/dist/server/core/runs/run.store.js +28 -10
- package/dist/server/core/runs/run.store.js.map +1 -1
- package/dist/server/kernel/errors.d.ts +1 -1
- package/dist/server/kernel/errors.d.ts.map +1 -1
- package/dist/server/kernel/errors.js +2 -0
- package/dist/server/kernel/errors.js.map +1 -1
- package/dist/server/kernel/events.d.ts +19 -0
- package/dist/server/kernel/events.d.ts.map +1 -1
- package/dist/server/kernel/events.js.map +1 -1
- package/dist/server/online/agent/contracts.d.ts +5 -0
- package/dist/server/online/agent/contracts.d.ts.map +1 -1
- package/dist/server/online/agent/native/session.d.ts +1 -0
- package/dist/server/online/agent/native/session.d.ts.map +1 -1
- package/dist/server/online/agent/native/session.js +4 -1
- package/dist/server/online/agent/native/session.js.map +1 -1
- package/dist/server/online/agent/persistence/store.d.ts.map +1 -1
- package/dist/server/online/agent/persistence/store.js +8 -1
- package/dist/server/online/agent/persistence/store.js.map +1 -1
- package/dist/server/online/agent/runner/backend.d.ts +15 -0
- package/dist/server/online/agent/runner/backend.d.ts.map +1 -1
- package/dist/server/online/agent/runner/backend.js +310 -70
- package/dist/server/online/agent/runner/backend.js.map +1 -1
- package/dist/server/online/agent/runner/projection.d.ts +24 -2
- package/dist/server/online/agent/runner/projection.d.ts.map +1 -1
- package/dist/server/online/agent/runner/projection.js +330 -26
- package/dist/server/online/agent/runner/projection.js.map +1 -1
- package/dist/server/online/agent/runner/types.d.ts +8 -1
- package/dist/server/online/agent/runner/types.d.ts.map +1 -1
- package/dist/server/online/agent/types.d.ts +2 -0
- package/dist/server/online/agent/types.d.ts.map +1 -1
- package/dist/server/providers/agents.d.ts +7 -0
- package/dist/server/providers/agents.d.ts.map +1 -1
- package/dist/server/providers/agents.js +3 -1
- package/dist/server/providers/agents.js.map +1 -1
- package/dist/server/providers/runners.d.ts.map +1 -1
- package/dist/server/providers/runners.js +12 -1
- package/dist/server/providers/runners.js.map +1 -1
- package/dist/server/routes/chat.d.ts.map +1 -1
- package/dist/server/routes/chat.js +270 -2
- package/dist/server/routes/chat.js.map +1 -1
- package/dist/server/routes/run.d.ts.map +1 -1
- package/dist/server/routes/run.js +44 -0
- package/dist/server/routes/run.js.map +1 -1
- package/dist/server/routes/schemas.d.ts +15 -1
- package/dist/server/routes/schemas.d.ts.map +1 -1
- package/dist/server/routes/schemas.js +116 -1
- package/dist/server/routes/schemas.js.map +1 -1
- package/dist/server/runners/workspaces.d.ts.map +1 -1
- package/dist/server/runners/workspaces.js +46 -2
- package/dist/server/runners/workspaces.js.map +1 -1
- package/dist/server/tokens.d.ts +1 -1
- package/dist/server/tokens.d.ts.map +1 -1
- package/dist/server/tokens.js.map +1 -1
- package/dist/shared/conversations.d.ts +107 -1
- package/dist/shared/conversations.d.ts.map +1 -1
- package/dist/shared/conversations.js +19 -0
- package/dist/shared/conversations.js.map +1 -1
- package/dist/shared/ui-message.d.ts +30 -0
- package/dist/shared/ui-message.d.ts.map +1 -1
- package/dist/shared/ui-message.js +17 -0
- package/dist/shared/ui-message.js.map +1 -1
- package/package.json +10 -10
- package/skills/nocobase-app-plugin-agents/references/extension-points.md +19 -19
- package/skills/nocobase-app-plugin-agents/references/online-agent.md +16 -2
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { runnerMessages, runnerUIPage, runnerMessageId } from './projection.js';
|
|
2
|
+
import { runnerMessages, runnerToolParts, runnerUIPage, runnerMessageId, runnerPlan, } from './projection.js';
|
|
3
3
|
import { isDeepStrictEqual } from 'node:util';
|
|
4
4
|
import { outputBlock } from '@nocobase/agent-protocol';
|
|
5
5
|
import { conversationsRepo, findConversation, lockConversation, messagesRepo, toMessage, } from '../../../core/conversations/conversation.store.js';
|
|
6
6
|
import { openRunsOnEach } from '../../../core/runs/index.js';
|
|
7
|
-
import { findRunRecord, toRun } from '../../../core/runs/run.store.js';
|
|
7
|
+
import { eventsRepo, findRunRecord, toRun, } from '../../../core/runs/run.store.js';
|
|
8
8
|
import { conflict, forbidden, invalid, notFound, } from '../../../kernel/errors.js';
|
|
9
9
|
import { asJson, jsonObject } from '../../../kernel/values.js';
|
|
10
10
|
import { unsupportedCapability } from '../errors.js';
|
|
@@ -43,6 +43,42 @@ function overrides(options) {
|
|
|
43
43
|
const { sessionId: _id, source: _source, title: _title, showInChatList: _showInChatList, selectedModel: _selectedModel, ...pending } = options;
|
|
44
44
|
return Object.keys(pending).length ? pending : undefined;
|
|
45
45
|
}
|
|
46
|
+
/** The response without its turn footer, which history shows once the run has ended; it is never streamed. */
|
|
47
|
+
function withoutTurn(message) {
|
|
48
|
+
if (!message)
|
|
49
|
+
return message;
|
|
50
|
+
const kept = message.parts.flatMap((part, index) => part.type === 'data-turn' ? [] : [index]);
|
|
51
|
+
if (kept.length === message.parts.length)
|
|
52
|
+
return message;
|
|
53
|
+
return {
|
|
54
|
+
...message,
|
|
55
|
+
parts: kept.map((index) => message.parts[index]),
|
|
56
|
+
metadata: {
|
|
57
|
+
...message.metadata,
|
|
58
|
+
sourceRecords: (message.metadata?.sourceRecords ?? []).filter((source) => !source.partId.startsWith('turn:')),
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** The text of the one user message Runner chat takes per send. */
|
|
63
|
+
function userText(messages, input) {
|
|
64
|
+
if (input?.selectedModel)
|
|
65
|
+
throw unsupportedCapability('runner', 'native models');
|
|
66
|
+
if (messages.length !== 1 || messages[0].role !== 'user')
|
|
67
|
+
throw invalid('Runner chat accepts one user message per turn.');
|
|
68
|
+
if (messages[0].providerOptions !== undefined)
|
|
69
|
+
throw unsupportedCapability('runner', 'provider-specific message options');
|
|
70
|
+
const content = messages[0].content;
|
|
71
|
+
if (Array.isArray(content) &&
|
|
72
|
+
content.some((part) => part.type !== 'text' || part.providerOptions !== undefined))
|
|
73
|
+
throw unsupportedCapability('runner', 'native binary parts; send attachments by attachmentIds');
|
|
74
|
+
if ((input?.attachmentIds?.length ?? 0) > 1)
|
|
75
|
+
throw invalid('Attachments must belong to the single user message.');
|
|
76
|
+
return typeof content === 'string'
|
|
77
|
+
? content
|
|
78
|
+
: content.map((part) => (part.type === 'text' ? part.text : '')).join('');
|
|
79
|
+
}
|
|
80
|
+
/** The most parts one streamed response carries: its text and reasoning blocks and its tool calls. */
|
|
81
|
+
const MAX_STREAM_PARTS = 256;
|
|
46
82
|
/** One trusted facade, with pending configuration and only its last accepted response pinned. */
|
|
47
83
|
export class RunnerRealtimeOnlineAgent {
|
|
48
84
|
host;
|
|
@@ -132,21 +168,43 @@ export class RunnerRealtimeOnlineAgent {
|
|
|
132
168
|
.catch(() => { });
|
|
133
169
|
return task;
|
|
134
170
|
}
|
|
171
|
+
async queue(messages, input) {
|
|
172
|
+
const { deps, state } = this.host;
|
|
173
|
+
if (state.stopping)
|
|
174
|
+
throw conflict('OnlineAgent is stopping.');
|
|
175
|
+
const text = userText(messages, input);
|
|
176
|
+
credentials(this.context);
|
|
177
|
+
if (input?.overrides || input?.agentId)
|
|
178
|
+
throw invalid('Queued messages keep the configuration of the work.');
|
|
179
|
+
// A first message still being admitted is the work this one joins.
|
|
180
|
+
while (state.admitting.has(this.sessionId))
|
|
181
|
+
await Promise.allSettled([...state.admissions]);
|
|
182
|
+
const before = await owned(deps, this.context.user.id, this.sessionId);
|
|
183
|
+
const current = runnerBinding(before.metadata)?.config;
|
|
184
|
+
if (!current)
|
|
185
|
+
throw invalid('This is not a Runner OnlineAgent conversation.');
|
|
186
|
+
if (current.agentId !== this.agentId)
|
|
187
|
+
throw conflict('This conversation now uses another agent; restore it before queueing.');
|
|
188
|
+
const accepted = await deps.conversations.sendRealtime(this.context.user.id, this.sessionId, {
|
|
189
|
+
content: text,
|
|
190
|
+
attachmentIds: input?.attachmentIds?.[0],
|
|
191
|
+
clientId: input?.clientId,
|
|
192
|
+
}, {
|
|
193
|
+
config: current,
|
|
194
|
+
expectedConfig: current,
|
|
195
|
+
workContext: input?.workContext,
|
|
196
|
+
business: this.context.business,
|
|
197
|
+
queue: true,
|
|
198
|
+
});
|
|
199
|
+
const execution = state.active.get(this.sessionId);
|
|
200
|
+
if (execution)
|
|
201
|
+
void this.host.refresh(execution).catch((error) => deps.onError(error));
|
|
202
|
+
return accepted.seq;
|
|
203
|
+
}
|
|
135
204
|
async submit(messages, input) {
|
|
136
205
|
const { deps, state } = this.host;
|
|
137
206
|
credentials(this.context);
|
|
138
|
-
|
|
139
|
-
throw unsupportedCapability('runner', 'native models');
|
|
140
|
-
if (messages.length !== 1 || messages[0].role !== 'user')
|
|
141
|
-
throw invalid('Runner chat accepts one user message per turn.');
|
|
142
|
-
if (messages[0].providerOptions !== undefined)
|
|
143
|
-
throw unsupportedCapability('runner', 'provider-specific message options');
|
|
144
|
-
const content = messages[0].content;
|
|
145
|
-
if (Array.isArray(content) &&
|
|
146
|
-
content.some((part) => part.type !== 'text' || part.providerOptions !== undefined))
|
|
147
|
-
throw unsupportedCapability('runner', 'native binary parts; send attachments by attachmentIds');
|
|
148
|
-
if ((input?.attachmentIds?.length ?? 0) > 1)
|
|
149
|
-
throw invalid('Attachments must belong to the single user message.');
|
|
207
|
+
const text = userText(messages, input);
|
|
150
208
|
const before = await owned(deps, this.context.user.id, this.sessionId);
|
|
151
209
|
if (state.active.has(this.sessionId) ||
|
|
152
210
|
runnerBinding(before.metadata)?.closingRunId ||
|
|
@@ -157,11 +215,14 @@ export class RunnerRealtimeOnlineAgent {
|
|
|
157
215
|
if (!current)
|
|
158
216
|
throw invalid('This is not a Runner OnlineAgent conversation.');
|
|
159
217
|
const requested = input?.overrides ?? this.pendingOptions;
|
|
160
|
-
const
|
|
218
|
+
const resolved = requested
|
|
161
219
|
? await deps.configurator.resolve(this.context, requested, current, this.boundary === 'http')
|
|
162
220
|
: current;
|
|
163
|
-
if (
|
|
221
|
+
if (resolved.agentId !== this.agentId)
|
|
164
222
|
throw conflict('Start a new conversation to choose another agent.');
|
|
223
|
+
const config = input?.agentId
|
|
224
|
+
? { ...resolved, agentId: input.agentId }
|
|
225
|
+
: resolved;
|
|
165
226
|
supported(config);
|
|
166
227
|
if (state.active.size >= 1024)
|
|
167
228
|
throw conflict('OnlineAgent execution limit exceeded.');
|
|
@@ -176,6 +237,8 @@ export class RunnerRealtimeOnlineAgent {
|
|
|
176
237
|
textLengths: new Map(),
|
|
177
238
|
previewDigests: new Map(),
|
|
178
239
|
previewParts: new Map(),
|
|
240
|
+
toolStates: new Map(),
|
|
241
|
+
stepSeq: 0,
|
|
179
242
|
ready: false,
|
|
180
243
|
started: false,
|
|
181
244
|
again: false,
|
|
@@ -184,11 +247,8 @@ export class RunnerRealtimeOnlineAgent {
|
|
|
184
247
|
let accepted;
|
|
185
248
|
try {
|
|
186
249
|
accepted = await deps.conversations.sendRealtime(this.context.user.id, this.sessionId, {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
: content
|
|
190
|
-
.map((part) => (part.type === 'text' ? part.text : ''))
|
|
191
|
-
.join(''),
|
|
250
|
+
agentId: input?.agentId,
|
|
251
|
+
content: text,
|
|
192
252
|
attachmentIds: input?.attachmentIds?.[0],
|
|
193
253
|
clientId: input?.clientId,
|
|
194
254
|
}, {
|
|
@@ -206,6 +266,7 @@ export class RunnerRealtimeOnlineAgent {
|
|
|
206
266
|
}
|
|
207
267
|
// Nothing after durable acceptance may turn it into a failed/retried business admission.
|
|
208
268
|
this.response = writer;
|
|
269
|
+
this.agentId = config.agentId;
|
|
209
270
|
this.pendingOptions = undefined;
|
|
210
271
|
execution.runId = accepted.run.id;
|
|
211
272
|
execution.cursor = accepted.seq;
|
|
@@ -232,6 +293,10 @@ export class RunnerOnlineAgentAdapter {
|
|
|
232
293
|
constructor(deps) {
|
|
233
294
|
this.deps = deps;
|
|
234
295
|
}
|
|
296
|
+
/** A configuration change must not race an admission or unfinished stream finalization. */
|
|
297
|
+
busy(sessionId) {
|
|
298
|
+
return (this.state.admitting.has(sessionId) || this.state.active.has(sessionId));
|
|
299
|
+
}
|
|
235
300
|
available(context) {
|
|
236
301
|
credentials(context);
|
|
237
302
|
if (this.state.stopping)
|
|
@@ -354,22 +419,9 @@ export class RunnerOnlineAgentAdapter {
|
|
|
354
419
|
if (execution.published.has(id))
|
|
355
420
|
return;
|
|
356
421
|
const type = event.type === 'thinking' ? 'reasoning' : 'text';
|
|
357
|
-
|
|
358
|
-
execution.writer.write({
|
|
359
|
-
type: 'start',
|
|
360
|
-
messageId: runnerMessageId(runId),
|
|
361
|
-
messageMetadata: {
|
|
362
|
-
conversationId: execution.id,
|
|
363
|
-
revision: 0,
|
|
364
|
-
createdAt: this.deps.now(),
|
|
365
|
-
executionId: runId,
|
|
366
|
-
turnId: runId,
|
|
367
|
-
},
|
|
368
|
-
});
|
|
369
|
-
execution.started = true;
|
|
370
|
-
}
|
|
422
|
+
this.ensureStarted(execution);
|
|
371
423
|
if (!execution.previewParts.has(id)) {
|
|
372
|
-
if (execution.previewParts.size >=
|
|
424
|
+
if (execution.previewParts.size >= MAX_STREAM_PARTS)
|
|
373
425
|
return;
|
|
374
426
|
execution.previewParts.set(id, {
|
|
375
427
|
index: execution.previewParts.size,
|
|
@@ -382,31 +434,7 @@ export class RunnerOnlineAgentAdapter {
|
|
|
382
434
|
execution.textLengths.set(id, 0);
|
|
383
435
|
execution.previewDigests.set(id, createHash('sha256'));
|
|
384
436
|
}
|
|
385
|
-
|
|
386
|
-
recordId: partId,
|
|
387
|
-
partId,
|
|
388
|
-
partIndex: part.index,
|
|
389
|
-
seq: execution.cursor + 1,
|
|
390
|
-
createdAt: this.deps.now(),
|
|
391
|
-
output: {
|
|
392
|
-
blockId: partId.slice(7),
|
|
393
|
-
phase: execution.published.has(partId)
|
|
394
|
-
? 'end'
|
|
395
|
-
: 'delta',
|
|
396
|
-
},
|
|
397
|
-
}));
|
|
398
|
-
execution.writer.write({
|
|
399
|
-
type: 'message-metadata',
|
|
400
|
-
messageMetadata: {
|
|
401
|
-
...execution.snapshot?.metadata,
|
|
402
|
-
revision: execution.snapshot?.metadata?.revision ?? 0,
|
|
403
|
-
conversationId: execution.id,
|
|
404
|
-
createdAt: execution.snapshot?.metadata?.createdAt ?? this.deps.now(),
|
|
405
|
-
sourceRecords: sources,
|
|
406
|
-
executionId: runId,
|
|
407
|
-
turnId: runId,
|
|
408
|
-
},
|
|
409
|
-
});
|
|
437
|
+
this.writeSources(execution);
|
|
410
438
|
let content = event.content ?? '';
|
|
411
439
|
if (output.phase === 'end' && event.content !== undefined) {
|
|
412
440
|
const length = this.prefixLength(execution, id, content);
|
|
@@ -441,7 +469,7 @@ export class RunnerOnlineAgentAdapter {
|
|
|
441
469
|
digest.copy().digest('hex') !==
|
|
442
470
|
createHash('sha256').update(canonical.slice(0, length)).digest('hex'))) {
|
|
443
471
|
for (const [partId, part] of execution.previewParts)
|
|
444
|
-
if (!execution.published.has(partId)) {
|
|
472
|
+
if (!execution.published.has(partId) && part.type !== 'tool') {
|
|
445
473
|
execution.writer.write({
|
|
446
474
|
type: part.type === 'text' ? 'text-end' : 'reasoning-end',
|
|
447
475
|
id: partId,
|
|
@@ -455,6 +483,208 @@ export class RunnerOnlineAgentAdapter {
|
|
|
455
483
|
}
|
|
456
484
|
return length;
|
|
457
485
|
}
|
|
486
|
+
/** The run's tool calls and results saved since the last read, as tool parts of the response, in order. */
|
|
487
|
+
async liveTools(execution) {
|
|
488
|
+
const events = await eventsRepo(this.deps.tx.read()).findMany({
|
|
489
|
+
filter: (f) => f.and([
|
|
490
|
+
f.string('runId').eq(execution.runId),
|
|
491
|
+
f.or([
|
|
492
|
+
f.string('type').eq('toolUse'),
|
|
493
|
+
f.string('type').eq('toolResult'),
|
|
494
|
+
f.and([
|
|
495
|
+
f.string('type').eq('status'),
|
|
496
|
+
f.text('content').eq('plan'),
|
|
497
|
+
]),
|
|
498
|
+
]),
|
|
499
|
+
f.number('seq').gt(execution.stepSeq),
|
|
500
|
+
]),
|
|
501
|
+
sort: (s) => s.field('seq').asc(),
|
|
502
|
+
limit: 200,
|
|
503
|
+
});
|
|
504
|
+
if (events.length === 0)
|
|
505
|
+
return;
|
|
506
|
+
execution.stepSeq = Number(events.at(-1).seq);
|
|
507
|
+
// The agent's plan, as it last reported it, replaces the one shown.
|
|
508
|
+
const plan = runnerPlan(events.filter((event) => event.type === 'status'));
|
|
509
|
+
const planId = `plan:${execution.runId}`;
|
|
510
|
+
if (plan &&
|
|
511
|
+
(execution.previewParts.has(planId) ||
|
|
512
|
+
execution.previewParts.size < MAX_STREAM_PARTS)) {
|
|
513
|
+
this.ensureStarted(execution);
|
|
514
|
+
if (!execution.previewParts.has(planId))
|
|
515
|
+
execution.previewParts.set(planId, {
|
|
516
|
+
index: execution.previewParts.size,
|
|
517
|
+
type: 'tool',
|
|
518
|
+
});
|
|
519
|
+
execution.writer.write({
|
|
520
|
+
type: 'data-plan',
|
|
521
|
+
id: planId,
|
|
522
|
+
data: plan.plan,
|
|
523
|
+
});
|
|
524
|
+
execution.published.add(planId);
|
|
525
|
+
this.writeSources(execution);
|
|
526
|
+
}
|
|
527
|
+
// A result whose call was read earlier still needs the call's input: read the calls of these results too.
|
|
528
|
+
const earlier = events
|
|
529
|
+
.filter((event) => event.type === 'toolResult')
|
|
530
|
+
.map((event) => jsonObject(event.meta).toolUseId)
|
|
531
|
+
.filter((id) => typeof id === 'string' && execution.toolStates.has(`tool:${id}`));
|
|
532
|
+
const calls = earlier.length
|
|
533
|
+
? await eventsRepo(this.deps.tx.read()).findMany({
|
|
534
|
+
filter: (f) => f.and([
|
|
535
|
+
f.string('runId').eq(execution.runId),
|
|
536
|
+
f.string('type').eq('toolUse'),
|
|
537
|
+
f.or(earlier.map((id) => f.json('meta').path(['toolUseId']).eq(id))),
|
|
538
|
+
]),
|
|
539
|
+
sort: (s) => s.field('seq').asc(),
|
|
540
|
+
limit: earlier.length,
|
|
541
|
+
})
|
|
542
|
+
: [];
|
|
543
|
+
const parts = runnerToolParts([
|
|
544
|
+
...calls.filter((call) => earlier.includes(String(jsonObject(call.meta).toolUseId))),
|
|
545
|
+
...events,
|
|
546
|
+
]);
|
|
547
|
+
let wrote = false;
|
|
548
|
+
for (const [id, { part }] of parts)
|
|
549
|
+
wrote = this.writeTool(execution, id, part) || wrote;
|
|
550
|
+
if (wrote)
|
|
551
|
+
this.writeSources(execution);
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Before the response has output, says where it stands (`data-phase`): waiting for a runner, starting the agent, or
|
|
555
|
+
* the agent thinking. The page shows it in place of a blank response; output replaces it.
|
|
556
|
+
*/
|
|
557
|
+
async livePhase(execution, run) {
|
|
558
|
+
if (execution.phase === 'done')
|
|
559
|
+
return;
|
|
560
|
+
if ([...execution.previewParts.keys()].some((id) => id !== 'phase')) {
|
|
561
|
+
execution.phase = 'done';
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
let phase = run.status === 'queued' ? 'waiting' : 'starting';
|
|
565
|
+
if (run.status === 'running' && execution.phase !== 'thinking') {
|
|
566
|
+
const started = await eventsRepo(this.deps.tx.read()).findOne({
|
|
567
|
+
filter: (f) => f.and([
|
|
568
|
+
f.string('runId').eq(run.id),
|
|
569
|
+
f.string('type').eq('status'),
|
|
570
|
+
f.text('content').eq('started'),
|
|
571
|
+
]),
|
|
572
|
+
});
|
|
573
|
+
if (started)
|
|
574
|
+
phase = 'thinking';
|
|
575
|
+
}
|
|
576
|
+
else if (execution.phase === 'thinking')
|
|
577
|
+
phase = 'thinking';
|
|
578
|
+
if (phase === execution.phase)
|
|
579
|
+
return;
|
|
580
|
+
this.ensureStarted(execution);
|
|
581
|
+
if (!execution.previewParts.has('phase'))
|
|
582
|
+
execution.previewParts.set('phase', {
|
|
583
|
+
index: execution.previewParts.size,
|
|
584
|
+
type: 'tool',
|
|
585
|
+
});
|
|
586
|
+
execution.published.add('phase');
|
|
587
|
+
execution.writer.write({
|
|
588
|
+
type: 'data-phase',
|
|
589
|
+
id: 'phase',
|
|
590
|
+
data: { phase },
|
|
591
|
+
});
|
|
592
|
+
execution.phase = phase;
|
|
593
|
+
this.writeSources(execution);
|
|
594
|
+
}
|
|
595
|
+
/** Writes what of a tool part the stream has not had; true when something was written. */
|
|
596
|
+
writeTool(execution, id, part) {
|
|
597
|
+
let state = execution.toolStates.get(id);
|
|
598
|
+
if (state === 'done')
|
|
599
|
+
return false;
|
|
600
|
+
let wrote = false;
|
|
601
|
+
if (!state) {
|
|
602
|
+
if (execution.previewParts.size >= MAX_STREAM_PARTS)
|
|
603
|
+
return false;
|
|
604
|
+
this.ensureStarted(execution);
|
|
605
|
+
execution.previewParts.set(id, {
|
|
606
|
+
index: execution.previewParts.size,
|
|
607
|
+
type: 'tool',
|
|
608
|
+
});
|
|
609
|
+
execution.writer.write({
|
|
610
|
+
type: 'tool-input-available',
|
|
611
|
+
toolCallId: part.toolCallId,
|
|
612
|
+
toolName: part.toolName,
|
|
613
|
+
input: part.input,
|
|
614
|
+
dynamic: true,
|
|
615
|
+
});
|
|
616
|
+
state = 'input';
|
|
617
|
+
execution.toolStates.set(id, state);
|
|
618
|
+
wrote = true;
|
|
619
|
+
}
|
|
620
|
+
if (part.state === 'output-available' || part.state === 'output-error') {
|
|
621
|
+
execution.writer.write(part.state === 'output-available'
|
|
622
|
+
? {
|
|
623
|
+
type: 'tool-output-available',
|
|
624
|
+
toolCallId: part.toolCallId,
|
|
625
|
+
output: part.output,
|
|
626
|
+
dynamic: true,
|
|
627
|
+
}
|
|
628
|
+
: {
|
|
629
|
+
type: 'tool-output-error',
|
|
630
|
+
toolCallId: part.toolCallId,
|
|
631
|
+
errorText: part.errorText,
|
|
632
|
+
dynamic: true,
|
|
633
|
+
});
|
|
634
|
+
execution.toolStates.set(id, 'done');
|
|
635
|
+
execution.published.add(id);
|
|
636
|
+
wrote = true;
|
|
637
|
+
}
|
|
638
|
+
return wrote;
|
|
639
|
+
}
|
|
640
|
+
ensureStarted(execution) {
|
|
641
|
+
if (execution.started)
|
|
642
|
+
return;
|
|
643
|
+
execution.writer.write({
|
|
644
|
+
type: 'start',
|
|
645
|
+
messageId: runnerMessageId(execution.runId),
|
|
646
|
+
messageMetadata: {
|
|
647
|
+
conversationId: execution.id,
|
|
648
|
+
revision: 0,
|
|
649
|
+
createdAt: this.deps.now(),
|
|
650
|
+
executionId: execution.runId,
|
|
651
|
+
turnId: execution.runId,
|
|
652
|
+
},
|
|
653
|
+
});
|
|
654
|
+
execution.started = true;
|
|
655
|
+
}
|
|
656
|
+
/** Where each part of the response streamed so far stands, so the page keeps them in order. */
|
|
657
|
+
writeSources(execution) {
|
|
658
|
+
const runId = execution.runId;
|
|
659
|
+
execution.writer.write({
|
|
660
|
+
type: 'message-metadata',
|
|
661
|
+
messageMetadata: {
|
|
662
|
+
...execution.snapshot?.metadata,
|
|
663
|
+
revision: execution.snapshot?.metadata?.revision ?? 0,
|
|
664
|
+
conversationId: execution.id,
|
|
665
|
+
createdAt: execution.snapshot?.metadata?.createdAt ?? this.deps.now(),
|
|
666
|
+
sourceRecords: [...execution.previewParts].map(([partId, part]) => ({
|
|
667
|
+
recordId: partId,
|
|
668
|
+
partId,
|
|
669
|
+
partIndex: part.index,
|
|
670
|
+
seq: execution.cursor + 1,
|
|
671
|
+
createdAt: this.deps.now(),
|
|
672
|
+
...(partId.startsWith('output:')
|
|
673
|
+
? {
|
|
674
|
+
output: {
|
|
675
|
+
blockId: partId.slice(7),
|
|
676
|
+
phase: execution.published.has(partId)
|
|
677
|
+
? 'end'
|
|
678
|
+
: 'delta',
|
|
679
|
+
},
|
|
680
|
+
}
|
|
681
|
+
: {}),
|
|
682
|
+
})),
|
|
683
|
+
executionId: runId,
|
|
684
|
+
turnId: runId,
|
|
685
|
+
},
|
|
686
|
+
});
|
|
687
|
+
}
|
|
458
688
|
refresh(execution) {
|
|
459
689
|
if (this.state.detaching)
|
|
460
690
|
return execution.pending ?? Promise.resolve();
|
|
@@ -494,7 +724,9 @@ export class RunnerOnlineAgentAdapter {
|
|
|
494
724
|
// A reclaimed attempt can be queued again without finishing the conversation stream.
|
|
495
725
|
if (run.status === 'queued' || run.status === 'dispatched')
|
|
496
726
|
for (const [id, part] of execution.previewParts)
|
|
497
|
-
if (id.startsWith('output:') &&
|
|
727
|
+
if (id.startsWith('output:') &&
|
|
728
|
+
part.type !== 'tool' &&
|
|
729
|
+
!execution.published.has(id)) {
|
|
498
730
|
execution.writer.write({
|
|
499
731
|
type: part.type === 'text' ? 'text-end' : 'reasoning-end',
|
|
500
732
|
id,
|
|
@@ -502,6 +734,8 @@ export class RunnerOnlineAgentAdapter {
|
|
|
502
734
|
execution.published.add(id);
|
|
503
735
|
execution.previewDigests.delete(id);
|
|
504
736
|
}
|
|
737
|
+
await this.liveTools(execution);
|
|
738
|
+
await this.livePhase(execution, run);
|
|
505
739
|
// Synchronization commits first. Independent notices are delivered by durable business rereads.
|
|
506
740
|
const row = await owned(this.deps, execution.userId, execution.id);
|
|
507
741
|
if (Number(row.lastSeq) > execution.cursor) {
|
|
@@ -513,7 +747,7 @@ export class RunnerOnlineAgentAdapter {
|
|
|
513
747
|
},
|
|
514
748
|
sort: (s) => s.field('seq').asc(),
|
|
515
749
|
});
|
|
516
|
-
const message = (await runnerMessages(this.deps.tx, records.map((record) => toMessage(record, this.deps.basePath()))))[0];
|
|
750
|
+
const message = withoutTurn((await runnerMessages(this.deps.tx, records.map((record) => toMessage(record, this.deps.basePath()))))[0]);
|
|
517
751
|
if (message) {
|
|
518
752
|
const sourceParts = new Map(message.metadata.sourceRecords.map((source) => [
|
|
519
753
|
source.partId,
|
|
@@ -523,11 +757,14 @@ export class RunnerOnlineAgentAdapter {
|
|
|
523
757
|
for (const source of message.metadata.sourceRecords) {
|
|
524
758
|
let part = execution.previewParts.get(source.partId);
|
|
525
759
|
if (!part) {
|
|
760
|
+
const type = sourceParts.get(source.partId)?.type;
|
|
526
761
|
part = {
|
|
527
762
|
index: execution.previewParts.size,
|
|
528
|
-
type:
|
|
763
|
+
type: type === 'reasoning'
|
|
529
764
|
? 'reasoning'
|
|
530
|
-
: '
|
|
765
|
+
: type === 'dynamic-tool' || type?.startsWith('data-')
|
|
766
|
+
? 'tool'
|
|
767
|
+
: 'text',
|
|
531
768
|
};
|
|
532
769
|
execution.previewParts.set(source.partId, part);
|
|
533
770
|
}
|
|
@@ -595,6 +832,10 @@ export class RunnerOnlineAgentAdapter {
|
|
|
595
832
|
execution.previewDigests.delete(source.partId);
|
|
596
833
|
}
|
|
597
834
|
}
|
|
835
|
+
else if (part.type === 'dynamic-tool') {
|
|
836
|
+
this.writeTool(execution, source.partId, part);
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
598
839
|
else if (part.type === 'file')
|
|
599
840
|
execution.writer.write({
|
|
600
841
|
type: 'file',
|
|
@@ -612,7 +853,7 @@ export class RunnerOnlineAgentAdapter {
|
|
|
612
853
|
if (!this.terminal(run))
|
|
613
854
|
return;
|
|
614
855
|
for (const [id, part] of execution.previewParts)
|
|
615
|
-
if (!execution.published.has(id)) {
|
|
856
|
+
if (!execution.published.has(id) && part.type !== 'tool') {
|
|
616
857
|
execution.writer.write({
|
|
617
858
|
type: part.type === 'text' ? 'text-end' : 'reasoning-end',
|
|
618
859
|
id,
|
|
@@ -658,9 +899,8 @@ export class RunnerOnlineAgentAdapter {
|
|
|
658
899
|
if (!current || current.userId !== userId)
|
|
659
900
|
throw notFound('Conversation');
|
|
660
901
|
const binding = runnerBinding(current.metadata);
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
if (!binding?.closingRunId)
|
|
902
|
+
// A later run (one the application woke) closes on its own.
|
|
903
|
+
if (!binding?.closingRunId || binding.closingRunId !== runId)
|
|
664
904
|
return;
|
|
665
905
|
const { closingRunId: _closed, ...kept } = binding;
|
|
666
906
|
await conversationsRepo(conn).updateMany({
|