@ixo/oracle-runtime-workers 0.4.1 → 0.10.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 +5 -1
- package/package.json +2 -2
- package/src/attachments/download.ts +43 -22
- package/src/attachments/index.ts +1 -0
- package/src/core/env.ts +28 -0
- package/src/core/main-agent.ts +56 -4
- package/src/core/middlewares/dangling-tool-calls.ts +99 -0
- package/src/core/middlewares/index.ts +5 -0
- package/src/core/runtime-context.ts +14 -5
- package/src/core/subagent-as-tool.ts +12 -1
- package/src/do/ambient.ts +13 -4
- package/src/do/boot-dirty.ts +38 -0
- package/src/do/contracts.ts +113 -18
- package/src/do/matrix-turn-ledger.ts +126 -0
- package/src/do/reauth-prompt.ts +76 -0
- package/src/do/room-mirror.ts +111 -0
- package/src/do/sse-stream.ts +104 -2
- package/src/do/transcript.ts +15 -2
- package/src/do/turn-config.ts +24 -0
- package/src/do/user-oracle-do.ts +484 -91
- package/src/matrix/gateway-do.ts +233 -42
- package/src/matrix/inbox-store.ts +188 -0
- package/src/matrix/txn-id.ts +35 -0
- package/src/matrix/work-status.ts +15 -3
- package/src/owner-store/ixo-vfs-store.ts +12 -5
- package/src/owner-store/matrix-media-store.ts +65 -40
- package/src/owner-store/measure.ts +39 -0
- package/src/owner-store/migrating-store.ts +29 -34
- package/src/owner-store/types.ts +0 -0
- package/src/plugin-api/types.ts +25 -1
- package/src/plugins/portal/action-log.ts +36 -9
- package/src/plugins/tasks/tasks-tools.ts +7 -7
- package/src/shell/app.ts +92 -7
- package/src/shell/turn-body-cap.ts +19 -0
- package/src/sqlite/cache-config.ts +27 -6
- package/src/sqlite/chunk-layout.ts +14 -0
- package/src/sqlite/database.ts +188 -19
- package/src/sqlite/do-vfs.ts +963 -99
- package/src/sqlite/index.ts +13 -0
- package/src/sqlite/page-tier.ts +668 -0
- package/src/tasks/schedule-input.ts +102 -0
- package/src/tasks/scheduler.ts +351 -54
- package/src/tasks/store.ts +198 -3
package/src/do/sse-stream.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
redactOperatorFault,
|
|
16
16
|
} from '../llm/provider-error';
|
|
17
17
|
import type { AIMessageChunk, ToolMessage } from '@langchain/core/messages';
|
|
18
|
+
import { isToolMessage } from '@langchain/core/messages';
|
|
18
19
|
|
|
19
20
|
export interface SseTurnRunnerInput {
|
|
20
21
|
/** `agent.streamEvents(stateInput, config)` async iterable. */
|
|
@@ -75,9 +76,30 @@ interface ToolCallPayload {
|
|
|
75
76
|
requestId: string;
|
|
76
77
|
toolName: string;
|
|
77
78
|
args: Record<string, unknown>;
|
|
78
|
-
status: 'isRunning' | 'done';
|
|
79
|
+
status: 'isRunning' | 'done' | 'error';
|
|
79
80
|
output?: string;
|
|
80
81
|
eventId?: string;
|
|
82
|
+
/** Set with `status: 'error'`: what the tool reported (schema rejection, retries exhausted). */
|
|
83
|
+
error?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Model calls the agent makes for itself — the summarization middleware
|
|
88
|
+
* condensing the thread, a sub-agent's inner turn — stream through the same
|
|
89
|
+
* `streamEvents` pipe as the user-facing reply. Only the outermost agent's
|
|
90
|
+
* model output belongs on the wire; everything else is bookkeeping the user
|
|
91
|
+
* never asked to read (the "Here is a summary of the conversation to date"
|
|
92
|
+
* leak). The summarizer tags its invoke with `lc_source: 'summarization'`;
|
|
93
|
+
* sub-agents run under their own run name below the main agent.
|
|
94
|
+
*/
|
|
95
|
+
export function isInternalModelEvent(evt: {
|
|
96
|
+
metadata?: Record<string, unknown>;
|
|
97
|
+
tags?: string[];
|
|
98
|
+
}): boolean {
|
|
99
|
+
const source = evt.metadata?.['lc_source'];
|
|
100
|
+
if (source === 'summarization') return true;
|
|
101
|
+
const tags = evt.tags ?? [];
|
|
102
|
+
return tags.some((t) => t === 'summarization' || t === 'internal');
|
|
81
103
|
}
|
|
82
104
|
|
|
83
105
|
interface ActionCallPayload {
|
|
@@ -230,6 +252,32 @@ export function createSseTurnStream(
|
|
|
230
252
|
toolCallMap.clear();
|
|
231
253
|
};
|
|
232
254
|
|
|
255
|
+
// Tool calls the main agent's model produced, by call id, until their
|
|
256
|
+
// tool run starts. A call whose arguments fail the tool's schema never
|
|
257
|
+
// starts (LangChain validates before the tool's start callback), so
|
|
258
|
+
// `on_tool_start` / `on_tool_end` never fire for it; the error tool
|
|
259
|
+
// message the tools node returns is the only trace. Anything still
|
|
260
|
+
// recorded when that node ends is reported from that message.
|
|
261
|
+
const pendingCalls = new Map<
|
|
262
|
+
string,
|
|
263
|
+
{ toolName: string; args: Record<string, unknown> }
|
|
264
|
+
>();
|
|
265
|
+
const sameArgs = (a: unknown, b: unknown) => {
|
|
266
|
+
try {
|
|
267
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
268
|
+
} catch {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
const forgetStartedCall = (toolName: string, args: unknown) => {
|
|
273
|
+
for (const [id, call] of pendingCalls) {
|
|
274
|
+
if (call.toolName === toolName && sameArgs(call.args, args)) {
|
|
275
|
+
pendingCalls.delete(id);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
|
|
233
281
|
const run = async () => {
|
|
234
282
|
let fullContent = '';
|
|
235
283
|
try {
|
|
@@ -240,12 +288,59 @@ export function createSseTurnStream(
|
|
|
240
288
|
run_id: string;
|
|
241
289
|
name?: string;
|
|
242
290
|
data: unknown;
|
|
291
|
+
metadata?: Record<string, unknown>;
|
|
292
|
+
tags?: string[];
|
|
243
293
|
};
|
|
294
|
+
if (evt.event === 'on_chat_model_end') {
|
|
295
|
+
if (isInternalModelEvent(evt)) continue;
|
|
296
|
+
const output = (evt.data as { output?: AIMessageChunk })?.output;
|
|
297
|
+
for (const call of output?.tool_calls ?? []) {
|
|
298
|
+
if (call.id)
|
|
299
|
+
pendingCalls.set(call.id, {
|
|
300
|
+
toolName: call.name,
|
|
301
|
+
args: (call.args ?? {}) as Record<string, unknown>,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (evt.event === 'on_chain_end' && evt.name === 'tools') {
|
|
307
|
+
if (pendingCalls.size === 0) continue;
|
|
308
|
+
const output = (evt.data as { output?: unknown })?.output;
|
|
309
|
+
const messages: unknown[] = Array.isArray(output)
|
|
310
|
+
? output
|
|
311
|
+
: Array.isArray((output as { messages?: unknown[] })?.messages)
|
|
312
|
+
? ((output as { messages: unknown[] }).messages ?? [])
|
|
313
|
+
: [];
|
|
314
|
+
for (const message of messages) {
|
|
315
|
+
if (!isToolMessage(message)) continue;
|
|
316
|
+
const call = pendingCalls.get(message.tool_call_id);
|
|
317
|
+
if (!call) continue;
|
|
318
|
+
pendingCalls.delete(message.tool_call_id);
|
|
319
|
+
const text = toText(message.content);
|
|
320
|
+
const failed = message.status === 'error';
|
|
321
|
+
write('tool_call', {
|
|
322
|
+
sessionId,
|
|
323
|
+
requestId,
|
|
324
|
+
toolName: message.name ?? call.toolName,
|
|
325
|
+
args: {
|
|
326
|
+
...call.args,
|
|
327
|
+
toolName: message.name ?? call.toolName,
|
|
328
|
+
},
|
|
329
|
+
status: failed ? 'error' : 'done',
|
|
330
|
+
output: text,
|
|
331
|
+
...(failed && { error: text || 'Tool call rejected' }),
|
|
332
|
+
eventId: message.tool_call_id,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
pendingCalls.clear();
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
244
338
|
if (evt.event === 'on_tool_start') {
|
|
245
339
|
const toolName = evt.name ?? 'tool';
|
|
246
340
|
const args = extractToolArgs(
|
|
247
341
|
(evt.data as { input?: unknown })?.input,
|
|
248
342
|
);
|
|
343
|
+
forgetStartedCall(toolName, args);
|
|
249
344
|
if (input.agActionNames?.has(toolName)) {
|
|
250
345
|
const payload: ActionCallPayload = {
|
|
251
346
|
requestId,
|
|
@@ -295,10 +390,16 @@ export function createSseTurnStream(
|
|
|
295
390
|
}
|
|
296
391
|
const tool = toolCallMap.get(evt.run_id);
|
|
297
392
|
if (tool) {
|
|
393
|
+
// A tool whose call was rejected (schema mismatch) or whose
|
|
394
|
+
// retries ran out comes back as a ToolMessage with
|
|
395
|
+
// `status: 'error'`, not as `on_tool_error`; the client must
|
|
396
|
+
// see it as failed, not as a finished call with odd output.
|
|
397
|
+
const failed = output?.status === 'error';
|
|
298
398
|
write('tool_call', {
|
|
299
399
|
...tool,
|
|
300
|
-
status: 'done',
|
|
400
|
+
status: failed ? 'error' : 'done',
|
|
301
401
|
output: text,
|
|
402
|
+
...(failed && { error: text || 'Tool failed' }),
|
|
302
403
|
eventId: evt.run_id,
|
|
303
404
|
args: {
|
|
304
405
|
...tool.args,
|
|
@@ -342,6 +443,7 @@ export function createSseTurnStream(
|
|
|
342
443
|
continue;
|
|
343
444
|
}
|
|
344
445
|
if (evt.event === 'on_chat_model_stream') {
|
|
446
|
+
if (isInternalModelEvent(evt)) continue;
|
|
345
447
|
const chunk = (evt.data as { chunk?: AIMessageChunk })?.chunk;
|
|
346
448
|
if (!chunk) continue;
|
|
347
449
|
const raw = chunk.additional_kwargs?.__raw_response as
|
package/src/do/transcript.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
isAttachmentPlaceholderText,
|
|
13
13
|
isAttachmentViewMessage,
|
|
14
14
|
} from '../attachments/retention';
|
|
15
|
+
import { SUMMARY_PREFIX } from '../core/middlewares/summarization';
|
|
15
16
|
|
|
16
17
|
export interface ToolCallDto {
|
|
17
18
|
name: string;
|
|
@@ -103,11 +104,23 @@ export function contentToText(content: BaseMessage['content']): string {
|
|
|
103
104
|
return String(content);
|
|
104
105
|
}
|
|
105
106
|
|
|
106
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* The summarization middleware's bookkeeping message — never shown to users.
|
|
109
|
+
* LangChain 1.4 writes it as a HUMAN message tagged
|
|
110
|
+
* `additional_kwargs.lc_source: 'summarization'` whose text starts with the
|
|
111
|
+
* summary prefix; older versions used a system message and other tags. All
|
|
112
|
+
* of them match here, so a condensed thread lists as the kept tail only.
|
|
113
|
+
*/
|
|
107
114
|
export function isSummarizationMessage(message: BaseMessage): boolean {
|
|
108
115
|
const kw = message.additional_kwargs as Record<string, unknown> | undefined;
|
|
109
|
-
if (
|
|
116
|
+
if (
|
|
117
|
+
kw?.lc_source === 'summarization' ||
|
|
118
|
+
kw?.lc_summary_message === true ||
|
|
119
|
+
kw?.summary === true
|
|
120
|
+
)
|
|
121
|
+
return true;
|
|
110
122
|
const text = contentToText(message.content);
|
|
123
|
+
if (text.startsWith(SUMMARY_PREFIX)) return true;
|
|
111
124
|
return message.type === 'system' && /summary of the conversation/i.test(text);
|
|
112
125
|
}
|
|
113
126
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { baseEnvSchema, TURN_RECURSION_LIMIT_DEFAULT } from '../core/env';
|
|
2
|
+
import type { OracleWorkerEnv } from './contracts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The LangGraph `recursionLimit` every turn runs with (HTTP/SSE turns, room
|
|
6
|
+
* turns and the task runner's background turns all build their config in
|
|
7
|
+
* one place). `TURN_RECURSION_LIMIT` is validated by the base env schema at
|
|
8
|
+
* boot; this re-parses the raw Worker var through the same field so the
|
|
9
|
+
* turn never sees anything but a positive integer, and falls back to the
|
|
10
|
+
* default (with a warning) if the var is somehow malformed.
|
|
11
|
+
*/
|
|
12
|
+
export function turnRecursionLimit(
|
|
13
|
+
env: Pick<OracleWorkerEnv, 'TURN_RECURSION_LIMIT'>,
|
|
14
|
+
log: Pick<Console, 'warn'> = console,
|
|
15
|
+
): number {
|
|
16
|
+
const parsed = baseEnvSchema.shape.TURN_RECURSION_LIMIT.safeParse(
|
|
17
|
+
env.TURN_RECURSION_LIMIT,
|
|
18
|
+
);
|
|
19
|
+
if (parsed.success) return parsed.data;
|
|
20
|
+
log.warn(
|
|
21
|
+
`[turn] TURN_RECURSION_LIMIT=${JSON.stringify(env.TURN_RECURSION_LIMIT)} is not a positive integer; using ${TURN_RECURSION_LIMIT_DEFAULT}`,
|
|
22
|
+
);
|
|
23
|
+
return TURN_RECURSION_LIMIT_DEFAULT;
|
|
24
|
+
}
|