@ixo/oracle-runtime-workers 0.37.2 → 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 +4 -4
- package/src/attachments/channel-links.ts +74 -7
- package/src/attachments/channel.ts +15 -4
- package/src/attachments/content-blocks.ts +19 -16
- package/src/attachments/download.ts +173 -50
- package/src/attachments/index.ts +6 -5
- package/src/attachments/pipeline.ts +117 -49
- package/src/attachments/resize.ts +142 -0
- package/src/attachments/retention.ts +26 -70
- package/src/attachments/room-media.ts +643 -0
- package/src/attachments/route.ts +47 -4
- package/src/attachments/sandbox-archive.ts +15 -4
- package/src/attachments/sanitize.ts +24 -13
- package/src/attachments/view.ts +82 -38
- 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 +53 -4
- package/src/core/main-agent.ts +228 -125
- 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/attachment-recall.ts +502 -0
- package/src/core/middlewares/capability-gate.ts +77 -28
- package/src/core/middlewares/context-guard.ts +154 -21
- package/src/core/middlewares/index.ts +5 -0
- 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 +64 -24
- package/src/do/room-mirror.ts +144 -20
- package/src/do/transcript.ts +41 -12
- package/src/do/turn-attachments.ts +45 -6
- package/src/do/turn-metadata.ts +16 -4
- package/src/do/user-oracle-do.ts +639 -152
- package/src/index.ts +1 -0
- package/src/llm/provider-error.ts +5 -2
- package/src/llm/stream-liveness.ts +3 -2
- package/src/matrix/gateway-do.ts +164 -102
- package/src/matrix/ingest.ts +12 -3
- package/src/matrix/media-deadline.ts +5 -47
- 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/attachments/attachments.plugin.ts +3 -3
- package/src/plugins/attachments/view-attachment-tool.ts +6 -4
- 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/inline-payloads.ts +121 -0
- package/src/sqlite/serialization.ts +12 -0
- package/src/sqlite/sqlite-saver.ts +258 -42
- package/src/tasks/schedule-input.ts +5 -22
|
@@ -67,7 +67,7 @@ export interface ComposePromptInput {
|
|
|
67
67
|
* by loaded capabilities. Empty string renders no section.
|
|
68
68
|
*/
|
|
69
69
|
customInstructions: string;
|
|
70
|
-
/** Operational-mode block
|
|
70
|
+
/** Operational-mode block (`hooks.operationalMode`). Empty renders no section. */
|
|
71
71
|
operationalMode: string;
|
|
72
72
|
/**
|
|
73
73
|
* Whether the discovery meta-tools (`search_skills`, `list_capabilities`,
|
|
@@ -78,8 +78,8 @@ export interface ComposePromptInput {
|
|
|
78
78
|
capabilityDiscovery?: boolean;
|
|
79
79
|
/**
|
|
80
80
|
* Browser tools the client declared for this turn (`state.browserTools`),
|
|
81
|
-
* by name, plus whether they are
|
|
82
|
-
* portal plugin is `always` visible
|
|
81
|
+
* by name, plus whether they are always callable: `bound` is true when the
|
|
82
|
+
* portal plugin is `always` visible.
|
|
83
83
|
* Omit (or pass empty `names`) on non-Portal turns — nothing renders.
|
|
84
84
|
*/
|
|
85
85
|
browserTools?: { names: string[]; bound: boolean };
|
|
@@ -135,12 +135,41 @@ export const CONTEXT_SECTION_LABELS: Record<ContextSlot, string> = {
|
|
|
135
135
|
};
|
|
136
136
|
|
|
137
137
|
// ── Memory-context rendering ────────────────────────────────────────────────
|
|
138
|
-
// Guardrail caps for the block. Cross-bucket dedup does the
|
|
139
|
-
//
|
|
138
|
+
// Guardrail caps for the block. Cross-bucket dedup does the first shrinking;
|
|
139
|
+
// the budget then decides what goes in (`fitContextLines`).
|
|
140
140
|
const COMPACT_ENTITY_SUMMARY_CAP = 700;
|
|
141
141
|
const COMPACT_BLOCK_BUDGET = 2000;
|
|
142
142
|
const COMPACT_OVERFLOW_NOTE = '_(More remembered — ask me to recall.)_';
|
|
143
143
|
|
|
144
|
+
/** The lines of one memory bucket, by kind. */
|
|
145
|
+
interface SlotLines {
|
|
146
|
+
entities: string[];
|
|
147
|
+
facts: string[];
|
|
148
|
+
episodes: string[];
|
|
149
|
+
communities: string[];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** How a bucket reads, kind by kind. */
|
|
153
|
+
const KIND_LABELS: ReadonlyArray<[keyof SlotLines, string]> = [
|
|
154
|
+
['entities', '_Key entities:_'],
|
|
155
|
+
['facts', '_Facts:_'],
|
|
156
|
+
['episodes', '_Episodes (raw):_'],
|
|
157
|
+
['communities', '_Topic clusters:_'],
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Who gets the budget first. A fact says one thing in one line. A raw episode
|
|
162
|
+
* is what a fact was extracted from, or a question that was never one, so it
|
|
163
|
+
* only takes room nothing else wants: the answer follows the block, and a
|
|
164
|
+
* fact behind the episodes of earlier buckets is a fact the model never reads.
|
|
165
|
+
*/
|
|
166
|
+
const KIND_PRIORITY: ReadonlyArray<keyof SlotLines> = [
|
|
167
|
+
'facts',
|
|
168
|
+
'entities',
|
|
169
|
+
'communities',
|
|
170
|
+
'episodes',
|
|
171
|
+
];
|
|
172
|
+
|
|
144
173
|
/**
|
|
145
174
|
* Normalize a line for duplicate detection: lowercase, strip surrounding
|
|
146
175
|
* punctuation and collapse whitespace. Intentionally conservative — it only
|
|
@@ -186,92 +215,117 @@ function capAtBoundary(text: string, max: number): string {
|
|
|
186
215
|
}
|
|
187
216
|
|
|
188
217
|
/**
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
218
|
+
* One bucket's lines. Dedups entities (by name) and facts (by normalized text)
|
|
219
|
+
* against a `seen` set shared across every bucket, using the richest summary
|
|
220
|
+
* collected for each entity. Facts are looked at first, in every bucket, so a
|
|
221
|
+
* fact is never lost to the line of an entity that may not fit.
|
|
192
222
|
*/
|
|
193
|
-
function
|
|
223
|
+
function collectFacts(
|
|
194
224
|
data: MemoryContextSection | undefined,
|
|
195
225
|
seen: Set<string>,
|
|
196
|
-
|
|
197
|
-
): string | null {
|
|
198
|
-
if (!data) return null;
|
|
199
|
-
const { entities, facts, episodes, communities } = data;
|
|
226
|
+
): string[] {
|
|
200
227
|
const lines: string[] = [];
|
|
228
|
+
for (const f of data?.facts ?? []) {
|
|
229
|
+
const text = f.fact?.trim();
|
|
230
|
+
if (!text) continue;
|
|
231
|
+
const key = normalizeForDedup(text);
|
|
232
|
+
if (seen.has(key)) continue;
|
|
233
|
+
seen.add(key);
|
|
234
|
+
lines.push(`- ${text}`);
|
|
235
|
+
}
|
|
236
|
+
return lines;
|
|
237
|
+
}
|
|
201
238
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
239
|
+
function collectOthers(
|
|
240
|
+
data: MemoryContextSection | undefined,
|
|
241
|
+
seen: Set<string>,
|
|
242
|
+
richestSummary: Map<string, string>,
|
|
243
|
+
): Omit<SlotLines, 'facts'> {
|
|
244
|
+
const entities: string[] = [];
|
|
245
|
+
for (const e of data?.entities ?? []) {
|
|
246
|
+
const nameKey = `entity:${normalizeForDedup(e.name)}`;
|
|
247
|
+
if (seen.has(nameKey)) continue;
|
|
248
|
+
seen.add(nameKey);
|
|
249
|
+
const labels = (e.labels ?? []).filter((l) => l !== 'Entity').join('/');
|
|
250
|
+
const tag = labels ? ` (${labels})` : '';
|
|
251
|
+
const rawSummary = richestSummary.get(normalizeForDedup(e.name)) ?? '';
|
|
252
|
+
const summaryParts = rawSummary ? dedupSummaryLines(rawSummary, seen) : [];
|
|
253
|
+
const summary = summaryParts.length
|
|
254
|
+
? capAtBoundary(summaryParts.join('; '), COMPACT_ENTITY_SUMMARY_CAP)
|
|
255
|
+
: '';
|
|
256
|
+
entities.push(
|
|
257
|
+
summary ? `- **${e.name}**${tag}: ${summary}` : `- **${e.name}**${tag}`,
|
|
258
|
+
);
|
|
222
259
|
}
|
|
223
260
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
if (factLines.length) {
|
|
235
|
-
if (lines.length) lines.push('');
|
|
236
|
-
lines.push('_Facts:_', ...factLines);
|
|
237
|
-
}
|
|
261
|
+
const episodes: string[] = [];
|
|
262
|
+
for (const ep of data?.episodes ?? []) {
|
|
263
|
+
const content = ep.content?.trim();
|
|
264
|
+
if (!content) continue;
|
|
265
|
+
const key = normalizeForDedup(content);
|
|
266
|
+
if (seen.has(key)) continue;
|
|
267
|
+
seen.add(key);
|
|
268
|
+
const date = ep.created_at?.slice(0, 10) ?? '';
|
|
269
|
+
episodes.push(date ? `- *${date}* — ${content}` : `- ${content}`);
|
|
238
270
|
}
|
|
239
271
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
epLines.push(date ? `- *${date}* — ${content}` : `- ${content}`);
|
|
250
|
-
}
|
|
251
|
-
if (epLines.length) {
|
|
252
|
-
if (lines.length) lines.push('');
|
|
253
|
-
lines.push('_Episodes (raw):_', ...epLines);
|
|
254
|
-
}
|
|
272
|
+
const communities: string[] = [];
|
|
273
|
+
for (const c of data?.communities ?? []) {
|
|
274
|
+
const nameKey = `community:${normalizeForDedup(c.name)}`;
|
|
275
|
+
if (seen.has(nameKey)) continue;
|
|
276
|
+
seen.add(nameKey);
|
|
277
|
+
const summary = c.summary?.trim();
|
|
278
|
+
communities.push(
|
|
279
|
+
summary ? `- **${c.name}**: ${summary}` : `- **${c.name}**`,
|
|
280
|
+
);
|
|
255
281
|
}
|
|
282
|
+
return { entities, episodes, communities };
|
|
283
|
+
}
|
|
256
284
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
285
|
+
/** The block as the model reads it, empty buckets and kinds left out. */
|
|
286
|
+
function renderContextSlots(
|
|
287
|
+
slots: ReadonlyMap<ContextSlot, SlotLines>,
|
|
288
|
+
): string {
|
|
289
|
+
const sections: string[] = [];
|
|
290
|
+
for (const [key, lines] of slots) {
|
|
291
|
+
const kinds = KIND_LABELS.filter(([kind]) => lines[kind].length > 0).map(
|
|
292
|
+
([kind, label]) => [label, ...lines[kind]].join('\n'),
|
|
293
|
+
);
|
|
294
|
+
if (kinds.length > 0)
|
|
295
|
+
sections.push(
|
|
296
|
+
`**${CONTEXT_SECTION_LABELS[key]}**\n${kinds.join('\n\n')}`,
|
|
266
297
|
);
|
|
267
|
-
}
|
|
268
|
-
if (commLines.length) {
|
|
269
|
-
if (lines.length) lines.push('');
|
|
270
|
-
lines.push('_Topic clusters:_', ...commLines);
|
|
271
|
-
}
|
|
272
298
|
}
|
|
299
|
+
return sections.join('\n\n');
|
|
300
|
+
}
|
|
273
301
|
|
|
274
|
-
|
|
302
|
+
/**
|
|
303
|
+
* Fills the budget kind by kind in `KIND_PRIORITY` order, and within a kind one
|
|
304
|
+
* line from every bucket in turn, so no bucket waits behind an earlier one's
|
|
305
|
+
* list. A line that does not fit is passed over for a shorter one after it.
|
|
306
|
+
* The same input always keeps the same lines: the block sits in the system
|
|
307
|
+
* prompt, which has to stay byte-identical between turns.
|
|
308
|
+
*/
|
|
309
|
+
function fitContextLines(
|
|
310
|
+
slots: ReadonlyMap<ContextSlot, SlotLines>,
|
|
311
|
+
): ReadonlyMap<ContextSlot, SlotLines> {
|
|
312
|
+
const kept = new Map<ContextSlot, SlotLines>();
|
|
313
|
+
for (const key of slots.keys())
|
|
314
|
+
kept.set(key, { entities: [], facts: [], episodes: [], communities: [] });
|
|
315
|
+
for (const kind of KIND_PRIORITY) {
|
|
316
|
+
const longest = Math.max(...[...slots.values()].map((s) => s[kind].length));
|
|
317
|
+
for (let line = 0; line < longest; line += 1) {
|
|
318
|
+
for (const [key, lines] of slots) {
|
|
319
|
+
const candidate = lines[kind][line];
|
|
320
|
+
const target = kept.get(key)?.[kind];
|
|
321
|
+
if (candidate === undefined || !target) continue;
|
|
322
|
+
target.push(candidate);
|
|
323
|
+
if (renderContextSlots(kept).length > COMPACT_BLOCK_BUDGET)
|
|
324
|
+
target.pop();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return kept;
|
|
275
329
|
}
|
|
276
330
|
|
|
277
331
|
/**
|
|
@@ -296,28 +350,31 @@ function collectRichestSummaries(typed: TypedUserContext): Map<string, string> {
|
|
|
296
350
|
|
|
297
351
|
/**
|
|
298
352
|
* Render the memory-context block — empty string when nothing is populated.
|
|
299
|
-
* Cross-bucket dedup
|
|
353
|
+
* Cross-bucket dedup is always applied; past the budget, `fitContextLines`
|
|
354
|
+
* decides what stays and the note says that more is remembered.
|
|
300
355
|
*/
|
|
301
356
|
function buildContextBlock(userContext: UserContextData | undefined): string {
|
|
302
357
|
if (!userContext) return '';
|
|
303
358
|
const typed = userContext as TypedUserContext;
|
|
359
|
+
const keys = Object.keys(CONTEXT_SECTION_LABELS) as ContextSlot[];
|
|
304
360
|
|
|
305
361
|
const seen = new Set<string>();
|
|
362
|
+
const facts = new Map(
|
|
363
|
+
keys.map((key) => [key, collectFacts(typed[key], seen)]),
|
|
364
|
+
);
|
|
306
365
|
const richestSummary = collectRichestSummaries(typed);
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
318
|
-
const block = sections.join('\n\n');
|
|
366
|
+
const slots = new Map<ContextSlot, SlotLines>(
|
|
367
|
+
keys.map((key) => [
|
|
368
|
+
key,
|
|
369
|
+
{
|
|
370
|
+
facts: facts.get(key) ?? [],
|
|
371
|
+
...collectOthers(typed[key], seen, richestSummary),
|
|
372
|
+
},
|
|
373
|
+
]),
|
|
374
|
+
);
|
|
375
|
+
const block = renderContextSlots(slots);
|
|
319
376
|
if (block.length <= COMPACT_BLOCK_BUDGET) return block;
|
|
320
|
-
return `${
|
|
377
|
+
return `${renderContextSlots(fitContextLines(slots))}\n\n${COMPACT_OVERFLOW_NOTE}`;
|
|
321
378
|
}
|
|
322
379
|
|
|
323
380
|
/** Render user preferences as a bullet list for the prompt. */
|
|
@@ -371,8 +428,8 @@ export const PORTAL_CAPABILITY = 'portal';
|
|
|
371
428
|
/**
|
|
372
429
|
* Render the per-turn "Browser tools this turn" section. Empty string when the
|
|
373
430
|
* client declared no browser tools, so non-Portal turns pay nothing for it.
|
|
374
|
-
* The load line
|
|
375
|
-
*
|
|
431
|
+
* The load line depends on the plugin's visibility and not on whether the
|
|
432
|
+
* thread loaded it: a loaded capability must not change the system prompt.
|
|
376
433
|
*/
|
|
377
434
|
function buildBrowserToolsBlock(
|
|
378
435
|
browserTools: ComposePromptInput['browserTools'],
|
|
@@ -385,28 +442,42 @@ function buildBrowserToolsBlock(
|
|
|
385
442
|
];
|
|
386
443
|
if (!browserTools.bound) {
|
|
387
444
|
lines.push(
|
|
388
|
-
`
|
|
445
|
+
`Unless they are in your tool list, call \`load_capability({ names: ['${PORTAL_CAPABILITY}'] })\` once before using them.`,
|
|
389
446
|
);
|
|
390
447
|
}
|
|
391
448
|
return lines.join('\n');
|
|
392
449
|
}
|
|
393
450
|
|
|
394
451
|
/**
|
|
395
|
-
*
|
|
396
|
-
* grow
|
|
452
|
+
* How to reach what is not in the tool list. Rendered on every turn whose
|
|
453
|
+
* surface can actually grow, i.e. wherever the meta-tools are bound. The
|
|
454
|
+
* catalogue in the capability block names what can be loaded, so reaching
|
|
455
|
+
* something takes one `load_capability`, and work the bound tools cover takes
|
|
456
|
+
* no discovery at all.
|
|
397
457
|
*/
|
|
398
|
-
const DISCOVERY_PRINCIPLE = `- **
|
|
399
|
-
1. **
|
|
400
|
-
2. **
|
|
401
|
-
3. **Browser tools** — the \`[Portal]\`-prefixed tools
|
|
402
|
-
Do this proactively — even when the task wording doesn't literally match a capability's \`whenToUse\`. Tell the user in ONE short sentence what you're checking ("Checking the skills registry for an invoice generator…") before the search call, not after. If something fits, use it (load + run). If nothing fits after the search, say so in one short sentence and only THEN build from scratch. Reusing a vetted capability is almost always better than reinventing it; silently skipping discovery is the worst failure mode.`;
|
|
458
|
+
const DISCOVERY_PRINCIPLE = `- **Use your tool list first.** Tools already in it are ready: call them directly and never load them. Answer directly when the request needs no tool. When it needs something your tools cannot do, or a tool failed, reach it, in this order:
|
|
459
|
+
1. **Capabilities** — pick from "On-demand capabilities" and call \`load_capability\` once with everything you need. Do this rather than guess or say you cannot: remembered or assumed data is no substitute for a capability that has the real answer or can do the job.
|
|
460
|
+
2. **Skills** — before generating a file, document, report or page, or starting a packaged workflow, call \`search_skills\`. Running a skill takes the \`sandbox\` capability. If nothing fits, build it yourself.
|
|
461
|
+
3. **Browser tools** — the \`[Portal]\`-prefixed tools act on the user's screen inside the Portal: navigate, open a page, create pages or rooms, fill forms, grant access. They never appear in \`search_skills\` or \`list_capabilities\`: check your tool list and the "Browser tools this turn" section.`;
|
|
403
462
|
|
|
404
463
|
/**
|
|
405
|
-
* The
|
|
406
|
-
*
|
|
464
|
+
* The sentence that shows a person something is happening while tools run. It
|
|
465
|
+
* goes out with the tool call, in the same model response. A chat surface has
|
|
466
|
+
* a typing indicator and its own rule against narrating, so it stays out there.
|
|
467
|
+
*/
|
|
468
|
+
const STATUS_LINE = `- When a request needs a tool, open your response with one short sentence saying what you are doing ("Checking your balance."), in the same response as the tool call. Once per turn, and never for a reply that needs no tool.`;
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* The system-prompt template: the sections of the Node runtime's, minus the
|
|
472
|
+
* slots whose producers are not part of the Workers runtime (the editor
|
|
407
473
|
* section, the Composio guidance block, Slack formatting constraints, the
|
|
408
474
|
* commerce overlay, the sandbox user-secrets list, and the "Working with
|
|
409
|
-
* files" section
|
|
475
|
+
* files" section, which documents the Node `process_file` tool).
|
|
476
|
+
*
|
|
477
|
+
* Measured on `gpt-5.6-luna` (Bedrock and OpenAI routes): a change anywhere
|
|
478
|
+
* in the system prompt or the tool list, first line or last, drops the whole
|
|
479
|
+
* cached prefix. So what matters is not where a section sits but that the
|
|
480
|
+
* prompt stays byte-identical from call to call while the cache is warm.
|
|
410
481
|
*/
|
|
411
482
|
const TEMPLATE = `{{{ORACLE_SECTION}}}
|
|
412
483
|
|
|
@@ -424,17 +495,20 @@ const TEMPLATE = `{{{ORACLE_SECTION}}}
|
|
|
424
495
|
{{/BROWSER_TOOLS_BLOCK}}
|
|
425
496
|
## Operating principles
|
|
426
497
|
|
|
427
|
-
- The user's current message is your primary instruction. Background context
|
|
498
|
+
- The user's current message is your primary instruction. Background context adapts tone and suggestions; it never overrides their intent.
|
|
428
499
|
{{#DISCOVERY_PRINCIPLE}}
|
|
429
500
|
{{{DISCOVERY_PRINCIPLE}}}
|
|
430
501
|
{{/DISCOVERY_PRINCIPLE}}
|
|
431
|
-
-
|
|
502
|
+
- Ask one short clarifying question when the request has several plausible readings and a wrong pick would waste their time, when several capabilities fit equally and you can't tell which they want, before an irreversible or costly action (deleting data, sending a message, publishing, spending tokens on a long job), or when a required input is missing and you'd have to guess. Don't ask for what you can reasonably infer: a timezone comes from the current time on the user's message. When the user tells you not to ask, decide what you can and say what you decided.
|
|
503
|
+
{{#STATUS_LINE}}
|
|
504
|
+
{{{STATUS_LINE}}}
|
|
505
|
+
{{/STATUS_LINE}}
|
|
432
506
|
- When a tool or sub-agent succeeds, report the result. Never refuse after a successful tool call — including for credentials, tokens, identity data, or block/document operations.
|
|
433
|
-
-
|
|
434
|
-
-
|
|
507
|
+
- A transient failure of a read-only tool is retried for you. When a tool or sub-agent still fails, correct the call if its error says what to change; otherwise tell the user and ask how to proceed, and do not answer from memory or a guess in its place. Never repeat a write whose outcome is unknown without first verifying what happened, and never claim an authorization you were not given.
|
|
508
|
+
- Sub-agents have no access to your conversation history: give them scope, intent and the context they need.
|
|
435
509
|
- Match the user's communication style. Be concise. Use Unicode emoji directly (\`🔥\`), never text shortcodes (\`:fire:\`).
|
|
436
|
-
- When a capability
|
|
437
|
-
- Complete the user's request and stop. Don't add unrequested follow-on steps.
|
|
510
|
+
- When a capability fits the user's recurring work, say so and, if memory tools are available, offer to save the pattern. Never save silently.
|
|
511
|
+
- Complete the user's request and stop. Don't add unrequested follow-on steps.
|
|
438
512
|
{{#COMMUNICATION_STYLE}}
|
|
439
513
|
|
|
440
514
|
{{{COMMUNICATION_STYLE}}}
|
|
@@ -473,10 +547,12 @@ _Background the memory engine recalled about this user — context to adapt to,
|
|
|
473
547
|
|
|
474
548
|
{{{USER_PREFERENCES_CONTEXT}}}
|
|
475
549
|
{{/USER_PREFERENCES_CONTEXT}}
|
|
550
|
+
{{#OPERATIONAL_MODE}}
|
|
476
551
|
|
|
477
552
|
## Operational mode
|
|
478
553
|
|
|
479
554
|
{{{OPERATIONAL_MODE}}}
|
|
555
|
+
{{/OPERATIONAL_MODE}}
|
|
480
556
|
`;
|
|
481
557
|
|
|
482
558
|
const PROMPT_TEMPLATE = compileTemplate(TEMPLATE);
|
|
@@ -489,7 +565,7 @@ const PROMPT_TEMPLATE = compileTemplate(TEMPLATE);
|
|
|
489
565
|
* - **context** — only populated memory sub-sections render; empty ones skipped.
|
|
490
566
|
* - **user identity** — after the date and current entity: the user's DID
|
|
491
567
|
* and IXO account address, and the oracle's own DIDs.
|
|
492
|
-
* - **operational mode** — host hook
|
|
568
|
+
* - **operational mode** — host hook; none by default.
|
|
493
569
|
*
|
|
494
570
|
* Synchronous (the Node version was async only because LangChain's
|
|
495
571
|
* `PromptTemplate.format` is); kept `Promise`-returning for call-site parity.
|
|
@@ -519,6 +595,7 @@ export async function composePrompt(
|
|
|
519
595
|
CUSTOM_INSTRUCTIONS: input.customInstructions,
|
|
520
596
|
COMMUNICATION_STYLE: communicationStyle,
|
|
521
597
|
SURFACE_BLOCK: input.surfaceBlock ?? '',
|
|
598
|
+
STATUS_LINE: input.surfaceBlock ? '' : STATUS_LINE,
|
|
522
599
|
CONTEXT_BLOCK: buildContextBlock(input.userContext),
|
|
523
600
|
TIME_CONTEXT: input.timeContext,
|
|
524
601
|
CURRENT_ENTITY_DID: input.currentEntityDid,
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `read_result` — pages through a tool result
|
|
3
|
-
* context (`result-cap.ts` saved it whole and
|
|
4
|
-
* tail and the handle)
|
|
2
|
+
* `read_result` — pages through a tool result the model no longer has in
|
|
3
|
+
* full: one too large for the context (`result-cap.ts` saved it whole and
|
|
4
|
+
* showed a head, a tail and the handle) or one trimmed from an earlier turn
|
|
5
|
+
* (`context-guard.ts` left a stub naming the tool call, and the message
|
|
6
|
+
* itself is still in the thread's state). Chunks of a saved result are byte
|
|
7
|
+
* ranges, of a trimmed one character ranges; `next` is the offset of the
|
|
5
8
|
* following chunk or `null` at the end.
|
|
6
9
|
*/
|
|
10
|
+
import { ToolMessage, type BaseMessage } from '@langchain/core/messages';
|
|
7
11
|
import { z } from 'zod';
|
|
8
12
|
import { tool } from '../plugin-api/tool-helper';
|
|
9
13
|
import type { PluginTool } from '../plugin-api/types';
|
|
10
14
|
import type { ReadResultOutcome } from '../do/result-store';
|
|
15
|
+
import { contentToText } from './middlewares/result-cap';
|
|
11
16
|
|
|
12
17
|
export interface ReadResultToolOptions {
|
|
13
18
|
read: (
|
|
@@ -21,6 +26,27 @@ export interface ReadResultToolOptions {
|
|
|
21
26
|
|
|
22
27
|
export const READ_RESULT_TOOL_NAME = 'read_result';
|
|
23
28
|
|
|
29
|
+
/** The longest chunk `read_result` hands back under a result cap: well under the cap, so a page is never capped itself. */
|
|
30
|
+
export function readResultMaxChars(resultCapChars: number): number {
|
|
31
|
+
return Math.min(16_000, Math.max(512, Math.floor(resultCapChars / 2)));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The text of the thread's tool result for call `id`, if it is still there.
|
|
36
|
+
* Newest first: a provider can use a call id again on a later turn.
|
|
37
|
+
*/
|
|
38
|
+
function threadResult(
|
|
39
|
+
messages: readonly BaseMessage[],
|
|
40
|
+
id: string,
|
|
41
|
+
): string | undefined {
|
|
42
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
43
|
+
const message = messages[i];
|
|
44
|
+
if (ToolMessage.isInstance(message) && message.tool_call_id === id)
|
|
45
|
+
return contentToText(message.content);
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
24
50
|
export function buildReadResultTool(
|
|
25
51
|
options: ReadResultToolOptions,
|
|
26
52
|
): PluginTool {
|
|
@@ -29,25 +55,33 @@ export function buildReadResultTool(
|
|
|
29
55
|
const schema = z.object({
|
|
30
56
|
id: z
|
|
31
57
|
.string()
|
|
32
|
-
.
|
|
33
|
-
.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
.int()
|
|
37
|
-
.min(0)
|
|
38
|
-
.default(0)
|
|
39
|
-
.describe('Byte offset to read from.'),
|
|
58
|
+
.min(1)
|
|
59
|
+
.max(256)
|
|
60
|
+
.describe('The id named in a "[Result truncated …]" footer or stub.'),
|
|
61
|
+
offset: z.number().int().min(0).default(0).describe('Offset to read from.'),
|
|
40
62
|
length: z
|
|
41
63
|
.number()
|
|
42
64
|
.int()
|
|
43
65
|
.min(256)
|
|
44
66
|
.max(maxChars)
|
|
45
67
|
.default(defaultChars)
|
|
46
|
-
.describe(`
|
|
68
|
+
.describe(`Length to read (max ${maxChars}).`),
|
|
47
69
|
});
|
|
48
70
|
return tool(
|
|
49
|
-
async (rawArgs) => {
|
|
71
|
+
async (rawArgs, ctx) => {
|
|
50
72
|
const { id, offset, length } = schema.parse(rawArgs);
|
|
73
|
+
const text = threadResult(ctx.history.messages, id);
|
|
74
|
+
if (text !== undefined) {
|
|
75
|
+
const end = offset + length;
|
|
76
|
+
return {
|
|
77
|
+
id,
|
|
78
|
+
offset,
|
|
79
|
+
length: Math.max(0, Math.min(end, text.length) - offset),
|
|
80
|
+
size: text.length,
|
|
81
|
+
next: end < text.length ? end : null,
|
|
82
|
+
text: text.slice(offset, end),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
51
85
|
const outcome = await options.read(id, offset, length);
|
|
52
86
|
if (outcome.status !== 'ok')
|
|
53
87
|
return {
|
|
@@ -70,7 +104,7 @@ export function buildReadResultTool(
|
|
|
70
104
|
effect: 'read',
|
|
71
105
|
visibility: 'always',
|
|
72
106
|
description:
|
|
73
|
-
'Read a
|
|
107
|
+
'Read a tool result you no longer have in full, in chunks. Use the id from a "[Result truncated …]" footer or a stub that says the result was removed; start at offset 0 and pass the returned `next` as the offset of the following call until `next` is null.',
|
|
74
108
|
schema,
|
|
75
109
|
},
|
|
76
110
|
);
|
package/src/core/registries.ts
CHANGED
|
@@ -90,7 +90,7 @@ export interface ToolSummary {
|
|
|
90
90
|
export interface ToolSummarySource {
|
|
91
91
|
toolSummariesForPlugin(
|
|
92
92
|
pluginName: string,
|
|
93
|
-
): Array<
|
|
93
|
+
): Array<Pick<PluginTool, 'name' | 'description' | 'visibility'>>;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
/**
|
|
@@ -98,7 +98,10 @@ export interface ToolSummarySource {
|
|
|
98
98
|
* collection order — never another turn's.
|
|
99
99
|
*/
|
|
100
100
|
export function turnToolSummaries(
|
|
101
|
-
tools: ReadonlyArray<{
|
|
101
|
+
tools: ReadonlyArray<{
|
|
102
|
+
pluginName: string;
|
|
103
|
+
tool: Pick<PluginTool, 'name' | 'description' | 'visibility'>;
|
|
104
|
+
}>,
|
|
102
105
|
): ToolSummarySource {
|
|
103
106
|
return {
|
|
104
107
|
toolSummariesForPlugin: (pluginName) =>
|
|
@@ -107,6 +110,7 @@ export function turnToolSummaries(
|
|
|
107
110
|
.map(({ tool }) => ({
|
|
108
111
|
name: tool.name,
|
|
109
112
|
description: tool.description,
|
|
113
|
+
visibility: tool.visibility,
|
|
110
114
|
})),
|
|
111
115
|
};
|
|
112
116
|
}
|
|
@@ -250,10 +254,14 @@ export class ToolRegistry {
|
|
|
250
254
|
*/
|
|
251
255
|
toolSummariesForPlugin(
|
|
252
256
|
pluginName: string,
|
|
253
|
-
): Array<
|
|
254
|
-
return this.
|
|
257
|
+
): Array<Pick<PluginTool, 'name' | 'description' | 'visibility'>> {
|
|
258
|
+
return (this.bootCache ?? [])
|
|
255
259
|
.filter((entry) => entry.pluginName === pluginName)
|
|
256
|
-
.map(({ name, description }) => ({
|
|
260
|
+
.map(({ tool: { name, description, visibility } }) => ({
|
|
261
|
+
name,
|
|
262
|
+
description,
|
|
263
|
+
visibility,
|
|
264
|
+
}));
|
|
257
265
|
}
|
|
258
266
|
|
|
259
267
|
/**
|
package/src/core/state.ts
CHANGED
|
@@ -99,11 +99,12 @@ export const MainAgentGraphState = Annotation.Root({
|
|
|
99
99
|
|
|
100
100
|
/**
|
|
101
101
|
* Names of plugins the agent has loaded for this thread via
|
|
102
|
-
* `load_capability`.
|
|
102
|
+
* `load_capability`. The channel the graph runs on, with its reducer, is
|
|
103
|
+
* declared by the capability gate (`capabilityGateState`); an idle plugin
|
|
104
|
+
* leaves it on a cold turn (`capability-expiry.ts`).
|
|
103
105
|
*/
|
|
104
106
|
loadedPlugins: Annotation<string[]>({
|
|
105
|
-
reducer: (
|
|
106
|
-
Array.from(new Set([...(current ?? []), ...(update ?? [])])),
|
|
107
|
+
reducer: (_, update) => update,
|
|
107
108
|
default: () => [],
|
|
108
109
|
}),
|
|
109
110
|
});
|
|
@@ -33,6 +33,8 @@ import { NOOP_LOGGER } from './utils';
|
|
|
33
33
|
export interface AgentSpec {
|
|
34
34
|
name: string;
|
|
35
35
|
description: string;
|
|
36
|
+
/** Added to the error the parent reads when the sub-agent could not run. */
|
|
37
|
+
failureHint?: string;
|
|
36
38
|
tools?: StructuredTool[];
|
|
37
39
|
passthroughTools?: StructuredTool[];
|
|
38
40
|
systemPrompt: string;
|
|
@@ -356,7 +358,8 @@ export function createSubagentAsTool(
|
|
|
356
358
|
// to the parent as its result.
|
|
357
359
|
if (config.signal?.aborted || isHarnessLimitError(err)) throw err;
|
|
358
360
|
const message = err instanceof Error ? err.message : String(err);
|
|
359
|
-
|
|
361
|
+
const hint = spec.failureHint ? ` ${spec.failureHint}` : '';
|
|
362
|
+
return `Error running ${spec.name}: ${message}${hint}`;
|
|
360
363
|
}
|
|
361
364
|
},
|
|
362
365
|
{
|