@ixo/oracle-runtime-workers 0.18.0 → 0.20.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/package.json +4 -4
- package/src/channels/auth.ts +30 -8
- package/src/channels/contract.ts +25 -12
- package/src/channels/failures.ts +3 -3
- package/src/channels/turns.ts +10 -10
- package/src/core/capability-router.ts +1 -0
- package/src/core/env.ts +32 -0
- package/src/core/main-agent.ts +20 -0
- package/src/core/middlewares/tool-marks.ts +2 -1
- package/src/core/prompt-composer.ts +3 -1
- package/src/delivery/profile.ts +4 -1
- package/src/delivery/schema.ts +1 -1
- package/src/do/contracts.ts +43 -5
- package/src/do/room-mirror.ts +1 -1
- package/src/do/user-oracle-do.ts +328 -14
- package/src/memory/session-history-indexer.ts +3 -0
- package/src/memory/user-context-fetcher.ts +977 -0
- package/src/memory/user-context-invalidation.ts +66 -0
- package/src/shell/app.ts +2 -1
package/src/do/user-oracle-do.ts
CHANGED
|
@@ -191,6 +191,7 @@ import {
|
|
|
191
191
|
type RunsStatus,
|
|
192
192
|
type ContextStatus,
|
|
193
193
|
type SessionContextStatus,
|
|
194
|
+
type UserContextPrefetchStatus,
|
|
194
195
|
type RunSummary,
|
|
195
196
|
type TurnIdentity,
|
|
196
197
|
type TurnRequest,
|
|
@@ -229,6 +230,14 @@ import {
|
|
|
229
230
|
type HistoryMessage,
|
|
230
231
|
type IndexableSession,
|
|
231
232
|
} from '../memory/session-history-indexer';
|
|
233
|
+
import {
|
|
234
|
+
USER_CONTEXT_SLOTS,
|
|
235
|
+
UserContextFetcher,
|
|
236
|
+
type UserContextFetchOutcome,
|
|
237
|
+
type UserContextFetchParams,
|
|
238
|
+
} from '../memory/user-context-fetcher';
|
|
239
|
+
import { createUserContextInvalidationMiddleware } from '../memory/user-context-invalidation';
|
|
240
|
+
import { MEMORY_CAPABILITY } from '../plugins/delegated-capabilities';
|
|
232
241
|
import {
|
|
233
242
|
createSseSubscriberStream,
|
|
234
243
|
runTurnFrames,
|
|
@@ -329,7 +338,7 @@ import {
|
|
|
329
338
|
resolveTurnDelegation,
|
|
330
339
|
WorkersUcanService,
|
|
331
340
|
} from './ucan-service';
|
|
332
|
-
import type { UcanDelegation } from '../plugin-api/types';
|
|
341
|
+
import type { UcanDelegation, UserContextData } from '../plugin-api/types';
|
|
333
342
|
|
|
334
343
|
/** What one attempt's agent build reads from the request body. */
|
|
335
344
|
type TurnBuildBody = Pick<
|
|
@@ -347,6 +356,56 @@ interface TurnBuildRun {
|
|
|
347
356
|
continuation: string | null;
|
|
348
357
|
/** Domain anchors the run started with (resumed attempts keep them). */
|
|
349
358
|
domainPins?: DomainContextPins;
|
|
359
|
+
/** A `router.update` step for the run's client while the build waits. */
|
|
360
|
+
onProgress?: (step: string) => void;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Where the user-context prefetch runs: the engine and the `USER_CONTEXT_*` knobs. */
|
|
364
|
+
interface UserContextSettings {
|
|
365
|
+
engineUrl: string;
|
|
366
|
+
capMs?: number;
|
|
367
|
+
cacheTtlMs?: number;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* The second half of a first-contact prefetch: waits for the gather (at
|
|
372
|
+
* most the cap; calling `onSlow` once it has waited past the grace) and
|
|
373
|
+
* returns the context the thread takes, or `undefined` when there is none
|
|
374
|
+
* — no answer within the cap, a failure, a skip, or the turn was aborted
|
|
375
|
+
* (the gather then keeps running in the background).
|
|
376
|
+
*/
|
|
377
|
+
type UserContextSettle = (
|
|
378
|
+
onSlow: () => void,
|
|
379
|
+
signal: AbortSignal,
|
|
380
|
+
) => Promise<UserContextData | undefined>;
|
|
381
|
+
|
|
382
|
+
/** How long a first turn waits on the user-context prefetch before saying so. */
|
|
383
|
+
const USER_CONTEXT_PROGRESS_GRACE_MS = 300;
|
|
384
|
+
const USER_CONTEXT_PROGRESS_STEP = 'Recalling your memories…';
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* A prefetched context with every slot present, a slot the engine did not
|
|
388
|
+
* return as an empty section (which renders nothing). The checkpointed
|
|
389
|
+
* value then marks a landed gather (see `hasUserContext`) even for a user
|
|
390
|
+
* with no memory yet, so the thread's later turns do not count as first
|
|
391
|
+
* contact; and the written value is the whole context under a merging
|
|
392
|
+
* reducer (the `MainAgentGraphState` annotation's) as well as under the
|
|
393
|
+
* last-value channel the agent declares (`userContextStateMiddleware`).
|
|
394
|
+
*/
|
|
395
|
+
function withEverySlot(context: UserContextData): UserContextData {
|
|
396
|
+
const out: UserContextData = { ...context };
|
|
397
|
+
for (const slot of USER_CONTEXT_SLOTS) out[slot] ??= {};
|
|
398
|
+
return out;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** A checkpointed `userContext` a gather has written (not the channel's `{}` default). */
|
|
402
|
+
function hasUserContext(value: unknown): boolean {
|
|
403
|
+
return (
|
|
404
|
+
typeof value === 'object' &&
|
|
405
|
+
value !== null &&
|
|
406
|
+
!Array.isArray(value) &&
|
|
407
|
+
Object.keys(value).length > 0
|
|
408
|
+
);
|
|
350
409
|
}
|
|
351
410
|
|
|
352
411
|
/**
|
|
@@ -877,6 +936,9 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
877
936
|
/** Session-history → memory-engine indexing (built lazily, see `scheduleHistoryIndexing`). */
|
|
878
937
|
private historyIndexer: SessionHistoryIndexer | null = null;
|
|
879
938
|
|
|
939
|
+
/** First-turn user-context prefetch (built lazily, see `startUserContextPrefetch`). */
|
|
940
|
+
private userContextFetcher: UserContextFetcher | null = null;
|
|
941
|
+
|
|
880
942
|
/**
|
|
881
943
|
* Transcripts captured BEFORE a session is deleted, keyed by session id:
|
|
882
944
|
* the indexer runs after the RPC returned, when the rows are gone.
|
|
@@ -1734,7 +1796,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
1734
1796
|
if (!roomId)
|
|
1735
1797
|
throw new ChannelError(
|
|
1736
1798
|
428,
|
|
1737
|
-
'
|
|
1799
|
+
"The user's oracle room is not ready",
|
|
1738
1800
|
'room_not_ready',
|
|
1739
1801
|
);
|
|
1740
1802
|
return (await this.createSession(identity, { roomId, markerTxnId }))
|
|
@@ -1753,13 +1815,13 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
1753
1815
|
)
|
|
1754
1816
|
throw new ChannelError(
|
|
1755
1817
|
404,
|
|
1756
|
-
'
|
|
1818
|
+
'Oracle session not found',
|
|
1757
1819
|
'session_not_found',
|
|
1758
1820
|
);
|
|
1759
1821
|
if (!(await this.roomIsEncrypted(roomId)))
|
|
1760
1822
|
throw new ChannelError(
|
|
1761
1823
|
409,
|
|
1762
|
-
'
|
|
1824
|
+
"The user's oracle room must be encrypted",
|
|
1763
1825
|
'room_not_encrypted',
|
|
1764
1826
|
);
|
|
1765
1827
|
},
|
|
@@ -2352,7 +2414,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
2352
2414
|
if (next !== null) deadlines.push(Math.max(next, now + 1000));
|
|
2353
2415
|
}
|
|
2354
2416
|
|
|
2355
|
-
// Channel requests: retry the
|
|
2417
|
+
// Channel requests: retry the user's oracle room mirrors that are due, and
|
|
2356
2418
|
// prune (bounded) receipts that never started a run and old run
|
|
2357
2419
|
// tombstones (channels/turns.ts).
|
|
2358
2420
|
if (this.channelTurns) {
|
|
@@ -3026,6 +3088,8 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
3026
3088
|
mintInvocation: (target, opts) =>
|
|
3027
3089
|
ambient.ucan.mintInvocation(userDid, target, opts),
|
|
3028
3090
|
},
|
|
3091
|
+
// Newly indexed history changes what the prefetch would recall.
|
|
3092
|
+
onIndexed: (roomId) => this.userContextFetcher?.invalidate(roomId),
|
|
3029
3093
|
speakerLabels: async (roomId) => {
|
|
3030
3094
|
const prefs = await this.preferences
|
|
3031
3095
|
?.get(roomId)
|
|
@@ -3058,6 +3122,195 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
3058
3122
|
);
|
|
3059
3123
|
}
|
|
3060
3124
|
|
|
3125
|
+
/**
|
|
3126
|
+
* The user-context prefetch's settings, or null where it does not run:
|
|
3127
|
+
* switched off (`USER_CONTEXT_PREFETCH=off`) or no memory engine.
|
|
3128
|
+
*/
|
|
3129
|
+
private userContextSettings(): UserContextSettings | null {
|
|
3130
|
+
const env = this.core.validatedEnv;
|
|
3131
|
+
if (env['USER_CONTEXT_PREFETCH'] === 'off') return null;
|
|
3132
|
+
const engineUrl = env['MEMORY_ENGINE_URL'];
|
|
3133
|
+
if (typeof engineUrl !== 'string' || engineUrl.length === 0) return null;
|
|
3134
|
+
const capMs = env['USER_CONTEXT_PREFETCH_CAP_MS'];
|
|
3135
|
+
const cacheTtlMs = env['USER_CONTEXT_CACHE_TTL_MS'];
|
|
3136
|
+
return {
|
|
3137
|
+
engineUrl,
|
|
3138
|
+
...(typeof capMs === 'number' ? { capMs } : {}),
|
|
3139
|
+
...(typeof cacheTtlMs === 'number' ? { cacheTtlMs } : {}),
|
|
3140
|
+
};
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
/** One fetcher per object: its cache outlives turns, sessions and resets. */
|
|
3144
|
+
private userContextFetcherFor(
|
|
3145
|
+
settings: UserContextSettings,
|
|
3146
|
+
): UserContextFetcher {
|
|
3147
|
+
this.userContextFetcher ??= new UserContextFetcher(
|
|
3148
|
+
{
|
|
3149
|
+
memoryEngineUrl: settings.engineUrl,
|
|
3150
|
+
oracleDid: this.env.ORACLE_DID,
|
|
3151
|
+
// Looked up per call, not captured: a working-copy reset replaces
|
|
3152
|
+
// the ambient services while a gather may still be running.
|
|
3153
|
+
ucan: {
|
|
3154
|
+
hasSigningKey: () => this.ambient?.ucan.hasSigningKey() ?? false,
|
|
3155
|
+
resolveServiceDid: async (url) =>
|
|
3156
|
+
(await this.ambient?.ucan.resolveServiceDid(url)) ?? null,
|
|
3157
|
+
mintInvocation: async (target, opts) => {
|
|
3158
|
+
const ambient = this.ambient;
|
|
3159
|
+
const userDid = this.userDid;
|
|
3160
|
+
if (!ambient || !userDid)
|
|
3161
|
+
throw new Error('the user object is not booted');
|
|
3162
|
+
return ambient.ucan.mintInvocation(userDid, target, opts);
|
|
3163
|
+
},
|
|
3164
|
+
},
|
|
3165
|
+
logger: console,
|
|
3166
|
+
},
|
|
3167
|
+
{
|
|
3168
|
+
...(settings.capMs !== undefined
|
|
3169
|
+
? { blockingCapMs: settings.capMs }
|
|
3170
|
+
: {}),
|
|
3171
|
+
...(settings.cacheTtlMs !== undefined
|
|
3172
|
+
? { cacheTtlMs: settings.cacheTtlMs }
|
|
3173
|
+
: {}),
|
|
3174
|
+
},
|
|
3175
|
+
);
|
|
3176
|
+
return this.userContextFetcher;
|
|
3177
|
+
}
|
|
3178
|
+
|
|
3179
|
+
/**
|
|
3180
|
+
* Start a turn's user-context prefetch — stale-while-revalidate, as the
|
|
3181
|
+
* Node AgentBuilder does it. A thread's context is fixed on first
|
|
3182
|
+
* contact: a thread whose checkpoint already holds a gathered context
|
|
3183
|
+
* never waits and never takes a newer answer; it only refreshes the
|
|
3184
|
+
* room's cache in the background (a no-op while the cache is fresh), so
|
|
3185
|
+
* the NEXT session in the room starts from it. First contact — including
|
|
3186
|
+
* a thread whose earlier first turn got nothing (timeout, failure, skip)
|
|
3187
|
+
* — waits for the gather, at most the cap. Returns the step the build
|
|
3188
|
+
* awaits right before the agent is built, or null where this turn
|
|
3189
|
+
* writes no context. Nothing here throws; every gather is handed to
|
|
3190
|
+
* `ctx.waitUntil`, so one that outlives the cap still fills the cache.
|
|
3191
|
+
*/
|
|
3192
|
+
private startUserContextPrefetch(input: {
|
|
3193
|
+
sessionId: string;
|
|
3194
|
+
roomId: string | undefined;
|
|
3195
|
+
userDid: string;
|
|
3196
|
+
delegation: UcanDelegation;
|
|
3197
|
+
prior: unknown;
|
|
3198
|
+
client: TurnRequest['client'];
|
|
3199
|
+
roomKind: TurnRequest['roomKind'];
|
|
3200
|
+
}): UserContextSettle | null {
|
|
3201
|
+
// Task runs never get the user's memory (see `scheduleHistoryIndexing`).
|
|
3202
|
+
if (!input.roomId || input.sessionId.startsWith(TASK_SESSION_PREFIX))
|
|
3203
|
+
return null;
|
|
3204
|
+
const settings = this.userContextSettings();
|
|
3205
|
+
const ambient = this.ambient;
|
|
3206
|
+
if (!settings || !ambient) return null;
|
|
3207
|
+
// A group room's reply is posted to every member: one user's memory
|
|
3208
|
+
// must not be (as direct reads are not, see `admissionApplies`).
|
|
3209
|
+
if (input.client === 'matrix' && input.roomKind === 'group') {
|
|
3210
|
+
console.log('[user-context] skipped: group room');
|
|
3211
|
+
return null;
|
|
3212
|
+
}
|
|
3213
|
+
// Without a memory grant the gather could only fail at the mint: skip
|
|
3214
|
+
// it before any request (the engine's did.json included).
|
|
3215
|
+
if (
|
|
3216
|
+
!ambient.ucan.hasCapability(
|
|
3217
|
+
input.delegation,
|
|
3218
|
+
MEMORY_CAPABILITY.resource,
|
|
3219
|
+
MEMORY_CAPABILITY.action,
|
|
3220
|
+
)
|
|
3221
|
+
) {
|
|
3222
|
+
console.log(
|
|
3223
|
+
`[user-context] skipped: the delegation grants no ${MEMORY_CAPABILITY.resource} ${MEMORY_CAPABILITY.action}`,
|
|
3224
|
+
);
|
|
3225
|
+
return null;
|
|
3226
|
+
}
|
|
3227
|
+
const fetcher = this.userContextFetcherFor(settings);
|
|
3228
|
+
const params: UserContextFetchParams = {
|
|
3229
|
+
roomId: input.roomId,
|
|
3230
|
+
userDid: input.userDid,
|
|
3231
|
+
sessionId: input.sessionId,
|
|
3232
|
+
};
|
|
3233
|
+
const keepGathering = (): void =>
|
|
3234
|
+
this.ctx.waitUntil(
|
|
3235
|
+
fetcher.refresh(params).catch((err: unknown) => {
|
|
3236
|
+
console.warn(
|
|
3237
|
+
`[user-context] background refresh failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
3238
|
+
);
|
|
3239
|
+
}),
|
|
3240
|
+
);
|
|
3241
|
+
if (hasUserContext(input.prior)) {
|
|
3242
|
+
keepGathering();
|
|
3243
|
+
return null;
|
|
3244
|
+
}
|
|
3245
|
+
const pending = fetcher
|
|
3246
|
+
.fetch(params)
|
|
3247
|
+
.catch((err: unknown): UserContextFetchOutcome => {
|
|
3248
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
3249
|
+
console.warn(`[user-context] failed: ${reason}`);
|
|
3250
|
+
return { kind: 'failed', reason };
|
|
3251
|
+
});
|
|
3252
|
+
// Joins the gather `fetch` started (a no-op on a cache hit).
|
|
3253
|
+
keepGathering();
|
|
3254
|
+
return async (onSlow, signal) => {
|
|
3255
|
+
if (signal.aborted) return undefined;
|
|
3256
|
+
const timer = setTimeout(onSlow, USER_CONTEXT_PROGRESS_GRACE_MS);
|
|
3257
|
+
let onAbort: (() => void) | undefined;
|
|
3258
|
+
const aborted = new Promise<undefined>((resolve) => {
|
|
3259
|
+
onAbort = () => resolve(undefined);
|
|
3260
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
3261
|
+
});
|
|
3262
|
+
try {
|
|
3263
|
+
const outcome = await Promise.race([pending, aborted]);
|
|
3264
|
+
return outcome?.kind === 'cached' || outcome?.kind === 'fresh'
|
|
3265
|
+
? withEverySlot(outcome.context)
|
|
3266
|
+
: undefined;
|
|
3267
|
+
} finally {
|
|
3268
|
+
clearTimeout(timer);
|
|
3269
|
+
if (onAbort) signal.removeEventListener('abort', onAbort);
|
|
3270
|
+
}
|
|
3271
|
+
};
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
/** `GET /debug/context` — the prefetch for a session's room, else the user's room. */
|
|
3275
|
+
private async userContextPrefetchStatus(
|
|
3276
|
+
sessionId: string | undefined,
|
|
3277
|
+
): Promise<UserContextPrefetchStatus> {
|
|
3278
|
+
const userDid = this.userDid;
|
|
3279
|
+
const sessionRoom = sessionId
|
|
3280
|
+
? (this.sessionRooms.get(sessionId) ??
|
|
3281
|
+
(await this.sessions?.getSession(sessionId))?.roomId)
|
|
3282
|
+
: undefined;
|
|
3283
|
+
const room =
|
|
3284
|
+
sessionRoom ??
|
|
3285
|
+
this.mainRoomId ??
|
|
3286
|
+
(userDid
|
|
3287
|
+
? (await this.gateway.resolveUserRoom(userDid).catch(() => null))
|
|
3288
|
+
?.roomId
|
|
3289
|
+
: undefined);
|
|
3290
|
+
const status =
|
|
3291
|
+
room && this.userContextFetcher
|
|
3292
|
+
? this.userContextFetcher.status(room)
|
|
3293
|
+
: undefined;
|
|
3294
|
+
const last = status?.lastOutcome;
|
|
3295
|
+
return {
|
|
3296
|
+
enabled: this.userContextSettings() !== null,
|
|
3297
|
+
...(room ? { room } : {}),
|
|
3298
|
+
cached: status?.cached ?? false,
|
|
3299
|
+
...(status?.ageMs !== undefined ? { ageMs: status.ageMs } : {}),
|
|
3300
|
+
inFlight: status?.inFlight ?? false,
|
|
3301
|
+
...(last ? { lastOutcome: last.kind } : {}),
|
|
3302
|
+
...(last?.kind === 'failed' || last?.kind === 'skipped'
|
|
3303
|
+
? { lastReason: last.reason }
|
|
3304
|
+
: {}),
|
|
3305
|
+
...(last?.kind === 'fresh'
|
|
3306
|
+
? { lastMs: last.ms, lastSlots: last.slots }
|
|
3307
|
+
: {}),
|
|
3308
|
+
...(status?.lastAt !== undefined
|
|
3309
|
+
? { lastAt: new Date(status.lastAt).toISOString() }
|
|
3310
|
+
: {}),
|
|
3311
|
+
};
|
|
3312
|
+
}
|
|
3313
|
+
|
|
3061
3314
|
async listMessages(
|
|
3062
3315
|
identity: TurnIdentity,
|
|
3063
3316
|
sessionId: string,
|
|
@@ -4454,6 +4707,21 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
4454
4707
|
resumed: !freshInput,
|
|
4455
4708
|
continuation: freshInput ? null : live.continuation,
|
|
4456
4709
|
...(stored.domainPins ? { domainPins: stored.domainPins } : {}),
|
|
4710
|
+
// The Portal shows `router.update` steps (as it shows "Queued…");
|
|
4711
|
+
// chat surfaces have no place for one.
|
|
4712
|
+
...(req.client === 'portal'
|
|
4713
|
+
? {
|
|
4714
|
+
onProgress: (step: string) => {
|
|
4715
|
+
if (live.buffer.isClosed) return;
|
|
4716
|
+
live.buffer.push('router.update', {
|
|
4717
|
+
step,
|
|
4718
|
+
sessionId: req.sessionId,
|
|
4719
|
+
requestId: req.requestId,
|
|
4720
|
+
runId: live.runId,
|
|
4721
|
+
});
|
|
4722
|
+
},
|
|
4723
|
+
}
|
|
4724
|
+
: {}),
|
|
4457
4725
|
},
|
|
4458
4726
|
);
|
|
4459
4727
|
const sessionId = req.sessionId;
|
|
@@ -4763,7 +5031,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
4763
5031
|
outcome: RunOutcome,
|
|
4764
5032
|
): Promise<void> {
|
|
4765
5033
|
// A channel run's reply (or the end of a run without one) is mirrored
|
|
4766
|
-
// into the
|
|
5034
|
+
// into the user's oracle room in the background: never awaited here, so
|
|
4767
5035
|
// the session's next queued run starts at once. Without the handle (a
|
|
4768
5036
|
// reset dropped it mid-run) the next poll or alarm mirrors it.
|
|
4769
5037
|
if (record.client === 'channel') this.channelTurns?.deliverReply();
|
|
@@ -4845,6 +5113,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
4845
5113
|
...(sessionId
|
|
4846
5114
|
? { session: await this.sessionContextStatus(sessionId) }
|
|
4847
5115
|
: {}),
|
|
5116
|
+
userContext: await this.userContextPrefetchStatus(sessionId),
|
|
4848
5117
|
};
|
|
4849
5118
|
}
|
|
4850
5119
|
|
|
@@ -5345,6 +5614,29 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
5345
5614
|
}
|
|
5346
5615
|
if (sessionRoomId) this.sessionRooms.set(req.sessionId, sessionRoomId);
|
|
5347
5616
|
|
|
5617
|
+
// The delegation the whole turn reads: the prefetch's memory grant
|
|
5618
|
+
// here, the router's candidates and every capability check of the
|
|
5619
|
+
// build and its tools below.
|
|
5620
|
+
const ucanDelegation = await this.turnDelegation(req.identity);
|
|
5621
|
+
const existing = await saver.getTupleWithoutMessages({
|
|
5622
|
+
configurable: { thread_id: req.sessionId },
|
|
5623
|
+
});
|
|
5624
|
+
const priorState = existing?.checkpoint.channel_values ?? {};
|
|
5625
|
+
// What the memory engine knows about the user, for the prompt's
|
|
5626
|
+
// context block: started now so the round-trip overlaps the rest of
|
|
5627
|
+
// the build, settled right before the agent is built.
|
|
5628
|
+
const settleUserContext = suppliedContextOnly
|
|
5629
|
+
? null
|
|
5630
|
+
: this.startUserContextPrefetch({
|
|
5631
|
+
sessionId: req.sessionId,
|
|
5632
|
+
roomId: sessionRoomId,
|
|
5633
|
+
userDid: req.identity.userDid,
|
|
5634
|
+
delegation: ucanDelegation,
|
|
5635
|
+
prior: priorState.userContext,
|
|
5636
|
+
client: req.client,
|
|
5637
|
+
roomKind: req.roomKind,
|
|
5638
|
+
});
|
|
5639
|
+
|
|
5348
5640
|
// A Matrix turn with no usable delegation: ask the web app to open its
|
|
5349
5641
|
// "authorize for Matrix" modal (throttled per user, best-effort) — the
|
|
5350
5642
|
// Node AgentBuilder's `maybePromptReauth`.
|
|
@@ -5425,10 +5717,6 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
5425
5717
|
signal: abortController.signal,
|
|
5426
5718
|
});
|
|
5427
5719
|
|
|
5428
|
-
const existing = await saver.getTupleWithoutMessages({
|
|
5429
|
-
configurable: { thread_id: req.sessionId },
|
|
5430
|
-
});
|
|
5431
|
-
const priorState = existing?.checkpoint.channel_values ?? {};
|
|
5432
5720
|
// Request metadata (editor room, space, session run, entity) → state,
|
|
5433
5721
|
// by the Node agent-builder's rules (see turn-metadata.ts).
|
|
5434
5722
|
const meta = parseTurnMetadata(
|
|
@@ -5461,9 +5749,6 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
5461
5749
|
client: req.client,
|
|
5462
5750
|
env: langsmithEnvFromWorkerEnv(this.env),
|
|
5463
5751
|
});
|
|
5464
|
-
// The delegation the whole turn reads: the router's candidates here,
|
|
5465
|
-
// every capability check of the build and its tools below.
|
|
5466
|
-
const ucanDelegation = await this.turnDelegation(req.identity);
|
|
5467
5752
|
const preloadedPlugins =
|
|
5468
5753
|
!suppliedContextOnly && this.capabilityRouter
|
|
5469
5754
|
? await this.capabilityRouter({
|
|
@@ -5597,6 +5882,17 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
5597
5882
|
// which is a tool result to the main agent. Wrapped tools are capped
|
|
5598
5883
|
// inside the wrapper (already under the cap by the time it runs).
|
|
5599
5884
|
const capMiddleware = createResultCapMiddleware(resultCap);
|
|
5885
|
+
// A successful memory write drops the room's prefetched context, so
|
|
5886
|
+
// the next session gathers again (this thread keeps its own).
|
|
5887
|
+
const userContextInvalidation =
|
|
5888
|
+
sessionRoomId && this.userContextSettings()
|
|
5889
|
+
? createUserContextInvalidationMiddleware({
|
|
5890
|
+
roomId: sessionRoomId,
|
|
5891
|
+
invalidate: (roomId) =>
|
|
5892
|
+
this.userContextFetcher?.invalidate(roomId),
|
|
5893
|
+
logger: console,
|
|
5894
|
+
})
|
|
5895
|
+
: null;
|
|
5600
5896
|
|
|
5601
5897
|
// Chat delivery: a channel or Matrix turn replies as short messages and
|
|
5602
5898
|
// may hand long content to `create_artifact` (bound only when artefact
|
|
@@ -5623,6 +5919,16 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
5623
5919
|
]
|
|
5624
5920
|
: [];
|
|
5625
5921
|
|
|
5922
|
+
// The prefetched user context on first contact (bounded by the cap and
|
|
5923
|
+
// the turn's abort); a wait past the grace tells a Portal client what
|
|
5924
|
+
// it waits on.
|
|
5925
|
+
const userContext = settleUserContext
|
|
5926
|
+
? await settleUserContext(
|
|
5927
|
+
() => run.onProgress?.(USER_CONTEXT_PROGRESS_STEP),
|
|
5928
|
+
abortController.signal,
|
|
5929
|
+
)
|
|
5930
|
+
: undefined;
|
|
5931
|
+
|
|
5626
5932
|
const built = await createMainAgent({
|
|
5627
5933
|
executionProfile: req.executionProfile,
|
|
5628
5934
|
registries: core.registries,
|
|
@@ -5660,7 +5966,11 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
5660
5966
|
: [],
|
|
5661
5967
|
// Applied to the main agent AND every sub-agent: a sub-agent's
|
|
5662
5968
|
// own tool calls are marked and capped too.
|
|
5663
|
-
toolMiddlewares: [
|
|
5969
|
+
toolMiddlewares: [
|
|
5970
|
+
marksMiddleware,
|
|
5971
|
+
capMiddleware,
|
|
5972
|
+
...(userContextInvalidation ? [userContextInvalidation] : []),
|
|
5973
|
+
],
|
|
5664
5974
|
// Budget, scheduling and write claims, innermost around each tool
|
|
5665
5975
|
// call (main agent and sub-agents alike).
|
|
5666
5976
|
toolExecution: executionMiddleware,
|
|
@@ -5708,6 +6018,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
5708
6018
|
state: {
|
|
5709
6019
|
...(suppliedContextOnly ? {} : priorState),
|
|
5710
6020
|
userPreferences,
|
|
6021
|
+
...(userContext ? { userContext } : {}),
|
|
5711
6022
|
...metadataBuildState(meta, priorMeta),
|
|
5712
6023
|
// The client-declared surface: the portal and AG-UI plugins turn
|
|
5713
6024
|
// these into tools at build time (`getRequestTools` reads
|
|
@@ -5791,6 +6102,9 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
5791
6102
|
],
|
|
5792
6103
|
config: { did: req.identity.userDid },
|
|
5793
6104
|
client: req.client,
|
|
6105
|
+
// First contact's gathered context is checkpointed with the turn,
|
|
6106
|
+
// every slot present; later turns write nothing and keep it.
|
|
6107
|
+
...(userContext ? { userContext } : {}),
|
|
5794
6108
|
...metadataGraphInput(meta, priorMeta),
|
|
5795
6109
|
...surface.input,
|
|
5796
6110
|
};
|
|
@@ -68,6 +68,8 @@ export interface SessionHistoryIndexerDeps {
|
|
|
68
68
|
): Promise<string>;
|
|
69
69
|
};
|
|
70
70
|
speakerLabels(roomId: string): Promise<SpeakerLabels>;
|
|
71
|
+
/** Called after the engine accepted an upload for the room. */
|
|
72
|
+
onIndexed?(roomId: string): void;
|
|
71
73
|
logger: Logger;
|
|
72
74
|
fetchImpl?: typeof fetch;
|
|
73
75
|
/** Test hooks. */
|
|
@@ -316,6 +318,7 @@ export class SessionHistoryIndexer {
|
|
|
316
318
|
`memory engine rejected the conversation (${res.status}): ${text.slice(0, 200)}`,
|
|
317
319
|
);
|
|
318
320
|
}
|
|
321
|
+
deps.onIndexed?.(roomId);
|
|
319
322
|
}
|
|
320
323
|
|
|
321
324
|
const newCount = lastProcessedCount + newMessages.length;
|