@ixo/oracle-runtime-workers 0.38.0 → 0.40.1
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 +5 -4
- package/src/action-interactions/routes.ts +78 -0
- package/src/action-interactions/store.ts +1038 -0
- package/src/channels/replies.ts +8 -4
- package/src/core/capability-expiry.ts +62 -0
- package/src/core/capability-router.ts +126 -95
- package/src/core/context-budget.ts +116 -4
- package/src/core/domain-context/document.ts +74 -2
- package/src/core/domain-context/index.ts +1 -0
- package/src/core/domain-context/resolver.ts +9 -6
- package/src/core/domain-context/turn-context.ts +62 -13
- package/src/core/domain-context/types.ts +17 -0
- package/src/core/env.ts +21 -2
- package/src/core/index.ts +4 -0
- package/src/core/llm.ts +29 -0
- package/src/core/main-agent-types.ts +45 -4
- package/src/core/main-agent.ts +210 -122
- package/src/core/manifest.ts +109 -24
- package/src/core/mcp-result.ts +56 -0
- package/src/core/meta-tools.ts +258 -110
- package/src/core/middlewares/capability-gate.ts +77 -28
- package/src/core/middlewares/context-guard.ts +154 -21
- package/src/core/middlewares/result-cap.ts +1 -1
- package/src/core/middlewares/summarization.ts +9 -0
- package/src/core/middlewares/tool-marks.ts +11 -2
- package/src/core/middlewares/tool-repetition-guard.ts +61 -1
- package/src/core/middlewares/turn-boundary.ts +22 -12
- package/src/core/operating-guides.ts +126 -0
- package/src/core/plugins/skills/skills-tools.ts +6 -31
- package/src/core/plugins/skills/skills.plugin.ts +4 -3
- package/src/core/plugins/weather/weather.plugin.ts +1 -0
- package/src/core/prompt-composer.ts +188 -111
- package/src/core/read-result-tool.ts +48 -14
- package/src/core/registries.ts +13 -5
- package/src/core/state.ts +4 -3
- package/src/core/sub-agent-fallback.ts +1 -0
- package/src/core/subagent-as-tool.ts +4 -1
- package/src/core/tool-access.ts +3 -32
- package/src/core/wrap-plugin-tool.ts +2 -1
- package/src/do/contracts.ts +45 -1
- package/src/do/transcript.ts +40 -11
- package/src/do/turn-metadata.ts +16 -4
- package/src/do/user-oracle-do.ts +358 -62
- package/src/index.ts +1 -0
- package/src/memory/session-history-indexer.ts +60 -14
- package/src/plugin-api/oracle-plugin.ts +4 -5
- package/src/plugin-api/types.ts +32 -3
- package/src/plugins/action-proposals/action-proposals.plugin.ts +7 -2
- package/src/plugins/agui/agui.plugin.ts +1 -0
- package/src/plugins/compact-schema.ts +80 -0
- package/src/plugins/composio/composio.plugin.ts +1 -0
- package/src/plugins/domain-indexer/domain-indexer.plugin.ts +4 -3
- package/src/plugins/editor/editor.plugin.ts +4 -0
- package/src/plugins/firecrawl/firecrawl-agent.ts +2 -0
- package/src/plugins/firecrawl/firecrawl.plugin.ts +1 -0
- package/src/plugins/flows/flows.plugin.ts +37 -6
- package/src/plugins/flows/prompts.ts +1 -1
- package/src/plugins/flows/tools/authoring.ts +5 -7
- package/src/plugins/flows/tools/discovery.ts +1 -4
- package/src/plugins/flows/tools/forms.ts +2 -8
- package/src/plugins/flows/tools/linkage.ts +1 -4
- package/src/plugins/flows/tools/settings.ts +1 -4
- package/src/plugins/flows/translator.ts +16 -5
- package/src/plugins/flows/types.ts +27 -48
- package/src/plugins/hub-actions/hub-actions.plugin.ts +30 -4
- package/src/plugins/index.ts +2 -2
- package/src/plugins/ixo-chain/ixo-chain.plugin.ts +29 -5
- package/src/plugins/ixo-chain/kinds/core.ts +2 -2
- package/src/plugins/ixo-chain/kinds/types.ts +34 -5
- package/src/plugins/ixo-chain/queries.ts +10 -1
- package/src/plugins/ixo-transaction/ixo-transaction.plugin.ts +1 -0
- package/src/plugins/matrix-group-chats/index.ts +1 -0
- package/src/plugins/memory/memory-tools.ts +4 -4
- package/src/plugins/memory/memory.plugin.ts +27 -1
- package/src/plugins/memory/oracle-document-tools.ts +24 -49
- package/src/plugins/pod-creator/pod-creator.plugin.ts +1 -0
- package/src/plugins/portal/portal.plugin.ts +3 -1
- package/src/plugins/sandbox/sandbox-write-blob.ts +3 -15
- package/src/plugins/sandbox/sandbox.plugin.ts +19 -2
- package/src/plugins/tasks/manifest.ts +1 -0
- package/src/plugins/tasks/tasks-tools.ts +34 -39
- package/src/plugins/user-preferences/user-preferences-tool.ts +11 -37
- package/src/plugins/user-preferences/user-preferences.plugin.ts +4 -0
- package/src/plugins/vfs/vfs-tools.ts +6 -6
- package/src/plugins/vfs/vfs.plugin.ts +4 -5
- package/src/plugins/yoma/yoma.plugin.ts +1 -0
- package/src/shell/app.ts +241 -6
- package/src/sqlite/serialization.ts +12 -0
- package/src/sqlite/sqlite-saver.ts +17 -0
- package/src/tasks/schedule-input.ts +5 -22
package/src/do/user-oracle-do.ts
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
import { taskCardSummary } from '../plugins/tasks/tasks-tools';
|
|
2
|
+
import {
|
|
3
|
+
ActionInteractionError,
|
|
4
|
+
type CheckClaimDispatch,
|
|
5
|
+
type CheckDispatch,
|
|
6
|
+
} from '@ixo/action-interactions';
|
|
7
|
+
import {
|
|
8
|
+
ActionInteractionStore,
|
|
9
|
+
interactionOutcome,
|
|
10
|
+
type InteractionCommand,
|
|
11
|
+
} from '../action-interactions/store';
|
|
1
12
|
import {
|
|
2
13
|
CHANNEL_DELIVERY_NOTICE_KWARG,
|
|
3
14
|
channelDeliveryNote,
|
|
@@ -186,9 +197,15 @@ import {
|
|
|
186
197
|
DEFAULT_MODEL_ID,
|
|
187
198
|
} from '../core/llm';
|
|
188
199
|
import { renderTurnTimeNote } from '../core/prompt-composer';
|
|
200
|
+
import type { RegisteredManifest } from '../core/registries';
|
|
201
|
+
import {
|
|
202
|
+
TURN_GUIDE_NOTE_KWARG,
|
|
203
|
+
USAGE_CARRIED_KWARG,
|
|
204
|
+
} from '../core/operating-guides';
|
|
189
205
|
import { TURN_TIME_NOTE_KWARG, withTurnTimeNote } from '../core/turn-time-note';
|
|
190
206
|
import {
|
|
191
207
|
capabilityRouterMode,
|
|
208
|
+
preloadThreshold,
|
|
192
209
|
createCapabilityRouter,
|
|
193
210
|
shadowAgreement,
|
|
194
211
|
type CapabilityRouter,
|
|
@@ -203,7 +220,6 @@ import type {
|
|
|
203
220
|
RequestAdmissionResult,
|
|
204
221
|
RequestDisposition,
|
|
205
222
|
} from '../plugin-api/request-admission';
|
|
206
|
-
import { bootHiddenPlugins } from '../core/tool-access';
|
|
207
223
|
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
208
224
|
import type { AmbientServices, LlmAdapter } from '../core/runtime-context';
|
|
209
225
|
import { chatGptBackendFromEnv } from '../llm/byo-client';
|
|
@@ -220,6 +236,8 @@ import { parseJwk } from '../secrets/jwe';
|
|
|
220
236
|
import { WorkersSecretsService } from '../secrets/secrets-service';
|
|
221
237
|
import { createSecretsAdapter } from './secrets-adapter';
|
|
222
238
|
import {
|
|
239
|
+
EDITOR_PLUGIN_NAME,
|
|
240
|
+
editorContextActive,
|
|
223
241
|
metadataBuildState,
|
|
224
242
|
metadataGraphInput,
|
|
225
243
|
parseTurnMetadata,
|
|
@@ -348,6 +366,7 @@ import {
|
|
|
348
366
|
pageOfMessages,
|
|
349
367
|
SessionHistoryIndexer,
|
|
350
368
|
type HistoryEntry,
|
|
369
|
+
conversationReader,
|
|
351
370
|
type HistoryMessage,
|
|
352
371
|
type HistoryPage,
|
|
353
372
|
type IndexableSession,
|
|
@@ -455,6 +474,7 @@ import {
|
|
|
455
474
|
contentToText,
|
|
456
475
|
isSummarizationMessage,
|
|
457
476
|
pageThreadTranscript,
|
|
477
|
+
recentTurnTexts,
|
|
458
478
|
TranscriptCursorError,
|
|
459
479
|
transformTranscript,
|
|
460
480
|
type TranscriptPageOptions,
|
|
@@ -571,6 +591,15 @@ type UserContextSettle = (
|
|
|
571
591
|
signal: AbortSignal,
|
|
572
592
|
) => Promise<UserContextData | undefined>;
|
|
573
593
|
|
|
594
|
+
/**
|
|
595
|
+
* What the capability router sees of the thread: enough to route a follow-up
|
|
596
|
+
* (two exchanges, each turn cut short), read from a bounded number of rows so
|
|
597
|
+
* a long thread costs nothing extra.
|
|
598
|
+
*/
|
|
599
|
+
const ROUTER_RECENT_TURNS = 4;
|
|
600
|
+
const ROUTER_RECENT_TURN_CHARS = 300;
|
|
601
|
+
const ROUTER_RECENT_ROWS = 12;
|
|
602
|
+
|
|
574
603
|
/** How long a first turn waits on the user-context prefetch before saying so. */
|
|
575
604
|
const USER_CONTEXT_PROGRESS_GRACE_MS = 300;
|
|
576
605
|
const USER_CONTEXT_PROGRESS_STEP = 'Recalling your memories…';
|
|
@@ -717,6 +746,11 @@ const META_LEGACY_CLEARED = 'meta:legacyCleared';
|
|
|
717
746
|
*/
|
|
718
747
|
const META_LEGACY_UNUSABLE = 'meta:legacyUnusable';
|
|
719
748
|
const META_USER_DID = 'meta:userDid';
|
|
749
|
+
/**
|
|
750
|
+
* Set when an evicted object keeps an alarm only for action-card draft
|
|
751
|
+
* retention; a boot clears it. The alarm then prunes on the bare object.
|
|
752
|
+
*/
|
|
753
|
+
const META_RETENTION_WAKE = 'meta:interactionRetentionWake';
|
|
720
754
|
|
|
721
755
|
/** KV key of a session's context-guard counters (`GET /debug/context?session=`). */
|
|
722
756
|
const contextStatsKey = (sessionId: string): string => `ctxstats:${sessionId}`;
|
|
@@ -1672,6 +1706,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
1672
1706
|
throw new Error(`Object bound to ${stored}, not ${userDid}`);
|
|
1673
1707
|
if (!stored) await this.ctx.storage.put(META_USER_DID, userDid);
|
|
1674
1708
|
this.userDid = userDid;
|
|
1709
|
+
await this.ctx.storage.delete(META_RETENTION_WAKE);
|
|
1675
1710
|
|
|
1676
1711
|
const env = this.env;
|
|
1677
1712
|
// The gateway lazily starts on first use; make sure it is up before the
|
|
@@ -2751,6 +2786,21 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
2751
2786
|
// exists once the object booted).
|
|
2752
2787
|
const keepAliveAt = this.runs?.keepAliveDeadline(wakeAt) ?? null;
|
|
2753
2788
|
const recoveryAt = this.runs?.nextRecoveryAt() ?? null;
|
|
2789
|
+
// An evicted object (no working copy, no housekeeping deadline) with
|
|
2790
|
+
// no socket is only woken for action-card draft retention: prune on
|
|
2791
|
+
// the bare object instead of importing the owner copy, and re-arm.
|
|
2792
|
+
if (
|
|
2793
|
+
!this.db &&
|
|
2794
|
+
nextPingAt === null &&
|
|
2795
|
+
keepAliveAt === null &&
|
|
2796
|
+
recoveryAt === null &&
|
|
2797
|
+
(await this.ctx.storage.get<boolean>(META_RETENTION_WAKE)) === true
|
|
2798
|
+
) {
|
|
2799
|
+
const next = await this.bareInteractionRetention();
|
|
2800
|
+
if (next !== null) await this.requestAlarm(next);
|
|
2801
|
+
else await this.ctx.storage.delete(META_RETENTION_WAKE);
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2754
2804
|
if (this.runs && recoveryAt !== null && recoveryAt <= wakeAt + 1000) {
|
|
2755
2805
|
// A store error here must not reject the alarm: the tick below (or
|
|
2756
2806
|
// the re-arm) still has to run.
|
|
@@ -2966,6 +3016,22 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
2966
3016
|
}
|
|
2967
3017
|
}
|
|
2968
3018
|
|
|
3019
|
+
// Action-card drafts past retention: one bounded batch per tick, so a
|
|
3020
|
+
// user who stopped preparing cards still loses expired draft values.
|
|
3021
|
+
// Its follow-up is no reason to keep an idle copy either.
|
|
3022
|
+
if (this.userDid) {
|
|
3023
|
+
try {
|
|
3024
|
+
await new ActionInteractionStore(this.ctx.storage, {
|
|
3025
|
+
oracleDid: this.env.ORACLE_DID,
|
|
3026
|
+
userDid: this.userDid,
|
|
3027
|
+
}).prune();
|
|
3028
|
+
} catch (err) {
|
|
3029
|
+
console.warn(
|
|
3030
|
+
`[action-interactions] retention tick failed for ${this.userDid}: ${err instanceof Error ? err.message : String(err)}`,
|
|
3031
|
+
);
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
|
|
2969
3035
|
// Idle housekeeping: a user silent for five days loses the cached
|
|
2970
3036
|
// pages — their file is safe upstream, and we stop paying to store a
|
|
2971
3037
|
// copy. Never evict while tasks are scheduled: their runs ARE
|
|
@@ -2997,12 +3063,22 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
2997
3063
|
});
|
|
2998
3064
|
if (outcome === 'evicted') {
|
|
2999
3065
|
await this.ctx.storage.delete(META_HOUSEKEEPING_AT);
|
|
3066
|
+
// Action-card drafts outlive the working copy: their retention
|
|
3067
|
+
// still needs a wake (handled on the bare object, see alarm()).
|
|
3068
|
+
const retentionAt = await this.interactionRetentionAt();
|
|
3000
3069
|
// An attached (idle) socket still needs its heartbeat rounds.
|
|
3001
|
-
if (nextPingAt !== null)
|
|
3070
|
+
if (nextPingAt !== null)
|
|
3071
|
+
await this.requestAlarm(
|
|
3072
|
+
Math.min(nextPingAt, retentionAt ?? Infinity),
|
|
3073
|
+
);
|
|
3002
3074
|
// A request that waited on the wipe has booted the object afresh
|
|
3003
3075
|
// and armed its own deadlines (a task Start's run): keep them.
|
|
3004
|
-
else if (this.initPromise === null)
|
|
3005
|
-
|
|
3076
|
+
else if (this.initPromise === null) {
|
|
3077
|
+
if (retentionAt !== null) {
|
|
3078
|
+
await this.ctx.storage.put(META_RETENTION_WAKE, true);
|
|
3079
|
+
await this.requestAlarm(retentionAt);
|
|
3080
|
+
} else await this.ctx.storage.deleteAlarm();
|
|
3081
|
+
}
|
|
3006
3082
|
return;
|
|
3007
3083
|
}
|
|
3008
3084
|
if (outcome === 'not-current') {
|
|
@@ -3178,6 +3254,162 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
3178
3254
|
|
|
3179
3255
|
// ── RPC surface (contracts.ts UserOracleObject) ───────────────────────
|
|
3180
3256
|
|
|
3257
|
+
private async interactionStore(
|
|
3258
|
+
identity: TurnIdentity,
|
|
3259
|
+
sessionId: string,
|
|
3260
|
+
options: { recordAccess?: boolean } = {},
|
|
3261
|
+
) {
|
|
3262
|
+
await this.ready(identity, options);
|
|
3263
|
+
if (
|
|
3264
|
+
this.userDid !== identity.userDid ||
|
|
3265
|
+
!(await this.sessions?.getSession(sessionId))
|
|
3266
|
+
)
|
|
3267
|
+
throw new ActionInteractionError(404, 'not_found', 'Session not found');
|
|
3268
|
+
return new ActionInteractionStore(this.ctx.storage, {
|
|
3269
|
+
oracleDid: this.env.ORACLE_DID,
|
|
3270
|
+
userDid: identity.userDid,
|
|
3271
|
+
});
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
async readActionInteractionTask(
|
|
3275
|
+
identity: TurnIdentity,
|
|
3276
|
+
sessionId: string,
|
|
3277
|
+
taskId: string,
|
|
3278
|
+
) {
|
|
3279
|
+
return interactionOutcome(async () => {
|
|
3280
|
+
// The task scheduler needs a booted object, but an open task card polls every 15 s:
|
|
3281
|
+
// the poll is not activity, so it never keeps an idle object from being evicted.
|
|
3282
|
+
await this.interactionStore(identity, sessionId, {
|
|
3283
|
+
recordAccess: false,
|
|
3284
|
+
});
|
|
3285
|
+
const task = await this.taskScheduler?.surface.get(taskId);
|
|
3286
|
+
if (!task)
|
|
3287
|
+
throw new ActionInteractionError(404, 'not_found', 'Task not found');
|
|
3288
|
+
return JSON.stringify({ task: taskCardSummary(task) });
|
|
3289
|
+
});
|
|
3290
|
+
}
|
|
3291
|
+
|
|
3292
|
+
/**
|
|
3293
|
+
* The draft store without a boot: the object is addressed by the
|
|
3294
|
+
* caller's DID, and the stored binding is checked against it.
|
|
3295
|
+
*/
|
|
3296
|
+
private async bareInteractionStore(identity: TurnIdentity) {
|
|
3297
|
+
const bound =
|
|
3298
|
+
this.userDid ?? (await this.ctx.storage.get<string>(META_USER_DID));
|
|
3299
|
+
if (bound && bound !== identity.userDid)
|
|
3300
|
+
throw new ActionInteractionError(404, 'not_found', 'Owner not found');
|
|
3301
|
+
return new ActionInteractionStore(this.ctx.storage, {
|
|
3302
|
+
oracleDid: this.env.ORACLE_DID,
|
|
3303
|
+
userDid: identity.userDid,
|
|
3304
|
+
});
|
|
3305
|
+
}
|
|
3306
|
+
|
|
3307
|
+
async checkDispatch(identity: TurnIdentity, input: CheckDispatch) {
|
|
3308
|
+
return interactionOutcome(async () => {
|
|
3309
|
+
// Runs before every bid, claim-block and approval effect: no boot, as in checkClaimDispatch.
|
|
3310
|
+
const store = await this.bareInteractionStore(identity);
|
|
3311
|
+
return JSON.stringify(await store.checkDispatch(input));
|
|
3312
|
+
});
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3315
|
+
async checkClaimDispatch(
|
|
3316
|
+
identity: TurnIdentity,
|
|
3317
|
+
input: CheckClaimDispatch,
|
|
3318
|
+
) {
|
|
3319
|
+
return interactionOutcome(async () => {
|
|
3320
|
+
// Every Portal claim calls this, Companion user or not, so it must
|
|
3321
|
+
// not boot the object (gateway, signer, SQLite, owner-copy import):
|
|
3322
|
+
// it only reads and writes a few keys. The object is addressed by
|
|
3323
|
+
// the caller's DID; the stored binding is checked without a boot.
|
|
3324
|
+
const store = await this.bareInteractionStore(identity);
|
|
3325
|
+
return JSON.stringify(await store.checkClaimDispatch(input));
|
|
3326
|
+
});
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
/** When stored action-card drafts next need retention, if any are kept. */
|
|
3330
|
+
private async interactionRetentionAt(): Promise<number | null> {
|
|
3331
|
+
const userDid =
|
|
3332
|
+
this.userDid ?? (await this.ctx.storage.get<string>(META_USER_DID));
|
|
3333
|
+
if (!userDid) return null;
|
|
3334
|
+
return new ActionInteractionStore(this.ctx.storage, {
|
|
3335
|
+
oracleDid: this.env.ORACLE_DID,
|
|
3336
|
+
userDid,
|
|
3337
|
+
})
|
|
3338
|
+
.nextRetentionAt()
|
|
3339
|
+
.catch(() => null);
|
|
3340
|
+
}
|
|
3341
|
+
|
|
3342
|
+
/** One retention pass on an un-booted object; returns the next due time. */
|
|
3343
|
+
private async bareInteractionRetention(): Promise<number | null> {
|
|
3344
|
+
const userDid = await this.ctx.storage.get<string>(META_USER_DID);
|
|
3345
|
+
if (!userDid) return null;
|
|
3346
|
+
const store = new ActionInteractionStore(this.ctx.storage, {
|
|
3347
|
+
oracleDid: this.env.ORACLE_DID,
|
|
3348
|
+
userDid,
|
|
3349
|
+
});
|
|
3350
|
+
try {
|
|
3351
|
+
await store.prune();
|
|
3352
|
+
} catch (err) {
|
|
3353
|
+
console.warn(
|
|
3354
|
+
`[action-interactions] retention on an evicted object failed for ${userDid}: ${err instanceof Error ? err.message : String(err)}`,
|
|
3355
|
+
);
|
|
3356
|
+
return Date.now() + 60 * 60 * 1000;
|
|
3357
|
+
}
|
|
3358
|
+
const next = await store.nextRetentionAt().catch(() => null);
|
|
3359
|
+
// A backlog larger than one pass resumes a minute later.
|
|
3360
|
+
return next === null ? null : Math.max(next, Date.now() + 60_000);
|
|
3361
|
+
}
|
|
3362
|
+
|
|
3363
|
+
/** Drafts of a deleted chat, in bounded batches. */
|
|
3364
|
+
private async forgetInteractions(userDid: string, sessionId: string) {
|
|
3365
|
+
const store = new ActionInteractionStore(this.ctx.storage, {
|
|
3366
|
+
oracleDid: this.env.ORACLE_DID,
|
|
3367
|
+
userDid,
|
|
3368
|
+
});
|
|
3369
|
+
while ((await store.forgetSession(sessionId)) > 0);
|
|
3370
|
+
}
|
|
3371
|
+
|
|
3372
|
+
async actionInteraction(
|
|
3373
|
+
identity: TurnIdentity,
|
|
3374
|
+
command: InteractionCommand,
|
|
3375
|
+
) {
|
|
3376
|
+
return interactionOutcome(async () => {
|
|
3377
|
+
const store = await this.interactionStore(
|
|
3378
|
+
identity,
|
|
3379
|
+
command.input.sessionId,
|
|
3380
|
+
);
|
|
3381
|
+
if (
|
|
3382
|
+
command.kind === 'prepare' &&
|
|
3383
|
+
command.input.owner.kind === 'task' &&
|
|
3384
|
+
!(await this.taskScheduler?.surface.get(command.input.owner.taskId))
|
|
3385
|
+
)
|
|
3386
|
+
throw new ActionInteractionError(404, 'not_found', 'Task not found');
|
|
3387
|
+
return JSON.stringify(await store.command(command));
|
|
3388
|
+
});
|
|
3389
|
+
}
|
|
3390
|
+
|
|
3391
|
+
async readActionInteraction(
|
|
3392
|
+
identity: TurnIdentity,
|
|
3393
|
+
sessionId: string,
|
|
3394
|
+
id?: string,
|
|
3395
|
+
references = false,
|
|
3396
|
+
) {
|
|
3397
|
+
return interactionOutcome(async () => {
|
|
3398
|
+
// Card polls never boot the object or count as activity: they read a
|
|
3399
|
+
// few keys, so an open card neither imports an evicted owner copy nor
|
|
3400
|
+
// keeps an idle object alive. A deleted chat's drafts are deleted
|
|
3401
|
+
// with it, so no session lookup is needed to hide them.
|
|
3402
|
+
const store = await this.bareInteractionStore(identity);
|
|
3403
|
+
return JSON.stringify(
|
|
3404
|
+
await (id
|
|
3405
|
+
? store.read(id, sessionId)
|
|
3406
|
+
: references
|
|
3407
|
+
? store.listReferences(sessionId)
|
|
3408
|
+
: store.list(sessionId)),
|
|
3409
|
+
);
|
|
3410
|
+
});
|
|
3411
|
+
}
|
|
3412
|
+
|
|
3181
3413
|
async createSession(
|
|
3182
3414
|
identity: TurnIdentity,
|
|
3183
3415
|
o: { roomId?: string; sessionId?: string; markerTxnId?: string } = {},
|
|
@@ -4094,6 +4326,15 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
4094
4326
|
});
|
|
4095
4327
|
await this.ctx.storage.delete(contextStatsKey(sessionId));
|
|
4096
4328
|
await this.feedbackMarkers!.forgetSession(sessionId);
|
|
4329
|
+
// The chat's action-card drafts go with it; a dispatched claim keeps
|
|
4330
|
+
// only the tombstones that stop a duplicate.
|
|
4331
|
+
await this.forgetInteractions(identity.userDid, sessionId).catch(
|
|
4332
|
+
(err: unknown) => {
|
|
4333
|
+
console.warn(
|
|
4334
|
+
`[action-interactions] could not delete the drafts of ${sessionId}; retention removes them later: ${err instanceof Error ? err.message : String(err)}`,
|
|
4335
|
+
);
|
|
4336
|
+
},
|
|
4337
|
+
);
|
|
4097
4338
|
this.markDirty();
|
|
4098
4339
|
}
|
|
4099
4340
|
// A channel binding that pointed at the session opens a new one on
|
|
@@ -4787,7 +5028,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
4787
5028
|
!isAttachmentViewMessage(m) &&
|
|
4788
5029
|
!isDeliveryNoticeMessage(m),
|
|
4789
5030
|
)
|
|
4790
|
-
.map((
|
|
5031
|
+
.map(conversationReader());
|
|
4791
5032
|
}
|
|
4792
5033
|
|
|
4793
5034
|
/**
|
|
@@ -4810,6 +5051,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
4810
5051
|
}
|
|
4811
5052
|
const pageRows = Math.max(1, limit);
|
|
4812
5053
|
const entries: HistoryEntry[] = [];
|
|
5054
|
+
const read = conversationReader();
|
|
4813
5055
|
for (;;) {
|
|
4814
5056
|
const rows = await saver.listThreadMessageRows(sessionId, {
|
|
4815
5057
|
direction: 'newer',
|
|
@@ -4826,7 +5068,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
4826
5068
|
)
|
|
4827
5069
|
continue;
|
|
4828
5070
|
entries.push({
|
|
4829
|
-
message:
|
|
5071
|
+
message: read(m),
|
|
4830
5072
|
cursor: row.messageId,
|
|
4831
5073
|
});
|
|
4832
5074
|
}
|
|
@@ -5021,6 +5263,8 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
5021
5263
|
prior: unknown;
|
|
5022
5264
|
client: TurnRequest['client'];
|
|
5023
5265
|
roomKind: TurnRequest['roomKind'];
|
|
5266
|
+
/** The provider's cache has expired, so the prompt may change. */
|
|
5267
|
+
coldTurn: boolean;
|
|
5024
5268
|
}): UserContextSettle | null {
|
|
5025
5269
|
// A first contact that fetches nothing still prints the one
|
|
5026
5270
|
// first-contact line (see the settle step below), with its reason, so
|
|
@@ -5075,9 +5319,11 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
5075
5319
|
// room answer — but only one already in the cache: an upload that
|
|
5076
5320
|
// just invalidated the room must not make the next message wait for
|
|
5077
5321
|
// a gather. On a miss the thread keeps its context and the
|
|
5078
|
-
// (single-flight) refresh warms the cache for a later turn.
|
|
5322
|
+
// (single-flight) refresh warms the cache for a later turn. The
|
|
5323
|
+
// answer sits in the system prompt, so it waits for a cold turn: a
|
|
5324
|
+
// change there re-reads the whole request at the uncached price.
|
|
5079
5325
|
const warm =
|
|
5080
|
-
input.client === 'channel'
|
|
5326
|
+
input.client === 'channel' && input.coldTurn
|
|
5081
5327
|
? fetcher.readCached(input.roomId)
|
|
5082
5328
|
: undefined;
|
|
5083
5329
|
keepGathering();
|
|
@@ -7178,13 +7424,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
7178
7424
|
sessionId: string,
|
|
7179
7425
|
): Promise<SessionContextStatus | null> {
|
|
7180
7426
|
if (!(await this.sessions!.getSession(sessionId))) return null;
|
|
7181
|
-
const
|
|
7182
|
-
configurable: { thread_id: sessionId },
|
|
7183
|
-
});
|
|
7184
|
-
const channel: unknown = tuple?.checkpoint.channel_values.messages;
|
|
7185
|
-
const context: BaseMessage[] = Array.isArray(channel)
|
|
7186
|
-
? channel.filter(isBaseMessage)
|
|
7187
|
-
: [];
|
|
7427
|
+
const context = await this.contextMessages(sessionId);
|
|
7188
7428
|
const thread = await this.saver!.listThreadMessages(sessionId);
|
|
7189
7429
|
const stats = await this.ctx.storage.get<SessionContextCounters>(
|
|
7190
7430
|
contextStatsKey(sessionId),
|
|
@@ -7204,6 +7444,15 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
7204
7444
|
};
|
|
7205
7445
|
}
|
|
7206
7446
|
|
|
7447
|
+
/** The messages the session's model calls start from (a summary, then the kept tail, once condensed). */
|
|
7448
|
+
private async contextMessages(sessionId: string): Promise<BaseMessage[]> {
|
|
7449
|
+
const tuple = await this.saver!.getTuple({
|
|
7450
|
+
configurable: { thread_id: sessionId },
|
|
7451
|
+
});
|
|
7452
|
+
const channel: unknown = tuple?.checkpoint.channel_values.messages;
|
|
7453
|
+
return Array.isArray(channel) ? channel.filter(isBaseMessage) : [];
|
|
7454
|
+
}
|
|
7455
|
+
|
|
7207
7456
|
/** Fold a guard event into the session's stored counters (ordered, never throws). */
|
|
7208
7457
|
private recordContextEvent(
|
|
7209
7458
|
sessionId: string,
|
|
@@ -7222,7 +7471,8 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
7222
7471
|
switch (event.kind) {
|
|
7223
7472
|
case 'prune':
|
|
7224
7473
|
next.prunes += 1;
|
|
7225
|
-
if (event.stage
|
|
7474
|
+
if (event.stage === 'hard' || event.stage === 'overflow')
|
|
7475
|
+
next.hardPrunes += 1;
|
|
7226
7476
|
next.prunedResults += event.pruned;
|
|
7227
7477
|
break;
|
|
7228
7478
|
case 'overflow-retry':
|
|
@@ -7721,6 +7971,13 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
7721
7971
|
configurable: { thread_id: req.sessionId },
|
|
7722
7972
|
});
|
|
7723
7973
|
const priorState = existing?.checkpoint.channel_values ?? {};
|
|
7974
|
+
// A new thread, or quiet for longer than the provider's cache lives:
|
|
7975
|
+
// this turn pays a miss whatever it sends, so it is the one to change
|
|
7976
|
+
// the prefix on.
|
|
7977
|
+
const coldTurn =
|
|
7978
|
+
!existing ||
|
|
7979
|
+
turnAt.getTime() - Date.parse(existing.checkpoint.ts) >=
|
|
7980
|
+
contextBudget.cacheColdMs;
|
|
7724
7981
|
// What the memory engine knows about the user, for the prompt's
|
|
7725
7982
|
// context block: started now so the round-trip overlaps the rest of
|
|
7726
7983
|
// the build, settled right before the agent is built.
|
|
@@ -7734,6 +7991,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
7734
7991
|
prior: priorState.userContext,
|
|
7735
7992
|
client: req.client,
|
|
7736
7993
|
roomKind: req.roomKind,
|
|
7994
|
+
coldTurn,
|
|
7737
7995
|
});
|
|
7738
7996
|
|
|
7739
7997
|
// A Matrix turn with no usable delegation: ask the web app to open its
|
|
@@ -7862,11 +8120,6 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
7862
8120
|
suppliedContextOnly ? {} : priorState,
|
|
7863
8121
|
);
|
|
7864
8122
|
|
|
7865
|
-
// Capability router: predict the on-demand plugin this message needs
|
|
7866
|
-
// and preload it for THIS turn only. `on` is awaited here, ahead of the
|
|
7867
|
-
// first model call; `shadow` returns at once and logs its verdict when
|
|
7868
|
-
// it lands. The preload never enters `stateInput` — the checkpointed
|
|
7869
|
-
// `loadedPlugins` channel is written by `load_capability` alone.
|
|
7870
8123
|
const priorLoaded: ReadonlySet<string> = new Set(
|
|
7871
8124
|
priorMeta.loadedPlugins ?? [],
|
|
7872
8125
|
);
|
|
@@ -7885,45 +8138,49 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
7885
8138
|
client: req.client,
|
|
7886
8139
|
env: langsmithEnvFromWorkerEnv(this.env),
|
|
7887
8140
|
});
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
|
|
7891
|
-
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
|
|
7895
|
-
|
|
7896
|
-
|
|
7897
|
-
|
|
7898
|
-
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
|
|
7902
|
-
|
|
8141
|
+
// Capability router: from what is left to load this turn (the build
|
|
8142
|
+
// works that out from the turn's tools), the plugins this message needs.
|
|
8143
|
+
// `on` is awaited in the build, ahead of the first model call, and the
|
|
8144
|
+
// plugins load on the thread like any other; `shadow` returns at once
|
|
8145
|
+
// and logs its verdict when it lands. A resumed attempt keeps what the
|
|
8146
|
+
// run started with.
|
|
8147
|
+
const router = this.capabilityRouter;
|
|
8148
|
+
const routeCapabilities =
|
|
8149
|
+
!suppliedContextOnly && !run.resumed && router
|
|
8150
|
+
? (candidates: readonly RegisteredManifest[]) =>
|
|
8151
|
+
router({
|
|
8152
|
+
mode: capabilityRouterMode(core.validatedEnv.CAPABILITY_ROUTER),
|
|
8153
|
+
candidates,
|
|
8154
|
+
...preloadThreshold(coldTurn, core.validatedEnv),
|
|
8155
|
+
text: body.message,
|
|
8156
|
+
recentTurns: async () =>
|
|
8157
|
+
recentTurnTexts(
|
|
8158
|
+
await saver.listRecentThreadMessages(
|
|
8159
|
+
req.sessionId,
|
|
8160
|
+
ROUTER_RECENT_ROWS,
|
|
7903
8161
|
),
|
|
7904
|
-
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
|
|
7909
|
-
|
|
7910
|
-
|
|
7911
|
-
|
|
7912
|
-
|
|
7913
|
-
|
|
7914
|
-
|
|
7915
|
-
|
|
7916
|
-
|
|
7917
|
-
|
|
8162
|
+
ROUTER_RECENT_TURNS,
|
|
8163
|
+
ROUTER_RECENT_TURN_CHARS,
|
|
8164
|
+
),
|
|
8165
|
+
requestId: req.requestId,
|
|
8166
|
+
signal: abortController.signal,
|
|
8167
|
+
trace: {
|
|
8168
|
+
...(tracing.callbacks && { callbacks: tracing.callbacks }),
|
|
8169
|
+
// `thread_id` matches the graph run's, so LangSmith's thread
|
|
8170
|
+
// view groups the router span with the turn.
|
|
8171
|
+
metadata: {
|
|
8172
|
+
...tracing.metadata,
|
|
8173
|
+
thread_id: req.sessionId,
|
|
8174
|
+
request_id: req.requestId,
|
|
8175
|
+
},
|
|
7918
8176
|
},
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
|
|
7924
|
-
|
|
7925
|
-
|
|
7926
|
-
})
|
|
8177
|
+
onShadowVerdict: (wouldPreload) =>
|
|
8178
|
+
this.shadowRoutes.set(req.sessionId, {
|
|
8179
|
+
requestId: req.requestId,
|
|
8180
|
+
priorLoaded,
|
|
8181
|
+
wouldPreload,
|
|
8182
|
+
}),
|
|
8183
|
+
})
|
|
7927
8184
|
: undefined;
|
|
7928
8185
|
|
|
7929
8186
|
// Host page-context / safety-guardrail hooks, resolved against this
|
|
@@ -8094,13 +8351,44 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
8094
8351
|
)
|
|
8095
8352
|
: undefined;
|
|
8096
8353
|
|
|
8354
|
+
// On a cold turn the thread's history shows which capabilities it has
|
|
8355
|
+
// moved on from. The messages are read only then, and only for a
|
|
8356
|
+
// thread with something to unload; the editor stays while its context
|
|
8357
|
+
// is active, and a resumed attempt keeps what the run started with.
|
|
8358
|
+
const unloadable = (priorMeta.loadedPlugins ?? []).filter(
|
|
8359
|
+
(plugin) =>
|
|
8360
|
+
plugin !== EDITOR_PLUGIN_NAME ||
|
|
8361
|
+
!editorContextActive(meta, priorMeta),
|
|
8362
|
+
);
|
|
8363
|
+
const capabilityExpiry =
|
|
8364
|
+
coldTurn &&
|
|
8365
|
+
!run.resumed &&
|
|
8366
|
+
contextBudget.pluginIdleTurns > 0 &&
|
|
8367
|
+
unloadable.length > 0
|
|
8368
|
+
? {
|
|
8369
|
+
loaded: unloadable,
|
|
8370
|
+
maxIdleTurns: contextBudget.pluginIdleTurns,
|
|
8371
|
+
messages: await this.contextMessages(req.sessionId),
|
|
8372
|
+
}
|
|
8373
|
+
: undefined;
|
|
8374
|
+
|
|
8097
8375
|
const built = await createMainAgent({
|
|
8098
8376
|
executionProfile: req.executionProfile,
|
|
8377
|
+
...(capabilityExpiry ? { capabilityExpiry } : {}),
|
|
8099
8378
|
registries: core.registries,
|
|
8100
8379
|
identity: core.identity,
|
|
8101
8380
|
config: core.validatedEnv,
|
|
8102
8381
|
availablePlugins: core.availablePlugins,
|
|
8103
|
-
|
|
8382
|
+
...(routeCapabilities ? { routeCapabilities } : {}),
|
|
8383
|
+
promptCache: {
|
|
8384
|
+
cold: coldTurn && !run.resumed,
|
|
8385
|
+
...(priorMeta.promptGuides
|
|
8386
|
+
? { promptGuides: priorMeta.promptGuides }
|
|
8387
|
+
: {}),
|
|
8388
|
+
...(priorMeta.capabilityUsage
|
|
8389
|
+
? { capabilityUsage: priorMeta.capabilityUsage }
|
|
8390
|
+
: {}),
|
|
8391
|
+
},
|
|
8104
8392
|
byoProvider: byoTurn?.provider,
|
|
8105
8393
|
contextBudget,
|
|
8106
8394
|
turnBudget: budget,
|
|
@@ -8224,7 +8512,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
8224
8512
|
checkpointer: saver,
|
|
8225
8513
|
abortSignal: abortController.signal,
|
|
8226
8514
|
});
|
|
8227
|
-
const { agent, context } = built;
|
|
8515
|
+
const { agent, context, guideNote } = built;
|
|
8228
8516
|
for (const [name, effect] of built.toolEffects)
|
|
8229
8517
|
toolEffects.set(name, effect);
|
|
8230
8518
|
for (const [name, surface] of built.toolSurfaces)
|
|
@@ -8277,9 +8565,10 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
8277
8565
|
const referenced = replyNote
|
|
8278
8566
|
? withTurnTimeNote(noticed, replyNote)
|
|
8279
8567
|
: noticed;
|
|
8280
|
-
const
|
|
8281
|
-
? withTurnTimeNote(referenced,
|
|
8568
|
+
const guided = guideNote
|
|
8569
|
+
? withTurnTimeNote(referenced, guideNote.text)
|
|
8282
8570
|
: referenced;
|
|
8571
|
+
const content = timeNote ? withTurnTimeNote(guided, timeNote) : guided;
|
|
8283
8572
|
const human = new HumanMessage({
|
|
8284
8573
|
content,
|
|
8285
8574
|
additional_kwargs: {
|
|
@@ -8289,6 +8578,12 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
8289
8578
|
...(noticeNote
|
|
8290
8579
|
? { [CHANNEL_DELIVERY_NOTICE_KWARG]: noticeNote }
|
|
8291
8580
|
: {}),
|
|
8581
|
+
...(guideNote
|
|
8582
|
+
? {
|
|
8583
|
+
[TURN_GUIDE_NOTE_KWARG]: guideNote.text,
|
|
8584
|
+
[USAGE_CARRIED_KWARG]: guideNote.plugins,
|
|
8585
|
+
}
|
|
8586
|
+
: {}),
|
|
8292
8587
|
oracleName: core.identity.name,
|
|
8293
8588
|
msgFromMatrixRoom: req.client === 'matrix',
|
|
8294
8589
|
...speakerKwargs,
|
|
@@ -8319,6 +8614,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
8319
8614
|
// every slot present; later turns write nothing and keep it.
|
|
8320
8615
|
...(userContext ? { userContext } : {}),
|
|
8321
8616
|
...metadataGraphInput(meta, priorMeta),
|
|
8617
|
+
...built.stateUpdate,
|
|
8322
8618
|
...surface.input,
|
|
8323
8619
|
};
|
|
8324
8620
|
const config = {
|