@ixo/oracle-runtime-workers 0.2.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 +110 -0
- package/package.json +77 -0
- package/src/attachments/classify.ts +114 -0
- package/src/attachments/content-blocks.ts +51 -0
- package/src/attachments/download.ts +235 -0
- package/src/attachments/extract.ts +241 -0
- package/src/attachments/index.ts +84 -0
- package/src/attachments/magic.ts +133 -0
- package/src/attachments/pipeline.ts +409 -0
- package/src/attachments/retention.ts +205 -0
- package/src/attachments/route.ts +41 -0
- package/src/attachments/sandbox-archive.ts +90 -0
- package/src/attachments/sanitize.ts +35 -0
- package/src/attachments/types.ts +86 -0
- package/src/attachments/view.ts +107 -0
- package/src/core/env.ts +200 -0
- package/src/core/index.ts +513 -0
- package/src/core/llm.ts +842 -0
- package/src/core/main-agent-types.ts +123 -0
- package/src/core/main-agent.ts +399 -0
- package/src/core/manifest.ts +365 -0
- package/src/core/meta-tools.ts +232 -0
- package/src/core/middlewares/byo-history-sanitizer.ts +165 -0
- package/src/core/middlewares/capability-gate.ts +76 -0
- package/src/core/middlewares/index.ts +18 -0
- package/src/core/middlewares/page-context.ts +98 -0
- package/src/core/middlewares/safety-guardrail.ts +117 -0
- package/src/core/middlewares/summarization.ts +99 -0
- package/src/core/middlewares/tool-repetition-guard.ts +125 -0
- package/src/core/middlewares/tool-validation.ts +83 -0
- package/src/core/middlewares/work-status.ts +57 -0
- package/src/core/openrouter-pricing.ts +124 -0
- package/src/core/plugin-loader.ts +192 -0
- package/src/core/plugins/index.ts +6 -0
- package/src/core/plugins/skills/index.ts +8 -0
- package/src/core/plugins/skills/skills-tools.ts +356 -0
- package/src/core/plugins/skills/skills.plugin.ts +113 -0
- package/src/core/plugins/weather/index.ts +8 -0
- package/src/core/plugins/weather/weather-client.ts +153 -0
- package/src/core/plugins/weather/weather-middleware.ts +27 -0
- package/src/core/plugins/weather/weather-sub-agent.ts +65 -0
- package/src/core/plugins/weather/weather-tools.ts +103 -0
- package/src/core/plugins/weather/weather.plugin.ts +227 -0
- package/src/core/prompt-composer.ts +469 -0
- package/src/core/registries.ts +642 -0
- package/src/core/runtime-context.ts +619 -0
- package/src/core/state.ts +108 -0
- package/src/core/sub-agent-fallback.ts +187 -0
- package/src/core/subagent-as-tool.ts +322 -0
- package/src/core/template.ts +245 -0
- package/src/core/utils.ts +246 -0
- package/src/core/wrap-plugin-tool.ts +100 -0
- package/src/do/ambient.ts +266 -0
- package/src/do/contracts.ts +562 -0
- package/src/do/debug-timers.ts +120 -0
- package/src/do/fresh-stub.ts +32 -0
- package/src/do/gateway-retry.ts +73 -0
- package/src/do/secrets-adapter.ts +32 -0
- package/src/do/size-alerts.ts +76 -0
- package/src/do/sse-stream.ts +472 -0
- package/src/do/transcript.ts +192 -0
- package/src/do/turn-body.ts +84 -0
- package/src/do/turn-metadata.ts +146 -0
- package/src/do/ucan-service.ts +339 -0
- package/src/do/user-oracle-do.ts +3035 -0
- package/src/gateway-worker.ts +82 -0
- package/src/index.ts +237 -0
- package/src/llm/byo-adapter.ts +59 -0
- package/src/llm/byo-catalog.ts +520 -0
- package/src/llm/byo-client.ts +280 -0
- package/src/llm/byo-routes.ts +255 -0
- package/src/llm/byo-service.ts +836 -0
- package/src/llm/chatgpt-oauth.ts +430 -0
- package/src/llm/provider-error.ts +283 -0
- package/src/matrix/gateway-do.ts +918 -0
- package/src/matrix/ingest.ts +209 -0
- package/src/matrix/replay-format.ts +34 -0
- package/src/matrix/reply-chain.ts +109 -0
- package/src/matrix/room-state-codec.ts +115 -0
- package/src/matrix/user-homeserver.ts +106 -0
- package/src/matrix/work-status.ts +294 -0
- package/src/memory/session-history-indexer.ts +328 -0
- package/src/owner-store/ixo-vfs-store.ts +570 -0
- package/src/owner-store/matrix-media-store.ts +136 -0
- package/src/owner-store/migrating-store.ts +185 -0
- package/src/owner-store/owner-copy-errors.ts +175 -0
- package/src/owner-store/retry.ts +45 -0
- package/src/owner-store/tus-upload.ts +333 -0
- package/src/owner-store/types.ts +0 -0
- package/src/plugin-api/define-plugin.ts +45 -0
- package/src/plugin-api/index.ts +56 -0
- package/src/plugin-api/oracle-plugin.ts +145 -0
- package/src/plugin-api/tool-helper.ts +82 -0
- package/src/plugin-api/types.ts +816 -0
- package/src/plugin-api/ucan-errors.ts +16 -0
- package/src/plugins/agui/agui-agent.ts +115 -0
- package/src/plugins/agui/agui.plugin.ts +148 -0
- package/src/plugins/agui/index.ts +2 -0
- package/src/plugins/attachments/attachments.plugin.ts +54 -0
- package/src/plugins/attachments/index.ts +6 -0
- package/src/plugins/attachments/view-attachment-tool.ts +91 -0
- package/src/plugins/base64.ts +26 -0
- package/src/plugins/composio/composio-tools.ts +365 -0
- package/src/plugins/composio/composio-ucan.ts +43 -0
- package/src/plugins/composio/composio.plugin.ts +214 -0
- package/src/plugins/composio/index.ts +15 -0
- package/src/plugins/domain-indexer/domain-indexer-agent.ts +57 -0
- package/src/plugins/domain-indexer/domain-indexer-tools.ts +253 -0
- package/src/plugins/domain-indexer/domain-indexer.plugin.ts +100 -0
- package/src/plugins/domain-indexer/index.ts +3 -0
- package/src/plugins/editor/blocknote-bridge.ts +120 -0
- package/src/plugins/editor/content-session.ts +331 -0
- package/src/plugins/editor/content-tools.ts +686 -0
- package/src/plugins/editor/document-model.ts +0 -0
- package/src/plugins/editor/editor-access-denied-tool.ts +58 -0
- package/src/plugins/editor/editor-agent.ts +34 -0
- package/src/plugins/editor/editor-config.ts +64 -0
- package/src/plugins/editor/editor-mx.ts +128 -0
- package/src/plugins/editor/editor.plugin.ts +143 -0
- package/src/plugins/editor/failures.ts +154 -0
- package/src/plugins/editor/index.ts +31 -0
- package/src/plugins/editor/jsdom-shim.ts +55 -0
- package/src/plugins/editor/prompts.ts +59 -0
- package/src/plugins/editor/prop-policy.ts +147 -0
- package/src/plugins/editor/provider.ts +386 -0
- package/src/plugins/editor/room-membership.ts +95 -0
- package/src/plugins/editor/standalone-editor-tool.ts +200 -0
- package/src/plugins/firecrawl/firecrawl-agent.ts +93 -0
- package/src/plugins/firecrawl/firecrawl-tools.ts +220 -0
- package/src/plugins/firecrawl/firecrawl.plugin.ts +105 -0
- package/src/plugins/firecrawl/index.ts +15 -0
- package/src/plugins/flows/action-metadata.ts +3505 -0
- package/src/plugins/flows/actions.ts +190 -0
- package/src/plugins/flows/edit.ts +314 -0
- package/src/plugins/flows/errors.ts +38 -0
- package/src/plugins/flows/explain.ts +43 -0
- package/src/plugins/flows/flow-doc.ts +111 -0
- package/src/plugins/flows/flows.plugin.ts +129 -0
- package/src/plugins/flows/forms.ts +330 -0
- package/src/plugins/flows/index.ts +2 -0
- package/src/plugins/flows/linkage.ts +147 -0
- package/src/plugins/flows/port-types.ts +53 -0
- package/src/plugins/flows/prompts.ts +65 -0
- package/src/plugins/flows/read.ts +392 -0
- package/src/plugins/flows/references.ts +66 -0
- package/src/plugins/flows/test-support.ts +165 -0
- package/src/plugins/flows/tools/authoring.ts +516 -0
- package/src/plugins/flows/tools/discovery.ts +119 -0
- package/src/plugins/flows/tools/forms.ts +142 -0
- package/src/plugins/flows/tools/inspect.ts +165 -0
- package/src/plugins/flows/tools/linkage.ts +117 -0
- package/src/plugins/flows/tools/settings.ts +184 -0
- package/src/plugins/flows/translator.ts +328 -0
- package/src/plugins/flows/types.ts +210 -0
- package/src/plugins/flows/ydoc-helpers.ts +543 -0
- package/src/plugins/index.ts +70 -0
- package/src/plugins/mcp-call-timeout.ts +38 -0
- package/src/plugins/mcp-tool-adapter.ts +87 -0
- package/src/plugins/memory/index.ts +17 -0
- package/src/plugins/memory/memory-tools.ts +463 -0
- package/src/plugins/memory/memory-ucan.ts +51 -0
- package/src/plugins/memory/memory.plugin.ts +122 -0
- package/src/plugins/memory/types.ts +147 -0
- package/src/plugins/portal/action-log.ts +34 -0
- package/src/plugins/portal/index.ts +2 -0
- package/src/plugins/portal/portal.plugin.ts +203 -0
- package/src/plugins/sandbox/index.ts +34 -0
- package/src/plugins/sandbox/sandbox-bridge.ts +295 -0
- package/src/plugins/sandbox/sandbox-mcp.ts +127 -0
- package/src/plugins/sandbox/sandbox-write-blob.ts +103 -0
- package/src/plugins/sandbox/sandbox.plugin.ts +599 -0
- package/src/plugins/tasks/index.ts +17 -0
- package/src/plugins/tasks/manifest.ts +81 -0
- package/src/plugins/tasks/middleware.ts +219 -0
- package/src/plugins/tasks/tasks-tools.ts +414 -0
- package/src/plugins/tasks/tasks.plugin.ts +74 -0
- package/src/plugins/ucan-failure.ts +133 -0
- package/src/plugins/user-preferences/index.ts +22 -0
- package/src/plugins/user-preferences/schema.ts +31 -0
- package/src/plugins/user-preferences/user-preferences-store.ts +172 -0
- package/src/plugins/user-preferences/user-preferences-tool.ts +131 -0
- package/src/plugins/user-preferences/user-preferences.plugin.ts +137 -0
- package/src/plugins/vfs/index.ts +40 -0
- package/src/plugins/vfs/vfs-auth.ts +56 -0
- package/src/plugins/vfs/vfs-client.ts +655 -0
- package/src/plugins/vfs/vfs-content.ts +181 -0
- package/src/plugins/vfs/vfs-errors.ts +163 -0
- package/src/plugins/vfs/vfs-sandbox-tools.ts +272 -0
- package/src/plugins/vfs/vfs-tools.ts +635 -0
- package/src/plugins/vfs/vfs.plugin.ts +201 -0
- package/src/realtime/frontend-call-registry.ts +163 -0
- package/src/realtime/realtime-endpoint.ts +619 -0
- package/src/realtime/session-socket-hub.ts +249 -0
- package/src/realtime/socket-io-codec.ts +187 -0
- package/src/secrets/jwe.ts +74 -0
- package/src/secrets/pin-cipher.ts +43 -0
- package/src/secrets/secrets-service.ts +319 -0
- package/src/shell/app.ts +564 -0
- package/src/shell/auth.ts +290 -0
- package/src/sqlite/blob-codec.ts +100 -0
- package/src/sqlite/blob-compactor.ts +146 -0
- package/src/sqlite/cache-config.ts +45 -0
- package/src/sqlite/database.ts +643 -0
- package/src/sqlite/do-vfs.ts +1485 -0
- package/src/sqlite/index.ts +61 -0
- package/src/sqlite/serialization.ts +370 -0
- package/src/sqlite/sessions-store.ts +311 -0
- package/src/sqlite/sqlite-saver.ts +919 -0
- package/src/sqlite/vacuum-policy.ts +77 -0
- package/src/sqlite/wa-sqlite-loader.ts +123 -0
- package/src/sqlite/wasm.d.ts +10 -0
- package/src/tasks/schedule.ts +174 -0
- package/src/tasks/scheduler.ts +753 -0
- package/src/tasks/spec.ts +125 -0
- package/src/tasks/store.ts +351 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-room user preferences, persisted as the `ixo.room.state` /
|
|
3
|
+
* `user_prefs` state event of the user↔oracle room — the SAME event, in the
|
|
4
|
+
* SAME Node-compatible envelope, that the Node runtime's
|
|
5
|
+
* `UserPreferencesService` reads and writes. A user migrated from Node keeps
|
|
6
|
+
* their preferences; a user who sets them here keeps them if they ever move
|
|
7
|
+
* back.
|
|
8
|
+
*
|
|
9
|
+
* The store is a host object (one per `UserOracleDO`): the turn preparer
|
|
10
|
+
* reads through it to hydrate `state.userPreferences` before the agent is
|
|
11
|
+
* built, and the `set_user_preferences` tool writes through it, so the
|
|
12
|
+
* 5-minute read cache is invalidated by the very write that changes it —
|
|
13
|
+
* the Node service's cache-manager semantics, without a shared singleton.
|
|
14
|
+
*/
|
|
15
|
+
import type { UserPreferences } from '../../core/state';
|
|
16
|
+
import {
|
|
17
|
+
decodeRoomStateContent,
|
|
18
|
+
encodeRoomStateContent,
|
|
19
|
+
ROOM_STATE_EVENT_TYPE,
|
|
20
|
+
} from '../../matrix/room-state-codec';
|
|
21
|
+
import type { Logger, UserPreferencesSurface } from '../../plugin-api/types';
|
|
22
|
+
import {
|
|
23
|
+
USER_PREFS_STATE_KEY,
|
|
24
|
+
UserPreferencesSchema,
|
|
25
|
+
type StoredUserPreferences,
|
|
26
|
+
} from './schema';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The slice of the Matrix gateway the store needs. The gateway Durable
|
|
30
|
+
* Object stub satisfies it structurally (`getRoomStateEvent` /
|
|
31
|
+
* `sendStateEvent` are its RPC methods); tests pass an in-memory map.
|
|
32
|
+
*/
|
|
33
|
+
export interface RoomStateAccess {
|
|
34
|
+
getRoomStateEvent(
|
|
35
|
+
roomId: string,
|
|
36
|
+
type: string,
|
|
37
|
+
stateKey?: string,
|
|
38
|
+
): Promise<string | null>;
|
|
39
|
+
sendStateEvent(
|
|
40
|
+
roomId: string,
|
|
41
|
+
type: string,
|
|
42
|
+
content: string,
|
|
43
|
+
stateKey?: string,
|
|
44
|
+
): Promise<string>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface UserPreferencesStoreOptions {
|
|
48
|
+
/** Read-cache TTL. Node caches for 5 minutes; same default here. */
|
|
49
|
+
ttlMs?: number;
|
|
50
|
+
logger?: Logger;
|
|
51
|
+
/** Clock, injectable for tests. */
|
|
52
|
+
now?: () => number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const DEFAULT_TTL_MS = 5 * 60 * 1000;
|
|
56
|
+
/** Bound the cache — one store serves one user, but rooms can accrue. */
|
|
57
|
+
const MAX_CACHE_ENTRIES = 64;
|
|
58
|
+
|
|
59
|
+
interface CacheEntry {
|
|
60
|
+
value: StoredUserPreferences | undefined;
|
|
61
|
+
expiresAt: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class UserPreferencesStore implements UserPreferencesSurface {
|
|
65
|
+
private readonly cache = new Map<string, CacheEntry>();
|
|
66
|
+
|
|
67
|
+
private readonly ttlMs: number;
|
|
68
|
+
|
|
69
|
+
private readonly logger: Logger | undefined;
|
|
70
|
+
|
|
71
|
+
private readonly now: () => number;
|
|
72
|
+
|
|
73
|
+
constructor(
|
|
74
|
+
private readonly access: RoomStateAccess,
|
|
75
|
+
options: UserPreferencesStoreOptions = {},
|
|
76
|
+
) {
|
|
77
|
+
this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
78
|
+
this.logger = options.logger;
|
|
79
|
+
this.now = options.now ?? (() => Date.now());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The room's preferences, or `undefined` when none are set, the payload
|
|
84
|
+
* is unreadable, or the read failed (each logged, never thrown — a
|
|
85
|
+
* preferences hiccup must not fail a turn). Misses are cached too, so a
|
|
86
|
+
* user without preferences costs one state read per TTL, not per turn.
|
|
87
|
+
*/
|
|
88
|
+
async get(roomId: string): Promise<UserPreferences | undefined> {
|
|
89
|
+
const hit = this.cache.get(roomId);
|
|
90
|
+
if (hit && hit.expiresAt > this.now()) return hit.value;
|
|
91
|
+
|
|
92
|
+
let decoded: unknown;
|
|
93
|
+
try {
|
|
94
|
+
const json = await this.access.getRoomStateEvent(
|
|
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);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
this.logger?.warn(
|
|
105
|
+
`[user-preferences] failed to load prefs for room ${roomId}: ${
|
|
106
|
+
error instanceof Error ? error.message : String(error)
|
|
107
|
+
}`,
|
|
108
|
+
);
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (decoded === null) {
|
|
113
|
+
this.remember(roomId, undefined);
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
const parsed = UserPreferencesSchema.safeParse(decoded);
|
|
117
|
+
if (!parsed.success) {
|
|
118
|
+
this.logger?.warn(
|
|
119
|
+
`[user-preferences] invalid prefs payload for room ${roomId}: ${parsed.error.message}`,
|
|
120
|
+
);
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
this.remember(roomId, parsed.data);
|
|
124
|
+
return parsed.data;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Merge `partial` into the stored preferences (only the provided fields
|
|
129
|
+
* change; `undefined` values are ignored rather than clearing a field) and
|
|
130
|
+
* persist the result with a fresh `updatedAt`. Returns the merged record.
|
|
131
|
+
* An empty `partial` is a no-op that returns the current record.
|
|
132
|
+
*/
|
|
133
|
+
async set(
|
|
134
|
+
roomId: string,
|
|
135
|
+
partial: Partial<UserPreferences>,
|
|
136
|
+
): Promise<UserPreferences> {
|
|
137
|
+
const current = (await this.get(roomId)) ?? {};
|
|
138
|
+
const provided = Object.fromEntries(
|
|
139
|
+
Object.entries(partial).filter(([, value]) => value !== undefined),
|
|
140
|
+
);
|
|
141
|
+
if (Object.keys(provided).length === 0) return current;
|
|
142
|
+
|
|
143
|
+
const merged = UserPreferencesSchema.parse({
|
|
144
|
+
...current,
|
|
145
|
+
...provided,
|
|
146
|
+
updatedAt: new Date(this.now()).toISOString(),
|
|
147
|
+
});
|
|
148
|
+
await this.access.sendStateEvent(
|
|
149
|
+
roomId,
|
|
150
|
+
ROOM_STATE_EVENT_TYPE,
|
|
151
|
+
JSON.stringify(await encodeRoomStateContent(merged)),
|
|
152
|
+
USER_PREFS_STATE_KEY,
|
|
153
|
+
);
|
|
154
|
+
this.invalidate(roomId);
|
|
155
|
+
return merged;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
invalidate(roomId: string): void {
|
|
159
|
+
this.cache.delete(roomId);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private remember(
|
|
163
|
+
roomId: string,
|
|
164
|
+
value: StoredUserPreferences | undefined,
|
|
165
|
+
): void {
|
|
166
|
+
if (this.cache.size >= MAX_CACHE_ENTRIES) {
|
|
167
|
+
const oldest = this.cache.keys().next().value;
|
|
168
|
+
if (oldest !== undefined) this.cache.delete(oldest);
|
|
169
|
+
}
|
|
170
|
+
this.cache.set(roomId, { value, expiresAt: this.now() + this.ttlMs });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `set_user_preferences` — the Node runtime's tool, verbatim in schema and
|
|
3
|
+
* description (the description is prompt material the model has been tuned
|
|
4
|
+
* against; keep it in sync with `packages/oracle-runtime`). The handler
|
|
5
|
+
* writes through the host's `ctx.preferences` surface for the active room.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { tool } from '../../plugin-api/tool-helper';
|
|
9
|
+
import type {
|
|
10
|
+
Logger,
|
|
11
|
+
PluginTool,
|
|
12
|
+
RuntimeContext,
|
|
13
|
+
} from '../../plugin-api/types';
|
|
14
|
+
import { FORMALITY_LEVELS } from './schema';
|
|
15
|
+
|
|
16
|
+
export const SET_USER_PREFERENCES_TOOL_NAME = 'set_user_preferences';
|
|
17
|
+
|
|
18
|
+
const NOOP_LOGGER: Logger = {
|
|
19
|
+
log: () => undefined,
|
|
20
|
+
warn: () => undefined,
|
|
21
|
+
error: () => undefined,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const setUserPreferencesSchema = z.object({
|
|
25
|
+
agentName: z
|
|
26
|
+
.string()
|
|
27
|
+
.max(80)
|
|
28
|
+
.optional()
|
|
29
|
+
.describe(
|
|
30
|
+
"What the user wants you (the agent) to be called (e.g. 'Companion'). Use exactly the form the user asked for. Max 80 characters.",
|
|
31
|
+
),
|
|
32
|
+
userName: z
|
|
33
|
+
.string()
|
|
34
|
+
.max(80)
|
|
35
|
+
.optional()
|
|
36
|
+
.describe(
|
|
37
|
+
"What the user wants you to call them (e.g. 'Yousef'). Use exactly the form the user asked for. Max 80 characters.",
|
|
38
|
+
),
|
|
39
|
+
language: z
|
|
40
|
+
.string()
|
|
41
|
+
.max(20)
|
|
42
|
+
.optional()
|
|
43
|
+
.describe(
|
|
44
|
+
"Preferred reply language as a free-form code or name (e.g. 'en', 'ar', 'Arabic', 'Egyptian Arabic'). " +
|
|
45
|
+
'Be precise — if the user specified a dialect or variant, capture that. Max 20 characters.',
|
|
46
|
+
),
|
|
47
|
+
tone: z
|
|
48
|
+
.string()
|
|
49
|
+
.optional()
|
|
50
|
+
.describe(
|
|
51
|
+
"Short tone label your future self will see at the top of every prompt (e.g. 'playful and warm', " +
|
|
52
|
+
"'concise and dry', 'patient teacher'). Pick descriptive words, not vague ones.",
|
|
53
|
+
),
|
|
54
|
+
formality: z
|
|
55
|
+
.enum(FORMALITY_LEVELS)
|
|
56
|
+
.optional()
|
|
57
|
+
.describe(
|
|
58
|
+
'How formal the replies should be. Pick the value closest to what the user actually wants, ' +
|
|
59
|
+
'not what the request literally said.',
|
|
60
|
+
),
|
|
61
|
+
customInstructions: z
|
|
62
|
+
.string()
|
|
63
|
+
.max(2000)
|
|
64
|
+
.optional()
|
|
65
|
+
.describe(
|
|
66
|
+
'Free-form custom instructions your FUTURE SELF will read every turn. Treat this like writing ' +
|
|
67
|
+
'a system prompt: be specific, complete, and unambiguous. Include the WHY when it matters ' +
|
|
68
|
+
"(e.g. 'User has dyslexia — keep sentences short and avoid long lists'). " +
|
|
69
|
+
'When updating, MERGE with existing instructions rather than replacing — read what is already ' +
|
|
70
|
+
'there (visible in your current system prompt under "User Preferences") and write the full new ' +
|
|
71
|
+
'instruction set, preserving anything still relevant. Max 2000 characters.',
|
|
72
|
+
),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export const SET_USER_PREFERENCES_DESCRIPTION =
|
|
76
|
+
"Update the user's preferences. Call this whenever the user asks you to change how you behave " +
|
|
77
|
+
"(e.g. 'call me Yousef', 'reply in Arabic', 'be more casual').\n\n" +
|
|
78
|
+
'IMPORTANT — you are writing instructions for your FUTURE SELF. ' +
|
|
79
|
+
'On every later turn this exact text will be injected back into your system prompt and you will read it cold, ' +
|
|
80
|
+
'with no memory of this conversation or why the user asked. So:\n' +
|
|
81
|
+
" • Be specific and self-contained. Don't write 'be more casual' — write 'Use casual, conversational " +
|
|
82
|
+
"tone. Avoid corporate phrasing. Use contractions. Drop filler words.'\n" +
|
|
83
|
+
" • Capture intent, not just the literal request. If the user says 'stop being so formal', infer the " +
|
|
84
|
+
'positive form: what should you do instead?\n' +
|
|
85
|
+
' • Preserve existing instructions when adding new ones — only overwrite the fields you mean to change. ' +
|
|
86
|
+
'Other fields are kept as-is.\n' +
|
|
87
|
+
" • If unsure between two interpretations, pick the more specific one — your future self can't ask back.\n\n" +
|
|
88
|
+
"Note: changes apply from the user's NEXT message (the current turn's prompt is already built).";
|
|
89
|
+
|
|
90
|
+
export interface CreateSetUserPreferencesToolOptions {
|
|
91
|
+
logger?: Logger;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Builds the `set_user_preferences` tool. Merges the supplied partial
|
|
96
|
+
* preferences into the active room's stored preferences; only the provided
|
|
97
|
+
* fields change. Errors come back as descriptive strings (never thrown) so
|
|
98
|
+
* the agent can recover — the Node tool's contract.
|
|
99
|
+
*/
|
|
100
|
+
export function createSetUserPreferencesTool(
|
|
101
|
+
options: CreateSetUserPreferencesToolOptions = {},
|
|
102
|
+
): PluginTool {
|
|
103
|
+
const logger = options.logger ?? NOOP_LOGGER;
|
|
104
|
+
|
|
105
|
+
return tool(
|
|
106
|
+
async (rawArgs: unknown, ctx: RuntimeContext) => {
|
|
107
|
+
const args = setUserPreferencesSchema.parse(rawArgs);
|
|
108
|
+
const roomId = ctx.session.roomId;
|
|
109
|
+
if (!roomId) {
|
|
110
|
+
return '[Error updating user preferences: no active room on this session]';
|
|
111
|
+
}
|
|
112
|
+
const surface = ctx.preferences;
|
|
113
|
+
if (!surface) {
|
|
114
|
+
return '[Error updating user preferences: preferences are not available on this host]';
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const merged = await surface.set(roomId, args);
|
|
118
|
+
return `Updated. New preferences: ${JSON.stringify(merged, null, 2)}`;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
121
|
+
logger.error(`set_user_preferences error: ${msg}`);
|
|
122
|
+
return `[Error updating user preferences: ${msg}]`;
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: SET_USER_PREFERENCES_TOOL_NAME,
|
|
127
|
+
description: SET_USER_PREFERENCES_DESCRIPTION,
|
|
128
|
+
schema: setUserPreferencesSchema,
|
|
129
|
+
},
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Preferences plugin — the Workers port of
|
|
3
|
+
* `packages/oracle-runtime/src/plugins/user-preferences`.
|
|
4
|
+
*
|
|
5
|
+
* - `state.userPreferences` is hydrated by the user object's turn preparer
|
|
6
|
+
* (through the host `UserPreferencesStore`) BEFORE the agent is built,
|
|
7
|
+
* so the system prompt sees the value on turn 1 — the Node AgentBuilder
|
|
8
|
+
* contract.
|
|
9
|
+
* - Exposes `set_user_preferences`, which the agent calls when the user
|
|
10
|
+
* asks to change behavioural preferences (tone, language, formality,
|
|
11
|
+
* what to call the agent / the user).
|
|
12
|
+
* - Ships `GET /user-preferences` (the NestJS controller became a
|
|
13
|
+
* `getRoutes()` entry): the caller's preferences, or `null`.
|
|
14
|
+
*
|
|
15
|
+
* Storage is the room's `ixo.room.state` / `user_prefs` event in the Node
|
|
16
|
+
* envelope, so preferences survive a Node ⇄ Workers migration intact.
|
|
17
|
+
*/
|
|
18
|
+
import type { OracleWorkerEnv } from '../../do/contracts';
|
|
19
|
+
import { OraclePlugin } from '../../plugin-api/oracle-plugin';
|
|
20
|
+
import type { PluginRoute } from '../../shell/app';
|
|
21
|
+
import type {
|
|
22
|
+
PluginContext,
|
|
23
|
+
PluginManifest,
|
|
24
|
+
PluginTool,
|
|
25
|
+
} from '../../plugin-api/types';
|
|
26
|
+
import {
|
|
27
|
+
UserPreferencesStore,
|
|
28
|
+
type RoomStateAccess,
|
|
29
|
+
} from './user-preferences-store';
|
|
30
|
+
import {
|
|
31
|
+
createSetUserPreferencesTool,
|
|
32
|
+
SET_USER_PREFERENCES_TOOL_NAME,
|
|
33
|
+
} from './user-preferences-tool';
|
|
34
|
+
|
|
35
|
+
/** What the route needs from the Matrix gateway: room lookup + room state. */
|
|
36
|
+
export interface PreferencesGateway extends RoomStateAccess {
|
|
37
|
+
resolveUserRoom(
|
|
38
|
+
userDid: string,
|
|
39
|
+
): Promise<{ roomId: string; alias: string } | null>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface UserPreferencesPluginOptions {
|
|
43
|
+
/**
|
|
44
|
+
* Resolves the oracle's Matrix gateway for a request. Defaults to the
|
|
45
|
+
* `MATRIX_GATEWAY` Durable Object stub; tests inject a fake.
|
|
46
|
+
*/
|
|
47
|
+
gatewayFor?: (env: OracleWorkerEnv) => PreferencesGateway;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function defaultGatewayFor(env: OracleWorkerEnv): PreferencesGateway {
|
|
51
|
+
const id = env.MATRIX_GATEWAY.idFromName(env.ORACLE_DID);
|
|
52
|
+
return env.MATRIX_GATEWAY.get(id);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const manifest: PluginManifest = {
|
|
56
|
+
title: 'User Preferences',
|
|
57
|
+
summary:
|
|
58
|
+
'Behavioral preferences — how the user wants you to respond (tone, language, formality, what to call you).',
|
|
59
|
+
whenToUse: [
|
|
60
|
+
'User states how they want you to behave: "be more terse", "respond in Spanish", "call me Alex", "stop using emojis".',
|
|
61
|
+
'User asks to change the voice, formality, or language of your replies — save it so it persists across sessions, not just this turn.',
|
|
62
|
+
],
|
|
63
|
+
whenNotToUse: [
|
|
64
|
+
'Facts about who the user is (name, role, project) — those go to memory, not preferences.',
|
|
65
|
+
'Artifacts you have produced or how the user reacted to them — also memory, not preferences.',
|
|
66
|
+
'One-turn formatting requests ("just for this answer, use bullets") — adapt locally without saving.',
|
|
67
|
+
],
|
|
68
|
+
examples: [
|
|
69
|
+
{
|
|
70
|
+
user: 'From now on, please respond in Spanish.',
|
|
71
|
+
thought: 'Behavioral preference about language — save it so it persists.',
|
|
72
|
+
tool: SET_USER_PREFERENCES_TOOL_NAME,
|
|
73
|
+
args: { language: 'Spanish' },
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
user: 'Be more concise — drop the explanations.',
|
|
77
|
+
thought: 'Preference about tone/length, not a single-turn request.',
|
|
78
|
+
tool: SET_USER_PREFERENCES_TOOL_NAME,
|
|
79
|
+
args: { tone: 'concise' },
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
visibility: 'always',
|
|
83
|
+
stability: 'stable',
|
|
84
|
+
category: 'core',
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export class UserPreferencesPlugin extends OraclePlugin {
|
|
88
|
+
readonly name = 'user-preferences';
|
|
89
|
+
|
|
90
|
+
readonly version = '1.0.0';
|
|
91
|
+
|
|
92
|
+
readonly manifest = manifest;
|
|
93
|
+
|
|
94
|
+
private readonly gatewayFor: (env: OracleWorkerEnv) => PreferencesGateway;
|
|
95
|
+
|
|
96
|
+
constructor(options: UserPreferencesPluginOptions = {}) {
|
|
97
|
+
super();
|
|
98
|
+
this.gatewayFor = options.gatewayFor ?? defaultGatewayFor;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
override getTools(ctx: PluginContext): PluginTool[] {
|
|
102
|
+
return [createSetUserPreferencesTool({ logger: ctx.logger })];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
override getRoutes(ctx: PluginContext): PluginRoute[] {
|
|
106
|
+
return [
|
|
107
|
+
{
|
|
108
|
+
method: 'GET',
|
|
109
|
+
path: '/user-preferences',
|
|
110
|
+
handler: async (_request, env, { auth }) => {
|
|
111
|
+
if (!auth) {
|
|
112
|
+
return Response.json(
|
|
113
|
+
{ statusCode: 401, message: 'Unauthorized' },
|
|
114
|
+
{ status: 401 },
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const gateway = this.gatewayFor(env);
|
|
118
|
+
const room = await gateway.resolveUserRoom(auth.userDid);
|
|
119
|
+
if (!room) {
|
|
120
|
+
ctx.logger.warn(
|
|
121
|
+
`[user-preferences] could not resolve user↔oracle room for userDid=${auth.userDid}`,
|
|
122
|
+
);
|
|
123
|
+
return Response.json(null);
|
|
124
|
+
}
|
|
125
|
+
const store = new UserPreferencesStore(gateway, {
|
|
126
|
+
logger: ctx.logger,
|
|
127
|
+
});
|
|
128
|
+
const prefs = await store.get(room.roomId);
|
|
129
|
+
ctx.logger.log(
|
|
130
|
+
`[user-preferences] fetched for userDid=${auth.userDid}, roomId=${room.roomId}: ${prefs ? 'Found' : 'null'}`,
|
|
131
|
+
);
|
|
132
|
+
return Response.json(prefs ?? null);
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export { VfsPlugin, type VfsConfig, type VfsPluginOptions } from './vfs.plugin';
|
|
2
|
+
export {
|
|
3
|
+
buildClient,
|
|
4
|
+
createVfsTools,
|
|
5
|
+
invalidArgs,
|
|
6
|
+
validatePath,
|
|
7
|
+
type CreateVfsToolsDeps,
|
|
8
|
+
} from './vfs-tools';
|
|
9
|
+
export {
|
|
10
|
+
createVfsSandboxTools,
|
|
11
|
+
type CreateVfsSandboxToolsDeps,
|
|
12
|
+
} from './vfs-sandbox-tools';
|
|
13
|
+
export { vfsBearer, type VfsBearerResult } from './vfs-auth';
|
|
14
|
+
export {
|
|
15
|
+
VfsClient,
|
|
16
|
+
type VfsBatchItemResult,
|
|
17
|
+
type VfsClientOptions,
|
|
18
|
+
type VfsContentBytes,
|
|
19
|
+
type VfsEditResult,
|
|
20
|
+
type VfsFileStat,
|
|
21
|
+
type VfsGlobMatch,
|
|
22
|
+
type VfsMintFn,
|
|
23
|
+
type VfsPublicResult,
|
|
24
|
+
type VfsReadWindow,
|
|
25
|
+
type VfsSearchHit,
|
|
26
|
+
type VfsSearchResult,
|
|
27
|
+
type VfsTreeEntry,
|
|
28
|
+
} from './vfs-client';
|
|
29
|
+
export { isTextMime, readForAgent } from './vfs-content';
|
|
30
|
+
export {
|
|
31
|
+
isAlreadyExistsConflict,
|
|
32
|
+
isWriteConflict,
|
|
33
|
+
mapVfsError,
|
|
34
|
+
NO_ACCESS_MESSAGE,
|
|
35
|
+
noAccessMessage,
|
|
36
|
+
VfsAuthError,
|
|
37
|
+
VfsHttpError,
|
|
38
|
+
type VfsAbility,
|
|
39
|
+
type VfsAuthErrorKind,
|
|
40
|
+
} from './vfs-errors';
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { RuntimeContext } from '../../plugin-api/types';
|
|
2
|
+
import type { VfsAbility, VfsAuthErrorKind } from './vfs-errors';
|
|
3
|
+
import type { VfsConfig } from './vfs.plugin';
|
|
4
|
+
|
|
5
|
+
/** Personal-namespace resource the user delegates over. */
|
|
6
|
+
const VFS_RESOURCE = 'ixo:filesystem';
|
|
7
|
+
|
|
8
|
+
/** Invocations are single-use per call, so a short TTL suffices. */
|
|
9
|
+
const INVOCATION_TTL_SECONDS = 60;
|
|
10
|
+
|
|
11
|
+
export type VfsBearerResult =
|
|
12
|
+
| { bearer: string }
|
|
13
|
+
| { error: VfsAuthErrorKind; detail?: string };
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolve a fresh, single-use VFS bearer for one operation via the two-hop
|
|
17
|
+
* UCAN flow (the same flow `src/owner-store/ixo-vfs-store.ts` uses):
|
|
18
|
+
*
|
|
19
|
+
* 1. Fetch the user's delegation for this oracle from the store worker
|
|
20
|
+
* (`getServiceDelegation`) over `ixo:filesystem`, requiring an ability
|
|
21
|
+
* that covers `ability`.
|
|
22
|
+
* 2. Mint an invocation proved by that delegation, attenuated to
|
|
23
|
+
* `{ can: ability, with: <granted resource> }`.
|
|
24
|
+
*
|
|
25
|
+
* Non-throwing: every failure is returned as `{ error, detail? }` so the
|
|
26
|
+
* client can surface an agent-actionable message instead of a stack.
|
|
27
|
+
*/
|
|
28
|
+
export async function vfsBearer(
|
|
29
|
+
rtCtx: RuntimeContext,
|
|
30
|
+
cfg: VfsConfig,
|
|
31
|
+
ability: VfsAbility,
|
|
32
|
+
targetResource?: string,
|
|
33
|
+
): Promise<VfsBearerResult> {
|
|
34
|
+
const delegation = await rtCtx.ucan.getServiceDelegation(rtCtx.user.did, {
|
|
35
|
+
storeUrl: cfg.UCAN_STORE_URL,
|
|
36
|
+
resource: VFS_RESOURCE,
|
|
37
|
+
requiredAbility: ability,
|
|
38
|
+
});
|
|
39
|
+
if ('error' in delegation) {
|
|
40
|
+
// `{ error: 'no-delegation' | 'store-error', detail? }` widens cleanly to
|
|
41
|
+
// `VfsBearerResult` (both kinds are in `VfsAuthErrorKind`).
|
|
42
|
+
return delegation;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const minted = await rtCtx.ucan.createInvocationFromDelegation(
|
|
46
|
+
delegation.token,
|
|
47
|
+
cfg.VFS_BASE_URL,
|
|
48
|
+
{ can: ability, with: targetResource ?? delegation.with },
|
|
49
|
+
{ maxTtlSeconds: INVOCATION_TTL_SECONDS },
|
|
50
|
+
);
|
|
51
|
+
if ('error' in minted) {
|
|
52
|
+
return { error: 'mint-failed', detail: minted.error };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { bearer: minted.invocation };
|
|
56
|
+
}
|