@ixo/oracle-runtime-workers 0.38.0 → 0.39.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 +2 -2
- 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/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 +13 -1
- package/src/do/transcript.ts +40 -11
- package/src/do/turn-metadata.ts +16 -4
- package/src/do/user-oracle-do.ts +132 -59
- 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 +1 -0
- 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 +14 -19
- 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/sqlite/serialization.ts +12 -0
- package/src/sqlite/sqlite-saver.ts +17 -0
- package/src/tasks/schedule-input.ts +5 -22
package/src/core/tool-access.ts
CHANGED
|
@@ -6,13 +6,8 @@ import type {
|
|
|
6
6
|
PluginTool,
|
|
7
7
|
} from '../plugin-api/types';
|
|
8
8
|
import type { CapabilityRequirement } from './manifest';
|
|
9
|
-
import type {
|
|
10
|
-
RegisteredSubAgent,
|
|
11
|
-
SubAgentRegistry,
|
|
12
|
-
ToolRegistry,
|
|
13
|
-
} from './registries';
|
|
9
|
+
import type { RegisteredSubAgent } from './registries';
|
|
14
10
|
import { computeSubAgentToolName } from './subagent-as-tool';
|
|
15
|
-
import { NOOP_LOGGER } from './utils';
|
|
16
11
|
|
|
17
12
|
/** `ctx.ucan.hasCapability` bound to the turn's delegation. */
|
|
18
13
|
export type HasCapability = (resource: string, action: string) => boolean;
|
|
@@ -53,8 +48,8 @@ export interface TurnToolAccess<
|
|
|
53
48
|
/**
|
|
54
49
|
* Plugins that contributed tools or sub-agents of which none is left.
|
|
55
50
|
* Treated like a plugin whose `requires` are unmet (left out of the
|
|
56
|
-
* prompt, refused by `load_capability`,
|
|
57
|
-
* are hidden outright: `list_capabilities` does not list them.
|
|
51
|
+
* prompt, refused by `load_capability`, not offered to the router) except
|
|
52
|
+
* that they are hidden outright: `list_capabilities` does not list them.
|
|
58
53
|
*/
|
|
59
54
|
hiddenPlugins: ReadonlySet<string>;
|
|
60
55
|
}
|
|
@@ -145,30 +140,6 @@ export function resolveTurnToolAccess<
|
|
|
145
140
|
return { tools, subAgents, withheldToolNames, hiddenPlugins };
|
|
146
141
|
}
|
|
147
142
|
|
|
148
|
-
/**
|
|
149
|
-
* `hiddenPlugins` from the boot-time contributions alone, for the
|
|
150
|
-
* capability router, which predicts before the turn's request-time tools are
|
|
151
|
-
* collected. A plugin whose request-time tools would make it usable may be
|
|
152
|
-
* left out here (the router does not offer it; `load_capability` still
|
|
153
|
-
* can); one whose request-time tools are all withheld may be offered, and
|
|
154
|
-
* the turn's build voids that preload (`createMainAgent`).
|
|
155
|
-
*/
|
|
156
|
-
export async function bootHiddenPlugins(input: {
|
|
157
|
-
registries: { tools: ToolRegistry; subAgents: SubAgentRegistry };
|
|
158
|
-
buildCtx: PluginContext;
|
|
159
|
-
has: HasCapability;
|
|
160
|
-
}): Promise<ReadonlySet<string>> {
|
|
161
|
-
const { registries, buildCtx, has } = input;
|
|
162
|
-
return resolveTurnToolAccess({
|
|
163
|
-
tools: await registries.tools.collectBoot(buildCtx),
|
|
164
|
-
subAgents: registries.subAgents.collectBoot(buildCtx),
|
|
165
|
-
buildCtx,
|
|
166
|
-
has,
|
|
167
|
-
// A failing sub-agent is reported by the turn's build, once.
|
|
168
|
-
logger: NOOP_LOGGER,
|
|
169
|
-
}).hiddenPlugins;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
143
|
/**
|
|
173
144
|
* The manifest as this turn may show it: `examples` naming a withheld tool
|
|
174
145
|
* removed, so neither the Tier-1 prompt nor `load_capability` teaches the
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ToolMessage } from '@langchain/core/messages';
|
|
2
2
|
import { Command } from '@langchain/langgraph';
|
|
3
|
+
import { withoutStructuredCopy } from './mcp-result';
|
|
3
4
|
import { capToolResult, type ResultCapConfig } from './middlewares/result-cap';
|
|
4
5
|
import { tool } from '@langchain/core/tools';
|
|
5
6
|
import type { StructuredTool } from 'langchain';
|
|
@@ -125,7 +126,7 @@ export function wrapPluginTool(
|
|
|
125
126
|
);
|
|
126
127
|
if (pluginName !== undefined)
|
|
127
128
|
requireToolPlane(ctx, pluginName, pluginTool);
|
|
128
|
-
const output = await pluginTool.handler(args, ctx);
|
|
129
|
+
const output = withoutStructuredCopy(await pluginTool.handler(args, ctx));
|
|
129
130
|
// A tool's own message (a result that says the tool did not run) is
|
|
130
131
|
// returned whole, never capped into text.
|
|
131
132
|
if (
|
package/src/do/contracts.ts
CHANGED
|
@@ -271,6 +271,8 @@ export interface OracleWorkerEnv {
|
|
|
271
271
|
LLM_STREAM_IDLE_TIMEOUT_MS?: string;
|
|
272
272
|
LLM_BYO_STREAM_IDLE_TIMEOUT_MS?: string;
|
|
273
273
|
LLM_STREAM_RETRIES?: string;
|
|
274
|
+
/** Most tokens one platform model call may generate, reasoning included (default 16384; 0 = the model's limit). */
|
|
275
|
+
LLM_MAX_OUTPUT_TOKENS?: string;
|
|
274
276
|
/** LangGraph steps one turn may take before `GraphRecursionError` (default 600; Node hard-codes 200). */
|
|
275
277
|
TURN_RECURSION_LIMIT?: string;
|
|
276
278
|
/** Turn budget: tool attempts (default 120) and deadline in ms (600000). */
|
|
@@ -295,7 +297,12 @@ export interface OracleWorkerEnv {
|
|
|
295
297
|
MODEL_CONTEXT_TOKENS?: string;
|
|
296
298
|
MODEL_CONTEXT_OVERRIDES?: string;
|
|
297
299
|
CONTEXT_SUMMARIZE_FRACTION?: string;
|
|
300
|
+
CONTEXT_SUMMARIZE_MAX_TOKENS?: string;
|
|
298
301
|
CONTEXT_PRUNE_FRACTION?: string;
|
|
302
|
+
CONTEXT_PRUNE_MAX_TOKENS?: string;
|
|
303
|
+
CONTEXT_STALE_RESULT_TURNS?: string;
|
|
304
|
+
CONTEXT_CACHE_COLD_MS?: string;
|
|
305
|
+
CONTEXT_PLUGIN_IDLE_TURNS?: string;
|
|
299
306
|
CONTEXT_RESULT_CAP_FRACTION?: string;
|
|
300
307
|
CONTEXT_RESULT_CAP_MAX_CHARS?: string;
|
|
301
308
|
CONTEXT_REQUEST_FRACTION?: string;
|
|
@@ -327,9 +334,14 @@ export interface OracleWorkerEnv {
|
|
|
327
334
|
/**
|
|
328
335
|
* Capability router (needs a `DECISION_PROVIDER`). `off` (default) never
|
|
329
336
|
* evaluates; `shadow` evaluates and logs the verdict without preloading;
|
|
330
|
-
* `on`
|
|
337
|
+
* `on` loads the predicted plugins on the thread before the first model
|
|
338
|
+
* call.
|
|
331
339
|
*/
|
|
332
340
|
CAPABILITY_ROUTER?: CapabilityRouterMode;
|
|
341
|
+
/** The score a preload needs on a warm turn (default 0.8; above 1, never). */
|
|
342
|
+
CAPABILITY_ROUTER_WARM_THRESHOLD?: string;
|
|
343
|
+
/** The score a preload needs on a cold turn (default 0.7). */
|
|
344
|
+
CAPABILITY_ROUTER_COLD_THRESHOLD?: string;
|
|
333
345
|
/** Applies exclusively to authenticated channel admission; Portal/Matrix turns never route. */
|
|
334
346
|
CHANNEL_SEMANTIC_ROUTING?: 'off' | 'shadow' | 'on';
|
|
335
347
|
CHANNEL_SEMANTIC_ROUTING_BINDINGS?: string;
|
package/src/do/transcript.ts
CHANGED
|
@@ -140,6 +140,45 @@ export function visibleText(message: BaseMessage): string {
|
|
|
140
140
|
return contentToText(visibleContent(message));
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Whether a stored message is one the user sees in the transcript. System and
|
|
145
|
+
* tool messages never are, nor a failed reply, nor anything the runtime wrote
|
|
146
|
+
* for the model about an attachment: the extracted text of a file (an AI
|
|
147
|
+
* message with an `attachment`) and `view_attachment` re-attaching a payload,
|
|
148
|
+
* nor a chat gateway's delivery notice.
|
|
149
|
+
*/
|
|
150
|
+
export function visibleToUser(message: BaseMessage): boolean {
|
|
151
|
+
if (message.type === 'system' || message.type === 'tool') return false;
|
|
152
|
+
const kw = (message.additional_kwargs ?? {}) as Record<string, unknown>;
|
|
153
|
+
if (kw.isError) return false;
|
|
154
|
+
if (message.type === 'ai' && Boolean(kw.attachment)) return false;
|
|
155
|
+
// A chat gateway's delivery notice: the model's, never a user message.
|
|
156
|
+
return !isAttachmentViewMessage(message) && !isDeliveryNoticeMessage(message);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The latest turns as the user saw them, oldest first and each as
|
|
161
|
+
* `user: …` or `assistant: …` cut to `maxChars`, from messages listed newest
|
|
162
|
+
* first. Only what `visibleToUser` lets through, without summaries and rows
|
|
163
|
+
* with no visible text.
|
|
164
|
+
*/
|
|
165
|
+
export function recentTurnTexts(
|
|
166
|
+
newestFirst: readonly BaseMessage[],
|
|
167
|
+
limit: number,
|
|
168
|
+
maxChars: number,
|
|
169
|
+
): string[] {
|
|
170
|
+
const turns: string[] = [];
|
|
171
|
+
for (const message of newestFirst) {
|
|
172
|
+
if (turns.length === limit) break;
|
|
173
|
+
if (isSummarizationMessage(message) || !visibleToUser(message)) continue;
|
|
174
|
+
const text = visibleText(message).trim();
|
|
175
|
+
if (!text) continue;
|
|
176
|
+
const speaker = message.type === 'human' ? 'user' : 'assistant';
|
|
177
|
+
turns.push(`${speaker}: ${text.slice(0, maxChars)}`);
|
|
178
|
+
}
|
|
179
|
+
return turns.reverse();
|
|
180
|
+
}
|
|
181
|
+
|
|
143
182
|
/**
|
|
144
183
|
* The summarization middleware's bookkeeping message — never shown to users.
|
|
145
184
|
* LangChain 1.4 writes it as a HUMAN message tagged
|
|
@@ -171,18 +210,8 @@ export async function transformTranscript(
|
|
|
171
210
|
for (const message of messages) {
|
|
172
211
|
const toolMsg = message.type === 'tool' ? (message as ToolMessage) : null;
|
|
173
212
|
const kw = (message.additional_kwargs ?? {}) as Record<string, unknown>;
|
|
174
|
-
const isExtractionContext = message.type === 'ai' && Boolean(kw.attachment);
|
|
175
213
|
|
|
176
|
-
if (
|
|
177
|
-
message.type !== 'system' &&
|
|
178
|
-
message.type !== 'tool' &&
|
|
179
|
-
!kw.isError &&
|
|
180
|
-
!isExtractionContext &&
|
|
181
|
-
// `view_attachment` re-attaching an offloaded payload — model-facing.
|
|
182
|
-
!isAttachmentViewMessage(message) &&
|
|
183
|
-
// A chat gateway's delivery notice — the model's, never a user message.
|
|
184
|
-
!isDeliveryNoticeMessage(message)
|
|
185
|
-
) {
|
|
214
|
+
if (visibleToUser(message)) {
|
|
186
215
|
const reasoning = reasoningToText(kw.reasoning);
|
|
187
216
|
const attachments =
|
|
188
217
|
message.type === 'human'
|
package/src/do/turn-metadata.ts
CHANGED
|
@@ -62,6 +62,16 @@ export interface PriorMetadataState {
|
|
|
62
62
|
sessionRunId?: string;
|
|
63
63
|
currentEntityDid?: string;
|
|
64
64
|
loadedPlugins?: string[];
|
|
65
|
+
/** The plugins whose operating guides the prompt carried (`operating-guides.ts`). */
|
|
66
|
+
promptGuides?: string[];
|
|
67
|
+
/** The plugins whose usage the thread's messages carry (`operating-guides.ts`). */
|
|
68
|
+
capabilityUsage?: string[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function strings(value: unknown): string[] | undefined {
|
|
72
|
+
return Array.isArray(value)
|
|
73
|
+
? value.filter((entry): entry is string => typeof entry === 'string')
|
|
74
|
+
: undefined;
|
|
65
75
|
}
|
|
66
76
|
|
|
67
77
|
function priorString(
|
|
@@ -75,15 +85,17 @@ function priorString(
|
|
|
75
85
|
export function priorMetadataState(
|
|
76
86
|
prior: Record<string, unknown>,
|
|
77
87
|
): PriorMetadataState {
|
|
78
|
-
const loaded = prior['loadedPlugins'];
|
|
88
|
+
const loaded = strings(prior['loadedPlugins']);
|
|
89
|
+
const promptGuides = strings(prior['promptGuides']);
|
|
90
|
+
const capabilityUsage = strings(prior['capabilityUsage']);
|
|
79
91
|
return {
|
|
80
92
|
editorRoomId: priorString(prior, 'editorRoomId'),
|
|
81
93
|
spaceId: priorString(prior, 'spaceId'),
|
|
82
94
|
sessionRunId: priorString(prior, 'sessionRunId'),
|
|
83
95
|
currentEntityDid: priorString(prior, 'currentEntityDid'),
|
|
84
|
-
...(
|
|
85
|
-
|
|
86
|
-
|
|
96
|
+
...(loaded ? { loadedPlugins: loaded } : {}),
|
|
97
|
+
...(promptGuides ? { promptGuides } : {}),
|
|
98
|
+
...(capabilityUsage ? { capabilityUsage } : {}),
|
|
87
99
|
};
|
|
88
100
|
}
|
|
89
101
|
|
package/src/do/user-oracle-do.ts
CHANGED
|
@@ -186,9 +186,15 @@ import {
|
|
|
186
186
|
DEFAULT_MODEL_ID,
|
|
187
187
|
} from '../core/llm';
|
|
188
188
|
import { renderTurnTimeNote } from '../core/prompt-composer';
|
|
189
|
+
import type { RegisteredManifest } from '../core/registries';
|
|
190
|
+
import {
|
|
191
|
+
TURN_GUIDE_NOTE_KWARG,
|
|
192
|
+
USAGE_CARRIED_KWARG,
|
|
193
|
+
} from '../core/operating-guides';
|
|
189
194
|
import { TURN_TIME_NOTE_KWARG, withTurnTimeNote } from '../core/turn-time-note';
|
|
190
195
|
import {
|
|
191
196
|
capabilityRouterMode,
|
|
197
|
+
preloadThreshold,
|
|
192
198
|
createCapabilityRouter,
|
|
193
199
|
shadowAgreement,
|
|
194
200
|
type CapabilityRouter,
|
|
@@ -203,7 +209,6 @@ import type {
|
|
|
203
209
|
RequestAdmissionResult,
|
|
204
210
|
RequestDisposition,
|
|
205
211
|
} from '../plugin-api/request-admission';
|
|
206
|
-
import { bootHiddenPlugins } from '../core/tool-access';
|
|
207
212
|
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
208
213
|
import type { AmbientServices, LlmAdapter } from '../core/runtime-context';
|
|
209
214
|
import { chatGptBackendFromEnv } from '../llm/byo-client';
|
|
@@ -220,6 +225,8 @@ import { parseJwk } from '../secrets/jwe';
|
|
|
220
225
|
import { WorkersSecretsService } from '../secrets/secrets-service';
|
|
221
226
|
import { createSecretsAdapter } from './secrets-adapter';
|
|
222
227
|
import {
|
|
228
|
+
EDITOR_PLUGIN_NAME,
|
|
229
|
+
editorContextActive,
|
|
223
230
|
metadataBuildState,
|
|
224
231
|
metadataGraphInput,
|
|
225
232
|
parseTurnMetadata,
|
|
@@ -348,6 +355,7 @@ import {
|
|
|
348
355
|
pageOfMessages,
|
|
349
356
|
SessionHistoryIndexer,
|
|
350
357
|
type HistoryEntry,
|
|
358
|
+
conversationReader,
|
|
351
359
|
type HistoryMessage,
|
|
352
360
|
type HistoryPage,
|
|
353
361
|
type IndexableSession,
|
|
@@ -455,6 +463,7 @@ import {
|
|
|
455
463
|
contentToText,
|
|
456
464
|
isSummarizationMessage,
|
|
457
465
|
pageThreadTranscript,
|
|
466
|
+
recentTurnTexts,
|
|
458
467
|
TranscriptCursorError,
|
|
459
468
|
transformTranscript,
|
|
460
469
|
type TranscriptPageOptions,
|
|
@@ -571,6 +580,15 @@ type UserContextSettle = (
|
|
|
571
580
|
signal: AbortSignal,
|
|
572
581
|
) => Promise<UserContextData | undefined>;
|
|
573
582
|
|
|
583
|
+
/**
|
|
584
|
+
* What the capability router sees of the thread: enough to route a follow-up
|
|
585
|
+
* (two exchanges, each turn cut short), read from a bounded number of rows so
|
|
586
|
+
* a long thread costs nothing extra.
|
|
587
|
+
*/
|
|
588
|
+
const ROUTER_RECENT_TURNS = 4;
|
|
589
|
+
const ROUTER_RECENT_TURN_CHARS = 300;
|
|
590
|
+
const ROUTER_RECENT_ROWS = 12;
|
|
591
|
+
|
|
574
592
|
/** How long a first turn waits on the user-context prefetch before saying so. */
|
|
575
593
|
const USER_CONTEXT_PROGRESS_GRACE_MS = 300;
|
|
576
594
|
const USER_CONTEXT_PROGRESS_STEP = 'Recalling your memories…';
|
|
@@ -4787,7 +4805,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
4787
4805
|
!isAttachmentViewMessage(m) &&
|
|
4788
4806
|
!isDeliveryNoticeMessage(m),
|
|
4789
4807
|
)
|
|
4790
|
-
.map((
|
|
4808
|
+
.map(conversationReader());
|
|
4791
4809
|
}
|
|
4792
4810
|
|
|
4793
4811
|
/**
|
|
@@ -4810,6 +4828,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
4810
4828
|
}
|
|
4811
4829
|
const pageRows = Math.max(1, limit);
|
|
4812
4830
|
const entries: HistoryEntry[] = [];
|
|
4831
|
+
const read = conversationReader();
|
|
4813
4832
|
for (;;) {
|
|
4814
4833
|
const rows = await saver.listThreadMessageRows(sessionId, {
|
|
4815
4834
|
direction: 'newer',
|
|
@@ -4826,7 +4845,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
4826
4845
|
)
|
|
4827
4846
|
continue;
|
|
4828
4847
|
entries.push({
|
|
4829
|
-
message:
|
|
4848
|
+
message: read(m),
|
|
4830
4849
|
cursor: row.messageId,
|
|
4831
4850
|
});
|
|
4832
4851
|
}
|
|
@@ -5021,6 +5040,8 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
5021
5040
|
prior: unknown;
|
|
5022
5041
|
client: TurnRequest['client'];
|
|
5023
5042
|
roomKind: TurnRequest['roomKind'];
|
|
5043
|
+
/** The provider's cache has expired, so the prompt may change. */
|
|
5044
|
+
coldTurn: boolean;
|
|
5024
5045
|
}): UserContextSettle | null {
|
|
5025
5046
|
// A first contact that fetches nothing still prints the one
|
|
5026
5047
|
// first-contact line (see the settle step below), with its reason, so
|
|
@@ -5075,9 +5096,11 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
5075
5096
|
// room answer — but only one already in the cache: an upload that
|
|
5076
5097
|
// just invalidated the room must not make the next message wait for
|
|
5077
5098
|
// a gather. On a miss the thread keeps its context and the
|
|
5078
|
-
// (single-flight) refresh warms the cache for a later turn.
|
|
5099
|
+
// (single-flight) refresh warms the cache for a later turn. The
|
|
5100
|
+
// answer sits in the system prompt, so it waits for a cold turn: a
|
|
5101
|
+
// change there re-reads the whole request at the uncached price.
|
|
5079
5102
|
const warm =
|
|
5080
|
-
input.client === 'channel'
|
|
5103
|
+
input.client === 'channel' && input.coldTurn
|
|
5081
5104
|
? fetcher.readCached(input.roomId)
|
|
5082
5105
|
: undefined;
|
|
5083
5106
|
keepGathering();
|
|
@@ -7178,13 +7201,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
7178
7201
|
sessionId: string,
|
|
7179
7202
|
): Promise<SessionContextStatus | null> {
|
|
7180
7203
|
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
|
-
: [];
|
|
7204
|
+
const context = await this.contextMessages(sessionId);
|
|
7188
7205
|
const thread = await this.saver!.listThreadMessages(sessionId);
|
|
7189
7206
|
const stats = await this.ctx.storage.get<SessionContextCounters>(
|
|
7190
7207
|
contextStatsKey(sessionId),
|
|
@@ -7204,6 +7221,15 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
7204
7221
|
};
|
|
7205
7222
|
}
|
|
7206
7223
|
|
|
7224
|
+
/** The messages the session's model calls start from (a summary, then the kept tail, once condensed). */
|
|
7225
|
+
private async contextMessages(sessionId: string): Promise<BaseMessage[]> {
|
|
7226
|
+
const tuple = await this.saver!.getTuple({
|
|
7227
|
+
configurable: { thread_id: sessionId },
|
|
7228
|
+
});
|
|
7229
|
+
const channel: unknown = tuple?.checkpoint.channel_values.messages;
|
|
7230
|
+
return Array.isArray(channel) ? channel.filter(isBaseMessage) : [];
|
|
7231
|
+
}
|
|
7232
|
+
|
|
7207
7233
|
/** Fold a guard event into the session's stored counters (ordered, never throws). */
|
|
7208
7234
|
private recordContextEvent(
|
|
7209
7235
|
sessionId: string,
|
|
@@ -7222,7 +7248,8 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
7222
7248
|
switch (event.kind) {
|
|
7223
7249
|
case 'prune':
|
|
7224
7250
|
next.prunes += 1;
|
|
7225
|
-
if (event.stage
|
|
7251
|
+
if (event.stage === 'hard' || event.stage === 'overflow')
|
|
7252
|
+
next.hardPrunes += 1;
|
|
7226
7253
|
next.prunedResults += event.pruned;
|
|
7227
7254
|
break;
|
|
7228
7255
|
case 'overflow-retry':
|
|
@@ -7721,6 +7748,13 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
7721
7748
|
configurable: { thread_id: req.sessionId },
|
|
7722
7749
|
});
|
|
7723
7750
|
const priorState = existing?.checkpoint.channel_values ?? {};
|
|
7751
|
+
// A new thread, or quiet for longer than the provider's cache lives:
|
|
7752
|
+
// this turn pays a miss whatever it sends, so it is the one to change
|
|
7753
|
+
// the prefix on.
|
|
7754
|
+
const coldTurn =
|
|
7755
|
+
!existing ||
|
|
7756
|
+
turnAt.getTime() - Date.parse(existing.checkpoint.ts) >=
|
|
7757
|
+
contextBudget.cacheColdMs;
|
|
7724
7758
|
// What the memory engine knows about the user, for the prompt's
|
|
7725
7759
|
// context block: started now so the round-trip overlaps the rest of
|
|
7726
7760
|
// the build, settled right before the agent is built.
|
|
@@ -7734,6 +7768,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
7734
7768
|
prior: priorState.userContext,
|
|
7735
7769
|
client: req.client,
|
|
7736
7770
|
roomKind: req.roomKind,
|
|
7771
|
+
coldTurn,
|
|
7737
7772
|
});
|
|
7738
7773
|
|
|
7739
7774
|
// A Matrix turn with no usable delegation: ask the web app to open its
|
|
@@ -7862,11 +7897,6 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
7862
7897
|
suppliedContextOnly ? {} : priorState,
|
|
7863
7898
|
);
|
|
7864
7899
|
|
|
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
7900
|
const priorLoaded: ReadonlySet<string> = new Set(
|
|
7871
7901
|
priorMeta.loadedPlugins ?? [],
|
|
7872
7902
|
);
|
|
@@ -7885,45 +7915,49 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
7885
7915
|
client: req.client,
|
|
7886
7916
|
env: langsmithEnvFromWorkerEnv(this.env),
|
|
7887
7917
|
});
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
|
|
7891
|
-
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
|
|
7895
|
-
|
|
7896
|
-
|
|
7897
|
-
|
|
7898
|
-
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
|
|
7902
|
-
|
|
7918
|
+
// Capability router: from what is left to load this turn (the build
|
|
7919
|
+
// works that out from the turn's tools), the plugins this message needs.
|
|
7920
|
+
// `on` is awaited in the build, ahead of the first model call, and the
|
|
7921
|
+
// plugins load on the thread like any other; `shadow` returns at once
|
|
7922
|
+
// and logs its verdict when it lands. A resumed attempt keeps what the
|
|
7923
|
+
// run started with.
|
|
7924
|
+
const router = this.capabilityRouter;
|
|
7925
|
+
const routeCapabilities =
|
|
7926
|
+
!suppliedContextOnly && !run.resumed && router
|
|
7927
|
+
? (candidates: readonly RegisteredManifest[]) =>
|
|
7928
|
+
router({
|
|
7929
|
+
mode: capabilityRouterMode(core.validatedEnv.CAPABILITY_ROUTER),
|
|
7930
|
+
candidates,
|
|
7931
|
+
...preloadThreshold(coldTurn, core.validatedEnv),
|
|
7932
|
+
text: body.message,
|
|
7933
|
+
recentTurns: async () =>
|
|
7934
|
+
recentTurnTexts(
|
|
7935
|
+
await saver.listRecentThreadMessages(
|
|
7936
|
+
req.sessionId,
|
|
7937
|
+
ROUTER_RECENT_ROWS,
|
|
7903
7938
|
),
|
|
7904
|
-
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
|
|
7909
|
-
|
|
7910
|
-
|
|
7911
|
-
|
|
7912
|
-
|
|
7913
|
-
|
|
7914
|
-
|
|
7915
|
-
|
|
7916
|
-
|
|
7917
|
-
|
|
7939
|
+
ROUTER_RECENT_TURNS,
|
|
7940
|
+
ROUTER_RECENT_TURN_CHARS,
|
|
7941
|
+
),
|
|
7942
|
+
requestId: req.requestId,
|
|
7943
|
+
signal: abortController.signal,
|
|
7944
|
+
trace: {
|
|
7945
|
+
...(tracing.callbacks && { callbacks: tracing.callbacks }),
|
|
7946
|
+
// `thread_id` matches the graph run's, so LangSmith's thread
|
|
7947
|
+
// view groups the router span with the turn.
|
|
7948
|
+
metadata: {
|
|
7949
|
+
...tracing.metadata,
|
|
7950
|
+
thread_id: req.sessionId,
|
|
7951
|
+
request_id: req.requestId,
|
|
7952
|
+
},
|
|
7918
7953
|
},
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
|
|
7924
|
-
|
|
7925
|
-
|
|
7926
|
-
})
|
|
7954
|
+
onShadowVerdict: (wouldPreload) =>
|
|
7955
|
+
this.shadowRoutes.set(req.sessionId, {
|
|
7956
|
+
requestId: req.requestId,
|
|
7957
|
+
priorLoaded,
|
|
7958
|
+
wouldPreload,
|
|
7959
|
+
}),
|
|
7960
|
+
})
|
|
7927
7961
|
: undefined;
|
|
7928
7962
|
|
|
7929
7963
|
// Host page-context / safety-guardrail hooks, resolved against this
|
|
@@ -8094,13 +8128,44 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
8094
8128
|
)
|
|
8095
8129
|
: undefined;
|
|
8096
8130
|
|
|
8131
|
+
// On a cold turn the thread's history shows which capabilities it has
|
|
8132
|
+
// moved on from. The messages are read only then, and only for a
|
|
8133
|
+
// thread with something to unload; the editor stays while its context
|
|
8134
|
+
// is active, and a resumed attempt keeps what the run started with.
|
|
8135
|
+
const unloadable = (priorMeta.loadedPlugins ?? []).filter(
|
|
8136
|
+
(plugin) =>
|
|
8137
|
+
plugin !== EDITOR_PLUGIN_NAME ||
|
|
8138
|
+
!editorContextActive(meta, priorMeta),
|
|
8139
|
+
);
|
|
8140
|
+
const capabilityExpiry =
|
|
8141
|
+
coldTurn &&
|
|
8142
|
+
!run.resumed &&
|
|
8143
|
+
contextBudget.pluginIdleTurns > 0 &&
|
|
8144
|
+
unloadable.length > 0
|
|
8145
|
+
? {
|
|
8146
|
+
loaded: unloadable,
|
|
8147
|
+
maxIdleTurns: contextBudget.pluginIdleTurns,
|
|
8148
|
+
messages: await this.contextMessages(req.sessionId),
|
|
8149
|
+
}
|
|
8150
|
+
: undefined;
|
|
8151
|
+
|
|
8097
8152
|
const built = await createMainAgent({
|
|
8098
8153
|
executionProfile: req.executionProfile,
|
|
8154
|
+
...(capabilityExpiry ? { capabilityExpiry } : {}),
|
|
8099
8155
|
registries: core.registries,
|
|
8100
8156
|
identity: core.identity,
|
|
8101
8157
|
config: core.validatedEnv,
|
|
8102
8158
|
availablePlugins: core.availablePlugins,
|
|
8103
|
-
|
|
8159
|
+
...(routeCapabilities ? { routeCapabilities } : {}),
|
|
8160
|
+
promptCache: {
|
|
8161
|
+
cold: coldTurn && !run.resumed,
|
|
8162
|
+
...(priorMeta.promptGuides
|
|
8163
|
+
? { promptGuides: priorMeta.promptGuides }
|
|
8164
|
+
: {}),
|
|
8165
|
+
...(priorMeta.capabilityUsage
|
|
8166
|
+
? { capabilityUsage: priorMeta.capabilityUsage }
|
|
8167
|
+
: {}),
|
|
8168
|
+
},
|
|
8104
8169
|
byoProvider: byoTurn?.provider,
|
|
8105
8170
|
contextBudget,
|
|
8106
8171
|
turnBudget: budget,
|
|
@@ -8224,7 +8289,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
8224
8289
|
checkpointer: saver,
|
|
8225
8290
|
abortSignal: abortController.signal,
|
|
8226
8291
|
});
|
|
8227
|
-
const { agent, context } = built;
|
|
8292
|
+
const { agent, context, guideNote } = built;
|
|
8228
8293
|
for (const [name, effect] of built.toolEffects)
|
|
8229
8294
|
toolEffects.set(name, effect);
|
|
8230
8295
|
for (const [name, surface] of built.toolSurfaces)
|
|
@@ -8277,9 +8342,10 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
8277
8342
|
const referenced = replyNote
|
|
8278
8343
|
? withTurnTimeNote(noticed, replyNote)
|
|
8279
8344
|
: noticed;
|
|
8280
|
-
const
|
|
8281
|
-
? withTurnTimeNote(referenced,
|
|
8345
|
+
const guided = guideNote
|
|
8346
|
+
? withTurnTimeNote(referenced, guideNote.text)
|
|
8282
8347
|
: referenced;
|
|
8348
|
+
const content = timeNote ? withTurnTimeNote(guided, timeNote) : guided;
|
|
8283
8349
|
const human = new HumanMessage({
|
|
8284
8350
|
content,
|
|
8285
8351
|
additional_kwargs: {
|
|
@@ -8289,6 +8355,12 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
8289
8355
|
...(noticeNote
|
|
8290
8356
|
? { [CHANNEL_DELIVERY_NOTICE_KWARG]: noticeNote }
|
|
8291
8357
|
: {}),
|
|
8358
|
+
...(guideNote
|
|
8359
|
+
? {
|
|
8360
|
+
[TURN_GUIDE_NOTE_KWARG]: guideNote.text,
|
|
8361
|
+
[USAGE_CARRIED_KWARG]: guideNote.plugins,
|
|
8362
|
+
}
|
|
8363
|
+
: {}),
|
|
8292
8364
|
oracleName: core.identity.name,
|
|
8293
8365
|
msgFromMatrixRoom: req.client === 'matrix',
|
|
8294
8366
|
...speakerKwargs,
|
|
@@ -8319,6 +8391,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
8319
8391
|
// every slot present; later turns write nothing and keep it.
|
|
8320
8392
|
...(userContext ? { userContext } : {}),
|
|
8321
8393
|
...metadataGraphInput(meta, priorMeta),
|
|
8394
|
+
...built.stateUpdate,
|
|
8322
8395
|
...surface.input,
|
|
8323
8396
|
};
|
|
8324
8397
|
const config = {
|