@ixo/oracle-runtime-workers 0.21.2 → 0.23.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 +16 -13
- package/package.json +10 -4
- package/src/action-proposals/policy.ts +54 -0
- package/src/action-proposals/reconcile.ts +87 -0
- package/src/action-proposals/store.ts +863 -0
- package/src/action-proposals/topic-budget.ts +84 -0
- package/src/action-proposals/topic-chain.ts +272 -0
- package/src/action-proposals/topic-effects.ts +44 -0
- package/src/action-proposals/topic-errors.ts +79 -0
- package/src/action-proposals/topic-history.ts +267 -0
- package/src/action-proposals/topic-matrix.ts +292 -0
- package/src/action-proposals/topic-prepare.ts +299 -0
- package/src/action-proposals/topic-store.ts +999 -0
- package/src/action-proposals/topic-verify.ts +279 -0
- package/src/action-proposals/verify.ts +223 -0
- package/src/channels/auth.ts +6 -1
- package/src/channels/contract.ts +4 -0
- package/src/channels/turns.ts +1 -0
- package/src/core/env.ts +107 -5
- package/src/core/index.ts +8 -2
- package/src/core/main-agent-types.ts +16 -0
- package/src/core/main-agent.ts +26 -0
- package/src/core/middlewares/tool-execution.ts +87 -46
- package/src/core/middlewares/tool-marks.ts +54 -19
- package/src/core/runtime-context.ts +27 -1
- package/src/core/state.ts +2 -0
- package/src/core/tool-scheduler.ts +129 -24
- package/src/delivery/plan.ts +43 -2
- package/src/delivery/schema.ts +25 -1
- package/src/delivery/types.ts +13 -2
- package/src/do/active-runs.ts +171 -0
- package/src/do/ambient.ts +6 -0
- package/src/do/contracts.ts +86 -3
- package/src/do/run-coordinator.ts +30 -0
- package/src/do/run-request.ts +15 -2
- package/src/do/run-store-test-do.ts +10 -0
- package/src/do/run-store.ts +66 -13
- package/src/do/turn-body.ts +13 -1
- package/src/do/user-oracle-do.ts +1420 -17
- package/src/hub-actions/client.ts +163 -0
- package/src/hub-actions/request.ts +110 -0
- package/src/hub-actions/store.ts +485 -0
- package/src/matrix/gateway-do.ts +176 -0
- package/src/matrix/reply-parts.ts +8 -2
- package/src/plugin-api/tool-helper.ts +6 -0
- package/src/plugin-api/types.ts +117 -7
- package/src/plugins/action-proposals/action-proposals.plugin.ts +486 -0
- package/src/plugins/action-proposals/evidence.ts +67 -0
- package/src/plugins/agui/agui.plugin.ts +7 -5
- package/src/plugins/hub-actions/hub-actions.plugin.ts +244 -0
- package/src/plugins/index.ts +6 -0
- package/src/plugins/ixo-transaction/wallet-signing.ts +5 -2
- package/src/plugins/portal/declared-tools.ts +12 -0
- package/src/plugins/portal/portal.plugin.ts +22 -8
- package/src/realtime/frontend-call-registry.ts +147 -27
- package/src/realtime/realtime-endpoint.ts +335 -41
- package/src/realtime/session-socket-hub.ts +90 -19
- package/src/shell/app.ts +218 -2
- package/src/sqlite/sessions-store.ts +22 -0
|
@@ -42,6 +42,14 @@ export const mainAgentRequestContextSchema = z.object({
|
|
|
42
42
|
id: z.string(),
|
|
43
43
|
client: z.enum(['portal', 'matrix', 'slack', 'channel']),
|
|
44
44
|
requestId: z.string(),
|
|
45
|
+
runId: z.string().optional(),
|
|
46
|
+
channel: z
|
|
47
|
+
.object({
|
|
48
|
+
provider: z.string(),
|
|
49
|
+
bindingId: z.string(),
|
|
50
|
+
bindingRevision: z.number().int().positive(),
|
|
51
|
+
})
|
|
52
|
+
.optional(),
|
|
45
53
|
wsId: z.string().optional(),
|
|
46
54
|
roomId: z.string().optional(),
|
|
47
55
|
}),
|
|
@@ -218,6 +226,14 @@ export interface MainAgentBuildResult {
|
|
|
218
226
|
* turn, `write` never does). Declared by the plugin, else by name.
|
|
219
227
|
*/
|
|
220
228
|
toolEffects: Map<string, 'read' | 'write'>;
|
|
229
|
+
/**
|
|
230
|
+
* The plugin tools the turn can run that act in the user's browser
|
|
231
|
+
* (`PluginTool.surface`), a sub-agent's inner tools included: scheduled
|
|
232
|
+
* in their session's `frontend` lane. A tool absent from the map (meta,
|
|
233
|
+
* domain and dispatch tools, every server-side tool, and a name two tools
|
|
234
|
+
* disagree on) is scheduled by its effect.
|
|
235
|
+
*/
|
|
236
|
+
toolSurfaces: Map<string, 'frontend'>;
|
|
221
237
|
/** Names of the sub-agent dispatch tools (`call_*_agent`): scheduled in their own lane. */
|
|
222
238
|
subAgentToolNames: ReadonlySet<string>;
|
|
223
239
|
/**
|
package/src/core/main-agent.ts
CHANGED
|
@@ -235,6 +235,7 @@ export async function createMainAgent(
|
|
|
235
235
|
systemPrompt,
|
|
236
236
|
boundToolNames: [],
|
|
237
237
|
toolEffects: new Map(),
|
|
238
|
+
toolSurfaces: new Map(),
|
|
238
239
|
subAgentToolNames: new Set(),
|
|
239
240
|
context: { user: requestCtx.user, session: requestCtx.session },
|
|
240
241
|
};
|
|
@@ -299,6 +300,8 @@ export async function createMainAgent(
|
|
|
299
300
|
id: requestCtx.session.id,
|
|
300
301
|
client: requestCtx.session.client,
|
|
301
302
|
requestId: requestCtx.session.requestId,
|
|
303
|
+
runId: requestCtx.session.runId,
|
|
304
|
+
channel: requestCtx.session.channel,
|
|
302
305
|
wsId: requestCtx.session.wsId,
|
|
303
306
|
roomId: requestCtx.session.roomId,
|
|
304
307
|
...(surface ? { surface } : {}),
|
|
@@ -556,6 +559,28 @@ export async function createMainAgent(
|
|
|
556
559
|
for (const { tool } of allTools)
|
|
557
560
|
toolEffects.set(tool.name, toolEffectOf(tool));
|
|
558
561
|
for (const t of subAgentTools) toolEffects.set(t.name, 'write');
|
|
562
|
+
// The plugin tools that act in the user's browser (`PluginTool.surface`),
|
|
563
|
+
// a sub-agent's inner tools included (the execution middleware schedules
|
|
564
|
+
// them too, by name): scheduled in their session's frontend lane. A name
|
|
565
|
+
// is listed only when every tool of that name declares it; meta, domain
|
|
566
|
+
// and dispatch tools never are.
|
|
567
|
+
const surfaceOf = new Map<string, PluginTool['surface']>();
|
|
568
|
+
const declareSurface = (tool: PluginTool): void => {
|
|
569
|
+
const known = surfaceOf.get(tool.name);
|
|
570
|
+
surfaceOf.set(
|
|
571
|
+
tool.name,
|
|
572
|
+
surfaceOf.has(tool.name) && known !== tool.surface
|
|
573
|
+
? undefined
|
|
574
|
+
: tool.surface,
|
|
575
|
+
);
|
|
576
|
+
};
|
|
577
|
+
for (const { tool } of [...turnTools, ...allTools]) declareSurface(tool);
|
|
578
|
+
for (const { subAgent } of subAgentEntries)
|
|
579
|
+
for (const tool of Array.isArray(subAgent.tools) ? subAgent.tools : [])
|
|
580
|
+
declareSurface(tool);
|
|
581
|
+
const toolSurfaces = new Map<string, 'frontend'>();
|
|
582
|
+
for (const [name, surface] of surfaceOf)
|
|
583
|
+
if (surface === 'frontend') toolSurfaces.set(name, surface);
|
|
559
584
|
// Tools whose identical call is a new action (a browser step), which the
|
|
560
585
|
// repetition guard caps like a read whatever their effect.
|
|
561
586
|
const repeatableToolNames = new Set(
|
|
@@ -909,6 +934,7 @@ export async function createMainAgent(
|
|
|
909
934
|
systemPrompt,
|
|
910
935
|
boundToolNames: tools.map((t) => t.name),
|
|
911
936
|
toolEffects,
|
|
937
|
+
toolSurfaces,
|
|
912
938
|
subAgentToolNames: new Set(subAgentTools.map((t) => t.name)),
|
|
913
939
|
context: runConfig.context,
|
|
914
940
|
...(domain
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
* run: charged to the turn's budget, scheduled in its lane, and — for a
|
|
4
4
|
* write — claimed in the run ledger for as long as its outcome is unknown.
|
|
5
5
|
*
|
|
6
|
-
* The claim is a fingerprint of the tool name and its canonical arguments
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* The claim is a fingerprint of the tool name and its canonical arguments
|
|
7
|
+
* (and, for a frontend write, its session), recorded before the write
|
|
8
|
+
* starts; a write is claimed by its effect, whichever lane it runs in. A
|
|
9
|
+
* returned result (success or a reported failure) releases it, and so does
|
|
10
|
+
* a frontend call that was never sent; a failure that says nothing about whether
|
|
9
11
|
* the write happened — abort, deadline, a dropped connection, a 5xx —
|
|
10
12
|
* leaves it in place, whether the tool threw it or caught it and returned
|
|
11
13
|
* it as an error result (many plugin tools and adapters never throw to the
|
|
@@ -30,6 +32,7 @@ import { type AgentMiddleware, createMiddleware } from 'langchain';
|
|
|
30
32
|
import type { Logger } from '../../plugin-api/types';
|
|
31
33
|
import type { ToolLane, ToolScheduler } from '../tool-scheduler';
|
|
32
34
|
import { findProviderStall } from '../../llm/stream-liveness';
|
|
35
|
+
import { isFrontendCallNotSent } from '../../realtime/frontend-call-registry';
|
|
33
36
|
import { isHarnessLimitError, type TurnBudget } from '../turn-budget';
|
|
34
37
|
import { NOOP_LOGGER } from '../utils';
|
|
35
38
|
|
|
@@ -51,8 +54,18 @@ export interface WriteClaimStore {
|
|
|
51
54
|
export interface ToolExecutionOptions {
|
|
52
55
|
budget: TurnBudget;
|
|
53
56
|
scheduler: ToolScheduler;
|
|
54
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Lane of a tool by name: `read` / `write` (user-wide, by effect),
|
|
59
|
+
* `frontend` for a tool acting in the user's browser
|
|
60
|
+
* (`PluginTool.surface`), `subagent` for a dispatch. The two per-session
|
|
61
|
+
* lanes are keyed by `sessionId` below.
|
|
62
|
+
*/
|
|
55
63
|
laneOf: (toolName: string) => ToolLane;
|
|
64
|
+
/**
|
|
65
|
+
* Effect of a tool by name. A `write` is claimed in the ledger whatever
|
|
66
|
+
* its lane (a frontend write too); a sub-agent dispatch never is.
|
|
67
|
+
*/
|
|
68
|
+
effectOf: (toolName: string) => 'read' | 'write';
|
|
56
69
|
runId: string;
|
|
57
70
|
sessionId: string;
|
|
58
71
|
/** Omitted → writes are scheduled and charged but not claimed (stateless builds, tests). */
|
|
@@ -77,12 +90,21 @@ export function canonicalArguments(value: unknown): string {
|
|
|
77
90
|
return JSON.stringify(value) ?? 'null';
|
|
78
91
|
}
|
|
79
92
|
|
|
80
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* SHA-256 of `name:args`, hex — the write's fingerprint in the ledger. With
|
|
95
|
+
* `sessionId` (a frontend write: it acts in that session's own browser, so
|
|
96
|
+
* the same call in another chat is a different action) the session is part
|
|
97
|
+
* of it: `name@<sessionId>:args`.
|
|
98
|
+
*/
|
|
81
99
|
export async function operationKey(
|
|
82
100
|
name: string,
|
|
83
101
|
args: unknown,
|
|
102
|
+
sessionId?: string,
|
|
84
103
|
): Promise<string> {
|
|
85
|
-
const
|
|
104
|
+
const scope = sessionId === undefined ? '' : `@${sessionId}`;
|
|
105
|
+
const bytes = new TextEncoder().encode(
|
|
106
|
+
`${name}${scope}:${canonicalArguments(args)}`,
|
|
107
|
+
);
|
|
86
108
|
const digest = await crypto.subtle.digest('SHA-256', bytes);
|
|
87
109
|
return Array.from(new Uint8Array(digest), (b) =>
|
|
88
110
|
b.toString(16).padStart(2, '0'),
|
|
@@ -141,12 +163,16 @@ const isServerStatus = (status: unknown): boolean =>
|
|
|
141
163
|
/**
|
|
142
164
|
* `true` when a thrown error says nothing about whether the tool's side
|
|
143
165
|
* effect happened: the turn was aborted or hit its deadline, the transport
|
|
144
|
-
* failed, or the service answered with a server error.
|
|
166
|
+
* failed, or the service answered with a server error. A frontend call that
|
|
167
|
+
* was never sent (`FrontendCallNotSentError`: parked until the turn aborted
|
|
168
|
+
* or its park deadline passed) is a known outcome even on an aborted turn:
|
|
169
|
+
* no browser saw it.
|
|
145
170
|
*/
|
|
146
171
|
export function isUncertainOutcome(
|
|
147
172
|
error: unknown,
|
|
148
173
|
signal?: AbortSignal,
|
|
149
174
|
): boolean {
|
|
175
|
+
if (isFrontendCallNotSent(error)) return false;
|
|
150
176
|
if (signal?.aborted) return true;
|
|
151
177
|
if (isHarnessLimitError(error)) return true;
|
|
152
178
|
// A tool whose own model call stalled (stream-liveness.ts) stopped at an
|
|
@@ -267,6 +293,10 @@ export function createToolExecutionMiddleware(
|
|
|
267
293
|
const { toolCall } = request;
|
|
268
294
|
const toolName = toolCall.name;
|
|
269
295
|
const lane = options.laneOf(toolName);
|
|
296
|
+
// Claimed by effect, whatever the lane: a frontend write too. A
|
|
297
|
+
// dispatch is never fingerprinted (its inner writes are, by name).
|
|
298
|
+
const claimed =
|
|
299
|
+
lane !== 'subagent' && options.effectOf(toolName) === 'write';
|
|
270
300
|
const callId = toolCall.id ?? '';
|
|
271
301
|
const attempt = (attempts.get(callId) ?? 0) + 1;
|
|
272
302
|
attempts.set(callId, attempt);
|
|
@@ -290,51 +320,62 @@ export function createToolExecutionMiddleware(
|
|
|
290
320
|
);
|
|
291
321
|
return output;
|
|
292
322
|
};
|
|
293
|
-
return scheduler.run(
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
`[tool-execution] ${toolName}: an identical write since ${claim.since} has no known outcome; not run again`,
|
|
323
|
+
return scheduler.run(
|
|
324
|
+
lane,
|
|
325
|
+
options.signal,
|
|
326
|
+
async () => {
|
|
327
|
+
budget.reserveTool(options.signal);
|
|
328
|
+
if (!claimed || !claims) return run();
|
|
329
|
+
// The ledger is user-wide; a frontend write's fingerprint names its
|
|
330
|
+
// session, so the same browser action in two chats never collides.
|
|
331
|
+
const fingerprint = await operationKey(
|
|
332
|
+
toolName,
|
|
333
|
+
toolCall.args,
|
|
334
|
+
lane === 'frontend' ? options.sessionId : undefined,
|
|
306
335
|
);
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
336
|
+
const claim = await claims.claimWrite({
|
|
337
|
+
fingerprint,
|
|
338
|
+
toolName,
|
|
339
|
+
runId: options.runId,
|
|
340
|
+
sessionId: options.sessionId,
|
|
312
341
|
});
|
|
313
|
-
|
|
314
|
-
try {
|
|
315
|
-
const output = await run();
|
|
316
|
-
const uncertain = uncertainResultReason(output);
|
|
317
|
-
if (uncertain !== null) {
|
|
342
|
+
if (claim.status === 'blocked') {
|
|
318
343
|
logger.warn(
|
|
319
|
-
`[tool-execution] ${toolName}:
|
|
344
|
+
`[tool-execution] ${toolName}: an identical write since ${claim.since} has no known outcome; not run again`,
|
|
320
345
|
);
|
|
321
|
-
|
|
322
|
-
|
|
346
|
+
return new ToolMessage({
|
|
347
|
+
tool_call_id: toolCall.id ?? '',
|
|
348
|
+
name: toolName,
|
|
349
|
+
content: uncertainWriteToolResult(toolName),
|
|
350
|
+
status: 'error',
|
|
351
|
+
});
|
|
323
352
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
.releaseWrite(fingerprint, options.runId)
|
|
333
|
-
|
|
353
|
+
try {
|
|
354
|
+
const output = await run();
|
|
355
|
+
const uncertain = uncertainResultReason(output);
|
|
356
|
+
if (uncertain !== null) {
|
|
357
|
+
logger.warn(
|
|
358
|
+
`[tool-execution] ${toolName}: returned a failure that leaves its outcome unknown (${uncertain}); its claim stays in the ledger`,
|
|
359
|
+
);
|
|
360
|
+
} else {
|
|
361
|
+
await claims.releaseWrite(fingerprint, options.runId);
|
|
362
|
+
}
|
|
363
|
+
return output;
|
|
364
|
+
} catch (error) {
|
|
365
|
+
if (isUncertainOutcome(error, options.signal)) {
|
|
366
|
+
logger.warn(
|
|
367
|
+
`[tool-execution] ${toolName}: outcome unknown (${error instanceof Error ? error.message.split('\n')[0] : String(error)}); its claim stays in the ledger`,
|
|
368
|
+
);
|
|
369
|
+
} else {
|
|
370
|
+
await claims
|
|
371
|
+
.releaseWrite(fingerprint, options.runId)
|
|
372
|
+
.catch(() => undefined);
|
|
373
|
+
}
|
|
374
|
+
throw error;
|
|
334
375
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
376
|
+
},
|
|
377
|
+
options.sessionId,
|
|
378
|
+
);
|
|
338
379
|
},
|
|
339
380
|
});
|
|
340
381
|
}
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
* gets a synthetic result saying the outcome is unknown, so it can tell
|
|
13
13
|
* the user or verify with a read;
|
|
14
14
|
* - a started, unfinished **read** call runs again (it has no effect);
|
|
15
|
+
* - a started, unfinished **frontend** call (`PluginTool.surface`) that
|
|
16
|
+
* was never sent — it was parked waiting for a browser socket when the
|
|
17
|
+
* object restarted, so no browser saw it — runs again whatever its
|
|
18
|
+
* effect. Once sent (`markSent`, recorded by the frontend bridge when a
|
|
19
|
+
* socket receives it) it follows the read / write rules above;
|
|
15
20
|
* - a call that already finished is not executed again either (its result
|
|
16
21
|
* is in the checkpoint's pending writes; reaching here means LangGraph
|
|
17
22
|
* could not replay it) — writes get the unknown-outcome result, reads run.
|
|
@@ -41,6 +46,8 @@ export interface ToolMarkStore {
|
|
|
41
46
|
effect: ToolEffect;
|
|
42
47
|
}): Promise<ToolMark | undefined>;
|
|
43
48
|
bumpMark(runId: string, toolCallId: string): Promise<void>;
|
|
49
|
+
/** The call was handed to its executor outside the object (a browser socket). */
|
|
50
|
+
markSent(runId: string, toolCallId: string): Promise<void>;
|
|
44
51
|
finishMark(
|
|
45
52
|
runId: string,
|
|
46
53
|
toolCallId: string,
|
|
@@ -53,6 +60,12 @@ export interface ToolMarksOptions {
|
|
|
53
60
|
store: ToolMarkStore;
|
|
54
61
|
/** The effect of a tool by name; unknown tools are writes (see `toolEffectOf`). */
|
|
55
62
|
effectOf: (toolName: string) => ToolEffect;
|
|
63
|
+
/**
|
|
64
|
+
* `frontend` for a tool that runs in the user's browser
|
|
65
|
+
* (`PluginTool.surface`); such a call that was never sent before a restart
|
|
66
|
+
* runs again whatever its effect. Omitted → no tool is a frontend tool.
|
|
67
|
+
*/
|
|
68
|
+
surfaceOf?: (toolName: string) => 'frontend' | undefined;
|
|
56
69
|
/**
|
|
57
70
|
* The reply text the user already received before the run was cut off.
|
|
58
71
|
* Set only on a resumed attempt; consumed by the first model call.
|
|
@@ -84,18 +97,32 @@ export function continuationNote(partialText: string): string {
|
|
|
84
97
|
);
|
|
85
98
|
}
|
|
86
99
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
'
|
|
93
|
-
'
|
|
94
|
-
'
|
|
95
|
-
'
|
|
96
|
-
'
|
|
97
|
-
'
|
|
100
|
+
/**
|
|
101
|
+
* Verbs a read-only tool name starts with, in either convention: snake case
|
|
102
|
+
* (`get_entity`) or camel case (`getEntity`, as browser tools are named).
|
|
103
|
+
*/
|
|
104
|
+
const READ_VERBS = [
|
|
105
|
+
'list',
|
|
106
|
+
'get',
|
|
107
|
+
'find',
|
|
108
|
+
'search',
|
|
109
|
+
'read',
|
|
110
|
+
'preview',
|
|
111
|
+
'describe',
|
|
112
|
+
'check',
|
|
113
|
+
'explain',
|
|
114
|
+
'validate',
|
|
115
|
+
'view',
|
|
98
116
|
];
|
|
117
|
+
|
|
118
|
+
function startsWithReadVerb(name: string): boolean {
|
|
119
|
+
for (const verb of READ_VERBS) {
|
|
120
|
+
if (!name.startsWith(verb)) continue;
|
|
121
|
+
const next = name.charAt(verb.length);
|
|
122
|
+
if (next === '_' || (next >= 'A' && next <= 'Z')) return true;
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
99
126
|
const READ_NAMES = new Set([
|
|
100
127
|
'requirements',
|
|
101
128
|
'flow_status',
|
|
@@ -125,9 +152,7 @@ export function toolEffectOf(tool: {
|
|
|
125
152
|
? tool.name.slice(tool.name.lastIndexOf('__') + 2)
|
|
126
153
|
: tool.name;
|
|
127
154
|
if (READ_NAMES.has(bare)) return 'read';
|
|
128
|
-
|
|
129
|
-
if (bare.startsWith(prefix)) return 'read';
|
|
130
|
-
return 'write';
|
|
155
|
+
return startsWithReadVerb(bare) ? 'read' : 'write';
|
|
131
156
|
}
|
|
132
157
|
|
|
133
158
|
/** Whether a tool call's output is an error result (`ToolMessage` status `error`). */
|
|
@@ -170,7 +195,16 @@ export function createToolMarksMiddleware(
|
|
|
170
195
|
effect,
|
|
171
196
|
});
|
|
172
197
|
if (existing) {
|
|
173
|
-
|
|
198
|
+
const neverSent =
|
|
199
|
+
!existing.doneAt &&
|
|
200
|
+
existing.sentAt === null &&
|
|
201
|
+
options.surfaceOf?.(toolName) === 'frontend';
|
|
202
|
+
if (neverSent) {
|
|
203
|
+
logger.log(
|
|
204
|
+
`[tool-marks] ${toolName} (${toolCallId}) was parked and never sent to a browser in an earlier attempt of run ${options.runId}; running it again`,
|
|
205
|
+
);
|
|
206
|
+
await options.store.bumpMark(options.runId, toolCallId);
|
|
207
|
+
} else if (effect === 'write') {
|
|
174
208
|
logger.warn(
|
|
175
209
|
`[tool-marks] ${toolName} (${toolCallId}) was ${existing.doneAt ? 'already executed' : 'interrupted mid-execution'} in an earlier attempt of run ${options.runId}; not run again`,
|
|
176
210
|
);
|
|
@@ -185,11 +219,12 @@ export function createToolMarksMiddleware(
|
|
|
185
219
|
content: unknownOutcomeToolResult(toolName),
|
|
186
220
|
status: 'error',
|
|
187
221
|
});
|
|
222
|
+
} else {
|
|
223
|
+
logger.log(
|
|
224
|
+
`[tool-marks] ${toolName} (${toolCallId}) is read-only; running it again after the interruption`,
|
|
225
|
+
);
|
|
226
|
+
await options.store.bumpMark(options.runId, toolCallId);
|
|
188
227
|
}
|
|
189
|
-
logger.log(
|
|
190
|
-
`[tool-marks] ${toolName} (${toolCallId}) is read-only; running it again after the interruption`,
|
|
191
|
-
);
|
|
192
|
-
await options.store.bumpMark(options.runId, toolCallId);
|
|
193
228
|
}
|
|
194
229
|
try {
|
|
195
230
|
const output = await handler(request);
|
|
@@ -189,6 +189,8 @@ export interface AmbientServices {
|
|
|
189
189
|
* `DecisionProviderUnavailableError`.
|
|
190
190
|
*/
|
|
191
191
|
decisions?: DecisionEvaluator;
|
|
192
|
+
actionProposals?: RuntimeContext['actionProposals'];
|
|
193
|
+
hubActions?: RuntimeContext['hubActions'];
|
|
192
194
|
/** Host task scheduler for the current user, when the host provides one. */
|
|
193
195
|
tasks?: import('../plugin-api/types').OracleTasksSurface;
|
|
194
196
|
/** Host user-preferences store for the current user, when provided. */
|
|
@@ -202,6 +204,12 @@ export interface AmbientServices {
|
|
|
202
204
|
onTurnEnd?: (dispose: () => void | Promise<void>) => void;
|
|
203
205
|
/** Host keep-alive for un-awaited work (`ctx.waitUntil` on Workers). */
|
|
204
206
|
background?: (work: Promise<unknown>) => void;
|
|
207
|
+
/**
|
|
208
|
+
* Records that the tool call `toolCallId` of this turn left the object
|
|
209
|
+
* (`ctx.markDispatched`). Set per turn by a host that keeps write-ahead
|
|
210
|
+
* tool marks.
|
|
211
|
+
*/
|
|
212
|
+
markDispatched?: (toolCallId: string) => void;
|
|
205
213
|
}
|
|
206
214
|
|
|
207
215
|
// ── Scoped emitter ──────────────────────────────────────────────────────────
|
|
@@ -317,6 +325,12 @@ export interface RuntimeSessionContext {
|
|
|
317
325
|
client: 'portal' | 'matrix' | 'slack' | 'channel';
|
|
318
326
|
wsId?: string;
|
|
319
327
|
requestId: string;
|
|
328
|
+
runId?: string;
|
|
329
|
+
channel?: {
|
|
330
|
+
provider: string;
|
|
331
|
+
bindingId: string;
|
|
332
|
+
bindingRevision: number;
|
|
333
|
+
};
|
|
320
334
|
roomId?: string;
|
|
321
335
|
surface?: SessionSurface;
|
|
322
336
|
}
|
|
@@ -434,6 +448,8 @@ export function buildRuntimeContext<TConfig = MergedConfig>(
|
|
|
434
448
|
};
|
|
435
449
|
|
|
436
450
|
const delegation = user.ucanDelegation;
|
|
451
|
+
const toolCallId = runConfig.toolCall?.id;
|
|
452
|
+
const { markDispatched } = ambient;
|
|
437
453
|
|
|
438
454
|
const ctx: RuntimeContext<TConfig> = {
|
|
439
455
|
user,
|
|
@@ -520,6 +536,10 @@ export function buildRuntimeContext<TConfig = MergedConfig>(
|
|
|
520
536
|
logger: ambient.logger,
|
|
521
537
|
abortSignal,
|
|
522
538
|
...(ambient.tasks ? { tasks: ambient.tasks } : {}),
|
|
539
|
+
...(ambient.actionProposals
|
|
540
|
+
? { actionProposals: ambient.actionProposals }
|
|
541
|
+
: {}),
|
|
542
|
+
...(ambient.hubActions ? { hubActions: ambient.hubActions } : {}),
|
|
523
543
|
...(ambient.preferences ? { preferences: ambient.preferences } : {}),
|
|
524
544
|
...(ambient.kv ? { kv: ambient.kv } : {}),
|
|
525
545
|
...(ambient.frontend ? { frontend: ambient.frontend } : {}),
|
|
@@ -527,7 +547,10 @@ export function buildRuntimeContext<TConfig = MergedConfig>(
|
|
|
527
547
|
...(ambient.onTurnEnd ? { onTurnEnd: ambient.onTurnEnd } : {}),
|
|
528
548
|
...(ambient.background ? { background: ambient.background } : {}),
|
|
529
549
|
shared: EMPTY_SHARED,
|
|
530
|
-
...(
|
|
550
|
+
...(toolCallId ? { toolCallId } : {}),
|
|
551
|
+
...(toolCallId && markDispatched
|
|
552
|
+
? { markDispatched: () => markDispatched(toolCallId) }
|
|
553
|
+
: {}),
|
|
531
554
|
};
|
|
532
555
|
if (sharedFactory) {
|
|
533
556
|
ctx.shared = sharedFactory(ctx);
|
|
@@ -730,5 +753,8 @@ export function createNoopAmbient(
|
|
|
730
753
|
logger: overrides.logger ?? NOOP_LOGGER,
|
|
731
754
|
...(overrides.decisions ? { decisions: overrides.decisions } : {}),
|
|
732
755
|
...(overrides.kv ? { kv: overrides.kv } : {}),
|
|
756
|
+
...(overrides.markDispatched
|
|
757
|
+
? { markDispatched: overrides.markDispatched }
|
|
758
|
+
: {}),
|
|
733
759
|
};
|
|
734
760
|
}
|
package/src/core/state.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface BrowserToolCall {
|
|
|
21
21
|
name: string;
|
|
22
22
|
description: string;
|
|
23
23
|
schema: Record<string, unknown>;
|
|
24
|
+
/** The tool's effect as the client declares it (`'read'` / `'write'`). */
|
|
25
|
+
effect?: 'read' | 'write';
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
/** AG-UI action descriptor injected per request from the client. */
|