@ixo/oracle-runtime-workers 0.12.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -10
- package/package.json +14 -9
- package/src/artifacts/config.ts +158 -0
- package/src/artifacts/crypto.ts +94 -0
- package/src/artifacts/routes.ts +73 -0
- package/src/artifacts/store.ts +238 -0
- package/src/artifacts/sweep.ts +200 -0
- package/src/artifacts/tool.ts +103 -0
- package/src/artifacts/viewer-page.ts +614 -0
- package/src/attachments/download.ts +168 -45
- package/src/attachments/index.ts +9 -0
- package/src/attachments/magic.ts +33 -0
- package/src/attachments/pipeline.ts +105 -49
- package/src/attachments/sandbox-archive.ts +9 -8
- package/src/attachments/view-cache.ts +142 -0
- package/src/attachments/view.ts +53 -12
- package/src/channels/auth.ts +267 -0
- package/src/channels/contract.ts +137 -0
- package/src/channels/turns.ts +270 -0
- package/src/core/budgeted-llm.ts +223 -0
- package/src/core/capability-router.ts +273 -0
- package/src/core/context-budget.ts +97 -10
- package/src/core/env.ts +111 -0
- package/src/core/execution-profile.ts +17 -0
- package/src/core/index.ts +267 -9
- package/src/core/llm.ts +30 -9
- package/src/core/main-agent-types.ts +41 -1
- package/src/core/main-agent.ts +367 -65
- package/src/core/manifest.ts +34 -0
- package/src/core/meta-tools.ts +128 -18
- package/src/core/middlewares/capability-gate.ts +130 -12
- package/src/core/middlewares/context-guard.ts +55 -24
- package/src/core/middlewares/index.ts +20 -2
- package/src/core/middlewares/page-context.ts +46 -2
- package/src/core/middlewares/return-direct-first.ts +47 -0
- package/src/core/middlewares/summarization.ts +152 -10
- package/src/core/middlewares/tool-execution.ts +323 -0
- package/src/core/middlewares/tool-repetition-guard.ts +202 -35
- package/src/core/middlewares/turn-boundary.ts +162 -0
- package/src/core/openrouter-pricing.ts +155 -22
- package/src/core/prompt-composer.ts +112 -7
- package/src/core/registries.ts +253 -48
- package/src/core/request-admission.ts +159 -0
- package/src/core/runtime-context.ts +107 -8
- package/src/core/state.ts +1 -1
- package/src/core/sub-agent-fallback.ts +21 -2
- package/src/core/subagent-as-tool.ts +78 -43
- package/src/core/tool-access.ts +189 -0
- package/src/core/tool-scheduler.ts +125 -0
- package/src/core/turn-budget.ts +205 -0
- package/src/core/turn-time-note.ts +43 -0
- package/src/core/user-kv.ts +446 -0
- package/src/core/wrap-plugin-tool.ts +20 -0
- package/src/delivery/plan.ts +278 -0
- package/src/delivery/profile.ts +161 -0
- package/src/delivery/prompt.ts +27 -0
- package/src/delivery/schema.ts +61 -0
- package/src/delivery/shaper.ts +389 -0
- package/src/delivery/types.ts +84 -0
- package/src/do/ambient.ts +51 -15
- package/src/do/contracts.ts +198 -6
- package/src/do/idle-eviction.ts +38 -0
- package/src/do/matrix-turn-ledger.ts +24 -5
- package/src/do/result-store-test-do.ts +137 -3
- package/src/do/result-store.ts +239 -58
- package/src/do/room-mirror.ts +29 -0
- package/src/do/run-buffer.ts +64 -9
- package/src/do/run-coordinator.ts +451 -76
- package/src/do/run-request.ts +2 -0
- package/src/do/run-store-test-do.ts +301 -1
- package/src/do/run-store.ts +385 -43
- package/src/do/secrets-adapter.ts +22 -3
- package/src/do/sse-stream.ts +60 -9
- package/src/do/transcript.ts +48 -5
- package/src/do/turn-body.ts +105 -5
- package/src/do/ucan-service.ts +204 -34
- package/src/do/user-oracle-do.ts +1883 -264
- package/src/feedback/config.ts +127 -0
- package/src/feedback/contract.ts +148 -0
- package/src/feedback/linear-sink.ts +285 -0
- package/src/feedback/privacy.ts +150 -0
- package/src/feedback/reservation.ts +298 -0
- package/src/feedback/submit.ts +247 -0
- package/src/index.ts +138 -6
- package/src/llm/byo-adapter.ts +27 -6
- package/src/llm/byo-catalog.ts +13 -0
- package/src/llm/byo-model-fallback.ts +248 -0
- package/src/llm/byo-service.ts +45 -8
- package/src/llm/provider-error.ts +139 -2
- package/src/matrix/gateway-do.ts +523 -111
- package/src/matrix/group-chat.ts +66 -23
- package/src/matrix/inbox-store.ts +16 -1
- package/src/matrix/ingest.ts +79 -14
- package/src/matrix/media-deadline.ts +130 -0
- package/src/matrix/replay-format.ts +5 -3
- package/src/matrix/reply-parts.ts +92 -0
- package/src/matrix/room-alias.ts +96 -0
- package/src/matrix/user-homeserver.ts +166 -7
- package/src/owner-store/ixo-vfs-store.ts +170 -43
- package/src/owner-store/migrating-store.ts +31 -15
- package/src/owner-store/types.ts +18 -3
- package/src/plugin-api/index.ts +43 -2
- package/src/plugin-api/oracle-plugin.ts +35 -0
- package/src/plugin-api/request-admission.ts +41 -0
- package/src/plugin-api/tool-helper.ts +25 -1
- package/src/plugin-api/tool-plane.ts +92 -0
- package/src/plugin-api/types.ts +194 -6
- package/src/plugins/agui/agui.plugin.ts +31 -16
- package/src/plugins/composio/composio-tools.ts +57 -6
- package/src/plugins/composio/composio-ucan.ts +3 -2
- package/src/plugins/composio/composio.plugin.ts +2 -0
- package/src/plugins/delegated-capabilities.ts +25 -0
- package/src/plugins/editor/content-session.ts +173 -83
- package/src/plugins/editor/content-tools.ts +76 -17
- package/src/plugins/editor/editor-mx.ts +28 -9
- package/src/plugins/editor/editor.plugin.ts +3 -1
- package/src/plugins/editor/failures.ts +28 -7
- package/src/plugins/editor/prompts.ts +2 -1
- package/src/plugins/editor/provider.ts +553 -40
- package/src/plugins/editor/standalone-editor-tool.ts +61 -18
- package/src/plugins/firecrawl/firecrawl-tools.ts +13 -5
- package/src/plugins/flows/action-metadata.ts +111 -11
- package/src/plugins/flows/actions.ts +18 -0
- package/src/plugins/flows/decision-context.ts +31 -0
- package/src/plugins/flows/edit.ts +223 -52
- package/src/plugins/flows/errors.ts +22 -1
- package/src/plugins/flows/flow-doc.ts +92 -9
- package/src/plugins/flows/flows.plugin.ts +11 -5
- package/src/plugins/flows/forms.ts +34 -0
- package/src/plugins/flows/index.ts +2 -0
- package/src/plugins/flows/input-policy.ts +140 -0
- package/src/plugins/flows/prompts.ts +14 -11
- package/src/plugins/flows/read.ts +49 -9
- package/src/plugins/flows/ref-syntax.ts +129 -0
- package/src/plugins/flows/test-homeserver.ts +245 -0
- package/src/plugins/flows/tools/authoring.ts +216 -112
- package/src/plugins/flows/tools/discovery.ts +4 -1
- package/src/plugins/flows/tools/forms.ts +21 -2
- package/src/plugins/flows/tools/settings.ts +34 -13
- package/src/plugins/flows/translator.ts +13 -10
- package/src/plugins/flows/types.ts +167 -42
- package/src/plugins/index.ts +11 -1
- package/src/plugins/ixo-transaction/chain-ids.ts +43 -0
- package/src/plugins/ixo-transaction/index.ts +2 -0
- package/src/plugins/ixo-transaction/ixo-transaction.plugin.ts +119 -0
- package/src/plugins/ixo-transaction/receipts.ts +99 -0
- package/src/plugins/ixo-transaction/tools.ts +329 -0
- package/src/plugins/ixo-transaction/wallet-signing.ts +202 -0
- package/src/plugins/mcp-tool-adapter.ts +23 -2
- package/src/plugins/memory/memory-tools.ts +7 -2
- package/src/plugins/memory/memory-ucan.ts +3 -2
- package/src/plugins/memory/memory.plugin.ts +2 -0
- package/src/plugins/pod-creator/blueprint-store.ts +163 -0
- package/src/plugins/pod-creator/blueprint-types.ts +61 -0
- package/src/plugins/pod-creator/bounded-map.ts +87 -0
- package/src/plugins/pod-creator/capsule-content-client.ts +285 -0
- package/src/plugins/pod-creator/chain-gateway.ts +110 -0
- package/src/plugins/pod-creator/config.ts +85 -0
- package/src/plugins/pod-creator/create-session-store.ts +180 -0
- package/src/plugins/pod-creator/create-tools.ts +538 -0
- package/src/plugins/pod-creator/design-pod-roles.ts +111 -0
- package/src/plugins/pod-creator/index.ts +65 -0
- package/src/plugins/pod-creator/orchestration-tools.ts +172 -0
- package/src/plugins/pod-creator/pod-creator.plugin.ts +190 -0
- package/src/plugins/pod-creator/stage.ts +201 -0
- package/src/plugins/pod-creator/sub-agents.ts +189 -0
- package/src/plugins/portal/action-log.ts +21 -1
- package/src/plugins/portal/declared-tools.ts +90 -0
- package/src/plugins/portal/portal.plugin.ts +42 -16
- package/src/plugins/sandbox/sandbox-bridge.ts +79 -10
- package/src/plugins/sandbox/sandbox-mcp.ts +3 -2
- package/src/plugins/sandbox/sandbox-write-blob.ts +11 -1
- package/src/plugins/sandbox/sandbox.plugin.ts +8 -16
- package/src/plugins/tasks/manifest.ts +1 -1
- package/src/plugins/tasks/middleware.ts +20 -7
- package/src/plugins/tasks/tasks-tools.ts +30 -3
- package/src/plugins/tasks/tasks.plugin.ts +64 -11
- package/src/plugins/user-preferences/schema.ts +12 -1
- package/src/plugins/user-preferences/user-preferences-store.ts +38 -13
- package/src/plugins/user-preferences/user-preferences-tool.ts +3 -2
- package/src/plugins/vfs/vfs-client.ts +250 -71
- package/src/plugins/vfs/vfs-content.ts +30 -11
- package/src/plugins/vfs/vfs-errors.ts +22 -0
- package/src/plugins/vfs/vfs-sandbox-tools.ts +12 -5
- package/src/plugins/vfs/vfs-tools.ts +60 -20
- package/src/realtime/frontend-call-registry.ts +228 -81
- package/src/realtime/realtime-endpoint.ts +215 -44
- package/src/realtime/session-socket-hub.ts +47 -2
- package/src/secrets/secrets-service.ts +9 -3
- package/src/shell/app.ts +529 -82
- package/src/shell/auth.ts +131 -37
- package/src/shell/turn-body-cap.ts +11 -13
- package/src/sqlite/blob-compactor.ts +61 -28
- package/src/sqlite/database.ts +156 -90
- package/src/sqlite/do-vfs.ts +416 -129
- package/src/sqlite/page-tier.ts +133 -40
- package/src/sqlite/serialization.ts +40 -0
- package/src/sqlite/sessions-store.ts +10 -2
- package/src/sqlite/sqlite-saver.ts +139 -19
- package/src/sqlite/user-kv-store.ts +317 -0
- package/src/tasks/schedule.ts +201 -12
- package/src/tasks/scheduler.ts +586 -98
- package/src/tasks/spec.ts +67 -3
- package/src/tasks/store.ts +292 -27
- package/src/tasks/topic-deliverables.ts +70 -0
|
@@ -56,7 +56,7 @@ export const tasksManifest: PluginManifest = {
|
|
|
56
56
|
{
|
|
57
57
|
user: '(replying to an approval request) Yes — go ahead.',
|
|
58
58
|
thought:
|
|
59
|
-
'The user approved the pending run — record it; approval
|
|
59
|
+
'The user approved the pending run — record it; approval starts the run, and its result is posted to the room when it finishes.',
|
|
60
60
|
tool: 'resolve_task_approval',
|
|
61
61
|
args: {
|
|
62
62
|
taskId: 'task_daily-linkedin-update_a1b2c3d4',
|
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
* - There are no dedicated task rooms or room→session bindings — pending
|
|
9
9
|
* approvals live on the task records themselves (`pendingApprovalAt`), so
|
|
10
10
|
* the gate reads them straight off `OracleTasksSurface.list()`.
|
|
11
|
-
* - The gate never resolves an approval itself:
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* - The gate never resolves an approval itself: resolving an approval
|
|
12
|
+
* starts the run (a full agent turn, on the user object's alarm), which
|
|
13
|
+
* is the model's call to make through the tool, not a model-call
|
|
14
|
+
* wrapper's. Plain yes/no replies are still classified
|
|
14
15
|
* deterministically (`classifyReplyFast`) — the classification shapes the
|
|
15
16
|
* hint, and the model records the decision through the
|
|
16
17
|
* `resolve_task_approval` tool.
|
|
@@ -22,6 +23,10 @@ import type { BaseMessage } from '@langchain/core/messages';
|
|
|
22
23
|
import { createMiddleware, type AgentMiddleware } from 'langchain';
|
|
23
24
|
import type { Logger, OracleTasksSurface } from '../../plugin-api/types';
|
|
24
25
|
import { pendingApprovalOf } from '../../tasks/store';
|
|
26
|
+
import {
|
|
27
|
+
stripTurnTimeNote,
|
|
28
|
+
TURN_TIME_NOTE_KWARG,
|
|
29
|
+
} from '../../core/turn-time-note';
|
|
25
30
|
|
|
26
31
|
const APPROVE = new Set([
|
|
27
32
|
'yes',
|
|
@@ -114,7 +119,7 @@ export async function computeApprovalHint(
|
|
|
114
119
|
if (only && decision === 'approved') {
|
|
115
120
|
return (
|
|
116
121
|
`\n\n[Task approval gate] The user's reply APPROVES the pending run of task ${only.id} ("${only.title}"). ` +
|
|
117
|
-
'Call `resolve_task_approval` with outcome "approved" NOW — approval
|
|
122
|
+
'Call `resolve_task_approval` with outcome "approved" NOW — approval starts the run, and its result is posted to the room when it finishes — then tell the user it is running.'
|
|
118
123
|
);
|
|
119
124
|
}
|
|
120
125
|
if (only && decision === 'rejected') {
|
|
@@ -128,7 +133,7 @@ export async function computeApprovalHint(
|
|
|
128
133
|
.join(', ');
|
|
129
134
|
return (
|
|
130
135
|
`\n\n[Task approval gate] ${pending.length} task run(s) are waiting for the user's approval: ${listing}. ` +
|
|
131
|
-
'If this reply decides one of them (possibly with tweaks), call `resolve_task_approval` with the outcome — "approved"
|
|
136
|
+
'If this reply decides one of them (possibly with tweaks), call `resolve_task_approval` with the outcome — "approved" starts the run (pass requested tweaks in `note`), "declined" drops it. ' +
|
|
132
137
|
'Otherwise answer normally and remind the user of the pending approval.'
|
|
133
138
|
);
|
|
134
139
|
}
|
|
@@ -144,12 +149,20 @@ export function userDidFromContext(context: unknown): string | undefined {
|
|
|
144
149
|
return typeof did === 'string' && did.length > 0 ? did : undefined;
|
|
145
150
|
}
|
|
146
151
|
|
|
147
|
-
/**
|
|
152
|
+
/**
|
|
153
|
+
* Text of the latest HumanMessage — string content or joined text parts —
|
|
154
|
+
* as the user sent it: without the turn time note the runtime puts in front.
|
|
155
|
+
*/
|
|
148
156
|
export function lastHumanText(messages: BaseMessage[]): string | null {
|
|
149
157
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
150
158
|
const message = messages[i];
|
|
151
159
|
if (!message || message.type !== 'human') continue;
|
|
152
|
-
return textOfContent(
|
|
160
|
+
return textOfContent(
|
|
161
|
+
stripTurnTimeNote(
|
|
162
|
+
message.content,
|
|
163
|
+
message.additional_kwargs?.[TURN_TIME_NOTE_KWARG],
|
|
164
|
+
),
|
|
165
|
+
);
|
|
153
166
|
}
|
|
154
167
|
return null;
|
|
155
168
|
}
|
|
@@ -38,7 +38,8 @@ const APPROVAL_FIELD_DESCRIPTION =
|
|
|
38
38
|
'fires it does NOT execute — it posts an approval request into the ' +
|
|
39
39
|
"user's oracle room and waits. The user approves or declines by replying " +
|
|
40
40
|
'there; record the decision with `resolve_task_approval` — approval ' +
|
|
41
|
-
'
|
|
41
|
+
'starts the run in the background (its result is posted to the room when ' +
|
|
42
|
+
'it finishes), a decline drops it.';
|
|
42
43
|
|
|
43
44
|
const taskInputShape = {
|
|
44
45
|
title: z.string().min(1).max(120),
|
|
@@ -72,6 +73,14 @@ function requireTasks(ctx: RuntimeContext): OracleTasksSurface | null {
|
|
|
72
73
|
return ctx.tasks ?? null;
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
/**
|
|
77
|
+
* A host-created supplied-context task: its intent is the authorized source
|
|
78
|
+
* text and its result was produced from it alone. Neither enters an ordinary
|
|
79
|
+
* conversation, and the task is one immutable attempt (cancel only).
|
|
80
|
+
*/
|
|
81
|
+
const RESTRICTED_TASK_NOTE =
|
|
82
|
+
'Restricted supplied-context task: its source text and result are not available in this conversation, and it cannot be edited, paused or resumed — only cancelled.';
|
|
83
|
+
|
|
75
84
|
function summarizeRecord(record: OracleTaskRecord): Record<string, unknown> {
|
|
76
85
|
const pendingApprovalAt = pendingApprovalOf(record);
|
|
77
86
|
return {
|
|
@@ -84,6 +93,10 @@ function summarizeRecord(record: OracleTaskRecord): Record<string, unknown> {
|
|
|
84
93
|
...(pendingApprovalAt !== undefined && {
|
|
85
94
|
awaitingApprovalSince: pendingApprovalAt,
|
|
86
95
|
}),
|
|
96
|
+
...(record.executionProfile && {
|
|
97
|
+
restricted: true,
|
|
98
|
+
executionProfile: record.executionProfile,
|
|
99
|
+
}),
|
|
87
100
|
};
|
|
88
101
|
}
|
|
89
102
|
|
|
@@ -234,6 +247,18 @@ function getTask(): PluginTool {
|
|
|
234
247
|
try {
|
|
235
248
|
const record = await tasks.get(taskId);
|
|
236
249
|
if (!record) return { ok: false, error: 'Task not found.' };
|
|
250
|
+
if (record.executionProfile) {
|
|
251
|
+
return {
|
|
252
|
+
ok: true,
|
|
253
|
+
...summarizeRecord(record),
|
|
254
|
+
lastRunAt: record.lastRunAt ?? null,
|
|
255
|
+
lastResult: record.lastResult
|
|
256
|
+
? { ok: record.lastResult.ok, at: record.lastResult.at }
|
|
257
|
+
: null,
|
|
258
|
+
consecutiveFailures: record.consecutiveFailures,
|
|
259
|
+
note: RESTRICTED_TASK_NOTE,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
237
262
|
return {
|
|
238
263
|
ok: true,
|
|
239
264
|
...summarizeRecord(record),
|
|
@@ -354,7 +379,7 @@ function resolveTaskApproval(): PluginTool {
|
|
|
354
379
|
outcome: args.outcome,
|
|
355
380
|
note:
|
|
356
381
|
args.outcome === 'approved'
|
|
357
|
-
? "
|
|
382
|
+
? "Approved — the run is starting now in the background; its result will be posted to the user's room when it finishes. Tell the user it is running; do not claim a result yet."
|
|
358
383
|
: 'Decision recorded — the pending run was dropped and nothing was executed.',
|
|
359
384
|
};
|
|
360
385
|
} catch (err) {
|
|
@@ -364,7 +389,7 @@ function resolveTaskApproval(): PluginTool {
|
|
|
364
389
|
{
|
|
365
390
|
name: 'resolve_task_approval',
|
|
366
391
|
description:
|
|
367
|
-
"Record the user's decision on a task run that is waiting for approval. 'approved'
|
|
392
|
+
"Record the user's decision on a task run that is waiting for approval. 'approved' starts the run right away in the background (its result is posted to the user's room when it finishes); 'declined' drops it without running. Call this when the user answers a pending approval request — pass their requested tweaks in `note`.",
|
|
368
393
|
schema: resolveApprovalInput,
|
|
369
394
|
},
|
|
370
395
|
);
|
|
@@ -381,6 +406,8 @@ function suggestSpecFix(): PluginTool {
|
|
|
381
406
|
try {
|
|
382
407
|
const record = await tasks.get(taskId);
|
|
383
408
|
if (!record) return { ok: false, error: 'Task not found.' };
|
|
409
|
+
if (record.executionProfile)
|
|
410
|
+
return { ok: false, error: RESTRICTED_TASK_NOTE };
|
|
384
411
|
const lastFailure =
|
|
385
412
|
record.lastResult && !record.lastResult.ok
|
|
386
413
|
? record.lastResult
|
|
@@ -22,9 +22,10 @@
|
|
|
22
22
|
* `TaskSchedulerHost.maxTasksPerUser` / `.minCronIntervalSec`.
|
|
23
23
|
*
|
|
24
24
|
* Middlewares are built once at boot while the tasks surface is per-user, so
|
|
25
|
-
* the plugin
|
|
26
|
-
* user DID (`getRequestTools` runs on every agent
|
|
27
|
-
*
|
|
25
|
+
* the plugin holds each request's `ctx.tasks` keyed by the authenticated
|
|
26
|
+
* user DID for the length of the turn (`getRequestTools` runs on every agent
|
|
27
|
+
* build, before any model call; `onTurnEnd` releases it) and the middleware
|
|
28
|
+
* looks the surface up per model call — see `TaskSurfaceRegistry`.
|
|
28
29
|
*/
|
|
29
30
|
import { z } from 'zod';
|
|
30
31
|
import { defineOraclePlugin } from '../../plugin-api/define-plugin';
|
|
@@ -41,12 +42,59 @@ export const tasksConfigSchema = z.object({
|
|
|
41
42
|
|
|
42
43
|
export const TASKS_PLUGIN_NAME = 'tasks';
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
/**
|
|
46
|
+
* `ctx.tasks` per user DID, for the turns that are running. One plugin
|
|
47
|
+
* instance serves every user object in the isolate, and a surface keeps its
|
|
48
|
+
* user object's scheduler and store reachable, so an entry lives only while
|
|
49
|
+
* a turn of that user holds it: `hold` at the agent build, the returned
|
|
50
|
+
* release at the turn's end. Overlapping turns of one user (two sessions)
|
|
51
|
+
* share the entry; the last release drops it.
|
|
52
|
+
*/
|
|
53
|
+
export class TaskSurfaceRegistry {
|
|
54
|
+
private readonly entries = new Map<
|
|
55
|
+
string,
|
|
56
|
+
{ surface: OracleTasksSurface; turns: number }
|
|
57
|
+
>();
|
|
58
|
+
|
|
59
|
+
/** Hold `surface` for `did` for one turn. The release runs once. */
|
|
60
|
+
hold(did: string, surface: OracleTasksSurface): () => void {
|
|
61
|
+
let entry = this.entries.get(did);
|
|
62
|
+
if (entry) {
|
|
63
|
+
entry.surface = surface;
|
|
64
|
+
entry.turns += 1;
|
|
65
|
+
} else {
|
|
66
|
+
entry = { surface, turns: 1 };
|
|
67
|
+
this.entries.set(did, entry);
|
|
68
|
+
}
|
|
69
|
+
const held = entry;
|
|
70
|
+
let released = false;
|
|
71
|
+
return () => {
|
|
72
|
+
if (released) return;
|
|
73
|
+
released = true;
|
|
74
|
+
held.turns -= 1;
|
|
75
|
+
if (held.turns <= 0 && this.entries.get(did) === held)
|
|
76
|
+
this.entries.delete(did);
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The user's turn runs without a scheduler: forget any surface. */
|
|
81
|
+
clear(did: string): void {
|
|
82
|
+
this.entries.delete(did);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
surfaceFor(did: string): OracleTasksSurface | undefined {
|
|
86
|
+
return this.entries.get(did)?.surface;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Users with an entry (diagnostics, tests). */
|
|
90
|
+
get size(): number {
|
|
91
|
+
return this.entries.size;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
49
94
|
|
|
95
|
+
export function createTasksPlugin(
|
|
96
|
+
surfaces: TaskSurfaceRegistry = new TaskSurfaceRegistry(),
|
|
97
|
+
): OraclePlugin {
|
|
50
98
|
return defineOraclePlugin({
|
|
51
99
|
name: TASKS_PLUGIN_NAME,
|
|
52
100
|
version: '1.0.0',
|
|
@@ -57,13 +105,18 @@ export function createTasksPlugin(): OraclePlugin {
|
|
|
57
105
|
autoDetect: () => true,
|
|
58
106
|
getTools: () => createTaskTools(),
|
|
59
107
|
getRequestTools: (rtCtx) => {
|
|
60
|
-
if (rtCtx.tasks)
|
|
61
|
-
|
|
108
|
+
if (!rtCtx.tasks) {
|
|
109
|
+
surfaces.clear(rtCtx.user.did);
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
// A host without per-turn cleanup keeps the entry until the user's
|
|
113
|
+
// next turn without a scheduler (the Workers host always has it).
|
|
114
|
+
rtCtx.onTurnEnd?.(surfaces.hold(rtCtx.user.did, rtCtx.tasks));
|
|
62
115
|
return [];
|
|
63
116
|
},
|
|
64
117
|
getMiddlewares: (ctx) => [
|
|
65
118
|
createTaskApprovalGateMiddleware({
|
|
66
|
-
surfaceFor: (userDid) => surfaces.
|
|
119
|
+
surfaceFor: (userDid) => surfaces.surfaceFor(userDid),
|
|
67
120
|
logger: ctx.logger,
|
|
68
121
|
}),
|
|
69
122
|
],
|
|
@@ -18,11 +18,22 @@ export const FORMALITY_LEVELS = [
|
|
|
18
18
|
'academic',
|
|
19
19
|
] as const;
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Longest `tone` label. The tone is pasted into every system prompt, so new
|
|
23
|
+
* writes over this length are rejected by the tool schema, and a longer value
|
|
24
|
+
* already stored (an older write, or one from the Node runtime, which never
|
|
25
|
+
* capped it) is cut to this length when it is read.
|
|
26
|
+
*/
|
|
27
|
+
export const MAX_TONE_LENGTH = 120;
|
|
28
|
+
|
|
21
29
|
export const UserPreferencesSchema = z.object({
|
|
22
30
|
agentName: z.string().max(80).optional(),
|
|
23
31
|
userName: z.string().max(80).optional(),
|
|
24
32
|
language: z.string().max(20).optional(), // BCP-47-ish, free-form
|
|
25
|
-
tone: z
|
|
33
|
+
tone: z
|
|
34
|
+
.string()
|
|
35
|
+
.transform((t) => t.slice(0, MAX_TONE_LENGTH))
|
|
36
|
+
.optional(), // e.g. "casual", "playful"
|
|
26
37
|
formality: z.enum(FORMALITY_LEVELS).optional(),
|
|
27
38
|
customInstructions: z.string().max(2000).optional(),
|
|
28
39
|
updatedAt: z.iso.datetime().optional(), // the store sets it on every write
|
|
@@ -86,29 +86,51 @@ export class UserPreferencesStore implements UserPreferencesSurface {
|
|
|
86
86
|
* user without preferences costs one state read per TTL, not per turn.
|
|
87
87
|
*/
|
|
88
88
|
async get(roomId: string): Promise<UserPreferences | undefined> {
|
|
89
|
+
try {
|
|
90
|
+
return await this.load(roomId);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
this.logger?.warn(
|
|
93
|
+
`[user-preferences] failed to load prefs for room ${roomId}: ${
|
|
94
|
+
error instanceof Error ? error.message : String(error)
|
|
95
|
+
}`,
|
|
96
|
+
);
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The room's preferences: `undefined` when no event exists yet or the
|
|
103
|
+
* stored payload is unreadable (logged); THROWS when the gateway read
|
|
104
|
+
* itself fails, so a caller about to overwrite the event can tell "nothing
|
|
105
|
+
* stored" from "could not look".
|
|
106
|
+
*/
|
|
107
|
+
private async load(
|
|
108
|
+
roomId: string,
|
|
109
|
+
): Promise<StoredUserPreferences | undefined> {
|
|
89
110
|
const hit = this.cache.get(roomId);
|
|
90
111
|
if (hit && hit.expiresAt > this.now()) return hit.value;
|
|
91
112
|
|
|
113
|
+
const json = await this.access.getRoomStateEvent(
|
|
114
|
+
roomId,
|
|
115
|
+
ROOM_STATE_EVENT_TYPE,
|
|
116
|
+
USER_PREFS_STATE_KEY,
|
|
117
|
+
);
|
|
118
|
+
if (json === null) {
|
|
119
|
+
this.remember(roomId, undefined);
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
92
123
|
let decoded: unknown;
|
|
93
124
|
try {
|
|
94
|
-
|
|
95
|
-
roomId,
|
|
96
|
-
ROOM_STATE_EVENT_TYPE,
|
|
97
|
-
USER_PREFS_STATE_KEY,
|
|
98
|
-
);
|
|
99
|
-
decoded =
|
|
100
|
-
json === null
|
|
101
|
-
? null
|
|
102
|
-
: await decodeRoomStateContent(JSON.parse(json) as unknown);
|
|
125
|
+
decoded = await decodeRoomStateContent(JSON.parse(json) as unknown);
|
|
103
126
|
} catch (error) {
|
|
104
127
|
this.logger?.warn(
|
|
105
|
-
`[user-preferences]
|
|
128
|
+
`[user-preferences] unreadable prefs payload for room ${roomId}: ${
|
|
106
129
|
error instanceof Error ? error.message : String(error)
|
|
107
130
|
}`,
|
|
108
131
|
);
|
|
109
132
|
return undefined;
|
|
110
133
|
}
|
|
111
|
-
|
|
112
134
|
if (decoded === null) {
|
|
113
135
|
this.remember(roomId, undefined);
|
|
114
136
|
return undefined;
|
|
@@ -128,13 +150,16 @@ export class UserPreferencesStore implements UserPreferencesSurface {
|
|
|
128
150
|
* Merge `partial` into the stored preferences (only the provided fields
|
|
129
151
|
* change; `undefined` values are ignored rather than clearing a field) and
|
|
130
152
|
* persist the result with a fresh `updatedAt`. Returns the merged record.
|
|
131
|
-
* An empty `partial` is a no-op that returns the current record.
|
|
153
|
+
* An empty `partial` is a no-op that returns the current record. Throws —
|
|
154
|
+
* without writing — when the current preferences cannot be read, so a
|
|
155
|
+
* transient gateway failure never replaces the stored fields with only the
|
|
156
|
+
* new ones. A payload that exists but cannot be decoded is replaced.
|
|
132
157
|
*/
|
|
133
158
|
async set(
|
|
134
159
|
roomId: string,
|
|
135
160
|
partial: Partial<UserPreferences>,
|
|
136
161
|
): Promise<UserPreferences> {
|
|
137
|
-
const current = (await this.
|
|
162
|
+
const current = (await this.load(roomId)) ?? {};
|
|
138
163
|
const provided = Object.fromEntries(
|
|
139
164
|
Object.entries(partial).filter(([, value]) => value !== undefined),
|
|
140
165
|
);
|
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
PluginTool,
|
|
12
12
|
RuntimeContext,
|
|
13
13
|
} from '../../plugin-api/types';
|
|
14
|
-
import { FORMALITY_LEVELS } from './schema';
|
|
14
|
+
import { FORMALITY_LEVELS, MAX_TONE_LENGTH } from './schema';
|
|
15
15
|
|
|
16
16
|
export const SET_USER_PREFERENCES_TOOL_NAME = 'set_user_preferences';
|
|
17
17
|
|
|
@@ -46,10 +46,11 @@ export const setUserPreferencesSchema = z.object({
|
|
|
46
46
|
),
|
|
47
47
|
tone: z
|
|
48
48
|
.string()
|
|
49
|
+
.max(MAX_TONE_LENGTH)
|
|
49
50
|
.optional()
|
|
50
51
|
.describe(
|
|
51
52
|
"Short tone label your future self will see at the top of every prompt (e.g. 'playful and warm', " +
|
|
52
|
-
|
|
53
|
+
`'concise and dry', 'patient teacher'). Pick descriptive words, not vague ones. Max ${MAX_TONE_LENGTH} characters.`,
|
|
53
54
|
),
|
|
54
55
|
formality: z
|
|
55
56
|
.enum(FORMALITY_LEVELS)
|