@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.
Files changed (215) hide show
  1. package/README.md +110 -0
  2. package/package.json +77 -0
  3. package/src/attachments/classify.ts +114 -0
  4. package/src/attachments/content-blocks.ts +51 -0
  5. package/src/attachments/download.ts +235 -0
  6. package/src/attachments/extract.ts +241 -0
  7. package/src/attachments/index.ts +84 -0
  8. package/src/attachments/magic.ts +133 -0
  9. package/src/attachments/pipeline.ts +409 -0
  10. package/src/attachments/retention.ts +205 -0
  11. package/src/attachments/route.ts +41 -0
  12. package/src/attachments/sandbox-archive.ts +90 -0
  13. package/src/attachments/sanitize.ts +35 -0
  14. package/src/attachments/types.ts +86 -0
  15. package/src/attachments/view.ts +107 -0
  16. package/src/core/env.ts +200 -0
  17. package/src/core/index.ts +513 -0
  18. package/src/core/llm.ts +842 -0
  19. package/src/core/main-agent-types.ts +123 -0
  20. package/src/core/main-agent.ts +399 -0
  21. package/src/core/manifest.ts +365 -0
  22. package/src/core/meta-tools.ts +232 -0
  23. package/src/core/middlewares/byo-history-sanitizer.ts +165 -0
  24. package/src/core/middlewares/capability-gate.ts +76 -0
  25. package/src/core/middlewares/index.ts +18 -0
  26. package/src/core/middlewares/page-context.ts +98 -0
  27. package/src/core/middlewares/safety-guardrail.ts +117 -0
  28. package/src/core/middlewares/summarization.ts +99 -0
  29. package/src/core/middlewares/tool-repetition-guard.ts +125 -0
  30. package/src/core/middlewares/tool-validation.ts +83 -0
  31. package/src/core/middlewares/work-status.ts +57 -0
  32. package/src/core/openrouter-pricing.ts +124 -0
  33. package/src/core/plugin-loader.ts +192 -0
  34. package/src/core/plugins/index.ts +6 -0
  35. package/src/core/plugins/skills/index.ts +8 -0
  36. package/src/core/plugins/skills/skills-tools.ts +356 -0
  37. package/src/core/plugins/skills/skills.plugin.ts +113 -0
  38. package/src/core/plugins/weather/index.ts +8 -0
  39. package/src/core/plugins/weather/weather-client.ts +153 -0
  40. package/src/core/plugins/weather/weather-middleware.ts +27 -0
  41. package/src/core/plugins/weather/weather-sub-agent.ts +65 -0
  42. package/src/core/plugins/weather/weather-tools.ts +103 -0
  43. package/src/core/plugins/weather/weather.plugin.ts +227 -0
  44. package/src/core/prompt-composer.ts +469 -0
  45. package/src/core/registries.ts +642 -0
  46. package/src/core/runtime-context.ts +619 -0
  47. package/src/core/state.ts +108 -0
  48. package/src/core/sub-agent-fallback.ts +187 -0
  49. package/src/core/subagent-as-tool.ts +322 -0
  50. package/src/core/template.ts +245 -0
  51. package/src/core/utils.ts +246 -0
  52. package/src/core/wrap-plugin-tool.ts +100 -0
  53. package/src/do/ambient.ts +266 -0
  54. package/src/do/contracts.ts +562 -0
  55. package/src/do/debug-timers.ts +120 -0
  56. package/src/do/fresh-stub.ts +32 -0
  57. package/src/do/gateway-retry.ts +73 -0
  58. package/src/do/secrets-adapter.ts +32 -0
  59. package/src/do/size-alerts.ts +76 -0
  60. package/src/do/sse-stream.ts +472 -0
  61. package/src/do/transcript.ts +192 -0
  62. package/src/do/turn-body.ts +84 -0
  63. package/src/do/turn-metadata.ts +146 -0
  64. package/src/do/ucan-service.ts +339 -0
  65. package/src/do/user-oracle-do.ts +3035 -0
  66. package/src/gateway-worker.ts +82 -0
  67. package/src/index.ts +237 -0
  68. package/src/llm/byo-adapter.ts +59 -0
  69. package/src/llm/byo-catalog.ts +520 -0
  70. package/src/llm/byo-client.ts +280 -0
  71. package/src/llm/byo-routes.ts +255 -0
  72. package/src/llm/byo-service.ts +836 -0
  73. package/src/llm/chatgpt-oauth.ts +430 -0
  74. package/src/llm/provider-error.ts +283 -0
  75. package/src/matrix/gateway-do.ts +918 -0
  76. package/src/matrix/ingest.ts +209 -0
  77. package/src/matrix/replay-format.ts +34 -0
  78. package/src/matrix/reply-chain.ts +109 -0
  79. package/src/matrix/room-state-codec.ts +115 -0
  80. package/src/matrix/user-homeserver.ts +106 -0
  81. package/src/matrix/work-status.ts +294 -0
  82. package/src/memory/session-history-indexer.ts +328 -0
  83. package/src/owner-store/ixo-vfs-store.ts +570 -0
  84. package/src/owner-store/matrix-media-store.ts +136 -0
  85. package/src/owner-store/migrating-store.ts +185 -0
  86. package/src/owner-store/owner-copy-errors.ts +175 -0
  87. package/src/owner-store/retry.ts +45 -0
  88. package/src/owner-store/tus-upload.ts +333 -0
  89. package/src/owner-store/types.ts +0 -0
  90. package/src/plugin-api/define-plugin.ts +45 -0
  91. package/src/plugin-api/index.ts +56 -0
  92. package/src/plugin-api/oracle-plugin.ts +145 -0
  93. package/src/plugin-api/tool-helper.ts +82 -0
  94. package/src/plugin-api/types.ts +816 -0
  95. package/src/plugin-api/ucan-errors.ts +16 -0
  96. package/src/plugins/agui/agui-agent.ts +115 -0
  97. package/src/plugins/agui/agui.plugin.ts +148 -0
  98. package/src/plugins/agui/index.ts +2 -0
  99. package/src/plugins/attachments/attachments.plugin.ts +54 -0
  100. package/src/plugins/attachments/index.ts +6 -0
  101. package/src/plugins/attachments/view-attachment-tool.ts +91 -0
  102. package/src/plugins/base64.ts +26 -0
  103. package/src/plugins/composio/composio-tools.ts +365 -0
  104. package/src/plugins/composio/composio-ucan.ts +43 -0
  105. package/src/plugins/composio/composio.plugin.ts +214 -0
  106. package/src/plugins/composio/index.ts +15 -0
  107. package/src/plugins/domain-indexer/domain-indexer-agent.ts +57 -0
  108. package/src/plugins/domain-indexer/domain-indexer-tools.ts +253 -0
  109. package/src/plugins/domain-indexer/domain-indexer.plugin.ts +100 -0
  110. package/src/plugins/domain-indexer/index.ts +3 -0
  111. package/src/plugins/editor/blocknote-bridge.ts +120 -0
  112. package/src/plugins/editor/content-session.ts +331 -0
  113. package/src/plugins/editor/content-tools.ts +686 -0
  114. package/src/plugins/editor/document-model.ts +0 -0
  115. package/src/plugins/editor/editor-access-denied-tool.ts +58 -0
  116. package/src/plugins/editor/editor-agent.ts +34 -0
  117. package/src/plugins/editor/editor-config.ts +64 -0
  118. package/src/plugins/editor/editor-mx.ts +128 -0
  119. package/src/plugins/editor/editor.plugin.ts +143 -0
  120. package/src/plugins/editor/failures.ts +154 -0
  121. package/src/plugins/editor/index.ts +31 -0
  122. package/src/plugins/editor/jsdom-shim.ts +55 -0
  123. package/src/plugins/editor/prompts.ts +59 -0
  124. package/src/plugins/editor/prop-policy.ts +147 -0
  125. package/src/plugins/editor/provider.ts +386 -0
  126. package/src/plugins/editor/room-membership.ts +95 -0
  127. package/src/plugins/editor/standalone-editor-tool.ts +200 -0
  128. package/src/plugins/firecrawl/firecrawl-agent.ts +93 -0
  129. package/src/plugins/firecrawl/firecrawl-tools.ts +220 -0
  130. package/src/plugins/firecrawl/firecrawl.plugin.ts +105 -0
  131. package/src/plugins/firecrawl/index.ts +15 -0
  132. package/src/plugins/flows/action-metadata.ts +3505 -0
  133. package/src/plugins/flows/actions.ts +190 -0
  134. package/src/plugins/flows/edit.ts +314 -0
  135. package/src/plugins/flows/errors.ts +38 -0
  136. package/src/plugins/flows/explain.ts +43 -0
  137. package/src/plugins/flows/flow-doc.ts +111 -0
  138. package/src/plugins/flows/flows.plugin.ts +129 -0
  139. package/src/plugins/flows/forms.ts +330 -0
  140. package/src/plugins/flows/index.ts +2 -0
  141. package/src/plugins/flows/linkage.ts +147 -0
  142. package/src/plugins/flows/port-types.ts +53 -0
  143. package/src/plugins/flows/prompts.ts +65 -0
  144. package/src/plugins/flows/read.ts +392 -0
  145. package/src/plugins/flows/references.ts +66 -0
  146. package/src/plugins/flows/test-support.ts +165 -0
  147. package/src/plugins/flows/tools/authoring.ts +516 -0
  148. package/src/plugins/flows/tools/discovery.ts +119 -0
  149. package/src/plugins/flows/tools/forms.ts +142 -0
  150. package/src/plugins/flows/tools/inspect.ts +165 -0
  151. package/src/plugins/flows/tools/linkage.ts +117 -0
  152. package/src/plugins/flows/tools/settings.ts +184 -0
  153. package/src/plugins/flows/translator.ts +328 -0
  154. package/src/plugins/flows/types.ts +210 -0
  155. package/src/plugins/flows/ydoc-helpers.ts +543 -0
  156. package/src/plugins/index.ts +70 -0
  157. package/src/plugins/mcp-call-timeout.ts +38 -0
  158. package/src/plugins/mcp-tool-adapter.ts +87 -0
  159. package/src/plugins/memory/index.ts +17 -0
  160. package/src/plugins/memory/memory-tools.ts +463 -0
  161. package/src/plugins/memory/memory-ucan.ts +51 -0
  162. package/src/plugins/memory/memory.plugin.ts +122 -0
  163. package/src/plugins/memory/types.ts +147 -0
  164. package/src/plugins/portal/action-log.ts +34 -0
  165. package/src/plugins/portal/index.ts +2 -0
  166. package/src/plugins/portal/portal.plugin.ts +203 -0
  167. package/src/plugins/sandbox/index.ts +34 -0
  168. package/src/plugins/sandbox/sandbox-bridge.ts +295 -0
  169. package/src/plugins/sandbox/sandbox-mcp.ts +127 -0
  170. package/src/plugins/sandbox/sandbox-write-blob.ts +103 -0
  171. package/src/plugins/sandbox/sandbox.plugin.ts +599 -0
  172. package/src/plugins/tasks/index.ts +17 -0
  173. package/src/plugins/tasks/manifest.ts +81 -0
  174. package/src/plugins/tasks/middleware.ts +219 -0
  175. package/src/plugins/tasks/tasks-tools.ts +414 -0
  176. package/src/plugins/tasks/tasks.plugin.ts +74 -0
  177. package/src/plugins/ucan-failure.ts +133 -0
  178. package/src/plugins/user-preferences/index.ts +22 -0
  179. package/src/plugins/user-preferences/schema.ts +31 -0
  180. package/src/plugins/user-preferences/user-preferences-store.ts +172 -0
  181. package/src/plugins/user-preferences/user-preferences-tool.ts +131 -0
  182. package/src/plugins/user-preferences/user-preferences.plugin.ts +137 -0
  183. package/src/plugins/vfs/index.ts +40 -0
  184. package/src/plugins/vfs/vfs-auth.ts +56 -0
  185. package/src/plugins/vfs/vfs-client.ts +655 -0
  186. package/src/plugins/vfs/vfs-content.ts +181 -0
  187. package/src/plugins/vfs/vfs-errors.ts +163 -0
  188. package/src/plugins/vfs/vfs-sandbox-tools.ts +272 -0
  189. package/src/plugins/vfs/vfs-tools.ts +635 -0
  190. package/src/plugins/vfs/vfs.plugin.ts +201 -0
  191. package/src/realtime/frontend-call-registry.ts +163 -0
  192. package/src/realtime/realtime-endpoint.ts +619 -0
  193. package/src/realtime/session-socket-hub.ts +249 -0
  194. package/src/realtime/socket-io-codec.ts +187 -0
  195. package/src/secrets/jwe.ts +74 -0
  196. package/src/secrets/pin-cipher.ts +43 -0
  197. package/src/secrets/secrets-service.ts +319 -0
  198. package/src/shell/app.ts +564 -0
  199. package/src/shell/auth.ts +290 -0
  200. package/src/sqlite/blob-codec.ts +100 -0
  201. package/src/sqlite/blob-compactor.ts +146 -0
  202. package/src/sqlite/cache-config.ts +45 -0
  203. package/src/sqlite/database.ts +643 -0
  204. package/src/sqlite/do-vfs.ts +1485 -0
  205. package/src/sqlite/index.ts +61 -0
  206. package/src/sqlite/serialization.ts +370 -0
  207. package/src/sqlite/sessions-store.ts +311 -0
  208. package/src/sqlite/sqlite-saver.ts +919 -0
  209. package/src/sqlite/vacuum-policy.ts +77 -0
  210. package/src/sqlite/wa-sqlite-loader.ts +123 -0
  211. package/src/sqlite/wasm.d.ts +10 -0
  212. package/src/tasks/schedule.ts +174 -0
  213. package/src/tasks/scheduler.ts +753 -0
  214. package/src/tasks/spec.ts +125 -0
  215. package/src/tasks/store.ts +351 -0
@@ -0,0 +1,414 @@
1
+ /**
2
+ * The 10 main-agent task tools — the Workers port of the Node runtime's
3
+ * tasks toolset. Every handler drives the host scheduler through `ctx.tasks`
4
+ * (`OracleTasksSurface`); there is no Redis, no queue and no preview-token
5
+ * plumbing — the surface itself validates and the Workers preview is a
6
+ * schedule dry-run (next fire times + problems), not an agent execution.
7
+ *
8
+ * All handlers return `{ ok: false, error }` instead of throwing so the
9
+ * model always gets an actionable message.
10
+ */
11
+ import { z } from 'zod';
12
+ import { tool } from '../../plugin-api/tool-helper';
13
+ import type {
14
+ OracleTaskRecord,
15
+ OracleTasksSurface,
16
+ PluginTool,
17
+ RuntimeContext,
18
+ } from '../../plugin-api/types';
19
+ import { summarizeSchedule } from '../../tasks/schedule';
20
+ import {
21
+ TASK_ID_PATTERN,
22
+ TASK_STATUSES,
23
+ TaskScheduleSchema,
24
+ } from '../../tasks/spec';
25
+ import { pendingApprovalOf } from '../../tasks/store';
26
+
27
+ const TASKS_UNAVAILABLE = {
28
+ ok: false,
29
+ error:
30
+ 'Task scheduling is not available on this runtime — the host provides no task scheduler.',
31
+ } as const;
32
+
33
+ const taskIdSchema = z.object({
34
+ taskId: z.string().regex(TASK_ID_PATTERN),
35
+ });
36
+
37
+ const APPROVAL_FIELD_DESCRIPTION =
38
+ "How each run behaves. 'never' (default) just does the work and delivers " +
39
+ "the result. 'before-action' is for runs the user should sign off on first " +
40
+ '(a tweet, a message, a ticket, an email, a publish): when such a task ' +
41
+ 'fires it does NOT execute — it posts an approval request into the ' +
42
+ "user's oracle room and waits. The user approves or declines by replying " +
43
+ 'there; record the decision with `resolve_task_approval` — approval ' +
44
+ 'executes the run and delivers its result, a decline drops it.';
45
+
46
+ const taskInputShape = {
47
+ title: z.string().min(1).max(120),
48
+ intent: z
49
+ .string()
50
+ .min(1)
51
+ .describe(
52
+ 'Markdown instructions the run executes. Scheduled runs are BACKGROUND sessions with NO memory of this conversation — put every ID, URL, and name the run needs into this text, never assume the run "knows" anything discussed here.',
53
+ ),
54
+ schedule: TaskScheduleSchema,
55
+ approval: z
56
+ .enum(['never', 'before-action'])
57
+ .default('never')
58
+ .describe(APPROVAL_FIELD_DESCRIPTION),
59
+ dedicatedRoom: z
60
+ .enum(['auto', 'yes', 'no'])
61
+ .default('auto')
62
+ .describe(
63
+ "Whether the task gets its own '[Task] <title>' room where each run's result " +
64
+ "(and, for 'before-action' tasks, each approval request) is posted. 'auto' " +
65
+ 'creates one for frequent (sub-daily) cron tasks, long intents and ongoing ' +
66
+ 'monitor/watch/track tasks; otherwise results go to the main oracle room. ' +
67
+ 'Pass yes/no only when the user asked for it.',
68
+ ),
69
+ };
70
+
71
+ function requireTasks(ctx: RuntimeContext): OracleTasksSurface | null {
72
+ return ctx.tasks ?? null;
73
+ }
74
+
75
+ function summarizeRecord(record: OracleTaskRecord): Record<string, unknown> {
76
+ const pendingApprovalAt = pendingApprovalOf(record);
77
+ return {
78
+ taskId: record.id,
79
+ title: record.title,
80
+ status: record.status,
81
+ schedule: summarizeSchedule(record.schedule),
82
+ nextRunAt: record.nextRunAt ?? null,
83
+ approval: record.approval,
84
+ ...(pendingApprovalAt !== undefined && {
85
+ awaitingApprovalSince: pendingApprovalAt,
86
+ }),
87
+ };
88
+ }
89
+
90
+ function failure(err: unknown): { ok: false; error: string } {
91
+ return {
92
+ ok: false,
93
+ error: err instanceof Error ? err.message : String(err),
94
+ };
95
+ }
96
+
97
+ export function createTaskTools(): PluginTool[] {
98
+ return [
99
+ previewTask(),
100
+ createTask(),
101
+ listMyTasks(),
102
+ getTask(),
103
+ updateTask(),
104
+ setStatusTool(
105
+ 'pause_task',
106
+ 'pause',
107
+ 'Pause a task. No runs fire until you resume it; an unanswered approval request is dropped with it.',
108
+ ),
109
+ setStatusTool(
110
+ 'resume_task',
111
+ 'resume',
112
+ 'Resume a paused or failed task. The next run is recomputed from its schedule and the failure counter resets.',
113
+ ),
114
+ setStatusTool(
115
+ 'cancel_task',
116
+ 'cancel',
117
+ 'Cancel a task permanently. The record is kept for the audit trail.',
118
+ ),
119
+ resolveTaskApproval(),
120
+ suggestSpecFix(),
121
+ ];
122
+ }
123
+
124
+ // ── preview_task ────────────────────────────────────────────────────────────
125
+
126
+ const previewInput = z.object(taskInputShape);
127
+
128
+ function previewTask(): PluginTool {
129
+ return tool(
130
+ async (rawArgs, ctx) => {
131
+ const args = previewInput.parse(rawArgs);
132
+ const tasks = requireTasks(ctx);
133
+ if (!tasks) return TASKS_UNAVAILABLE;
134
+ try {
135
+ const result = await tasks.preview(args);
136
+ return {
137
+ ...result,
138
+ note: result.ok
139
+ ? 'STOP here. Show the user what the task will do and when it will next run (nextRuns), and ask whether to schedule it. Call create_task with the SAME title/intent/schedule only after they confirm in a new message.'
140
+ : 'The spec has problems — fix them with the user before calling create_task.',
141
+ };
142
+ } catch (err) {
143
+ return failure(err);
144
+ }
145
+ },
146
+ {
147
+ name: 'preview_task',
148
+ description:
149
+ 'Validate a candidate task spec and return its next run times WITHOUT creating anything. ALWAYS call this before create_task and show the user the result. IMPORTANT: scheduled runs are fresh background sessions with NO memory of this conversation — put every ID, URL, and name the run needs into the intent text.',
150
+ schema: previewInput,
151
+ },
152
+ );
153
+ }
154
+
155
+ // ── create_task ─────────────────────────────────────────────────────────────
156
+
157
+ const createInput = z.object(taskInputShape);
158
+
159
+ function createTask(): PluginTool {
160
+ return tool(
161
+ async (rawArgs, ctx) => {
162
+ const args = createInput.parse(rawArgs);
163
+ const tasks = requireTasks(ctx);
164
+ if (!tasks) return TASKS_UNAVAILABLE;
165
+ try {
166
+ const record = await tasks.create(args);
167
+ return {
168
+ ok: true,
169
+ ...summarizeRecord(record),
170
+ note: "Scheduled. Results are delivered to the user's oracle chat room when the task fires — NOT inline in this conversation — so tell the user where to expect them.",
171
+ };
172
+ } catch (err) {
173
+ return failure(err);
174
+ }
175
+ },
176
+ {
177
+ name: 'create_task',
178
+ description:
179
+ 'Schedule a previewed task. Call this ONLY after the user has seen the preview_task output and confirmed in a NEW message — never in the same turn as preview_task. TRIGGER CHOICE: "in 10 minutes", "tomorrow at 5pm" = kind "once" (compute `at` from now) — cron is ONLY for genuinely recurring schedules; `*/10 * * * *` means every-10-minutes-forever, not once-in-10-minutes. Set `approval` to "before-action" when a run would send/post/publish/create anything on the user\'s behalf: each fire then posts an approval request into their room and runs only once they approve. Runs are FRESH background sessions with no memory of this conversation — every ID/URL/name the run needs must be in the intent.',
180
+ schema: createInput,
181
+ },
182
+ );
183
+ }
184
+
185
+ // ── list_my_tasks ───────────────────────────────────────────────────────────
186
+
187
+ const listInput = z.object({
188
+ status: z.array(z.enum(TASK_STATUSES)).optional(),
189
+ });
190
+
191
+ function listMyTasks(): PluginTool {
192
+ return tool(
193
+ async (rawArgs, ctx) => {
194
+ const args = listInput.parse(rawArgs);
195
+ const tasks = requireTasks(ctx);
196
+ if (!tasks) return TASKS_UNAVAILABLE;
197
+ try {
198
+ const records = await tasks.list();
199
+ const rows = records
200
+ .filter(
201
+ (record) =>
202
+ !args.status?.length || args.status.includes(record.status),
203
+ )
204
+ .map(summarizeRecord)
205
+ .sort((a, b) =>
206
+ String(a.nextRunAt ?? '~').localeCompare(
207
+ String(b.nextRunAt ?? '~'),
208
+ ),
209
+ );
210
+ return { ok: true, tasks: rows, count: rows.length };
211
+ } catch (err) {
212
+ return failure(err);
213
+ }
214
+ },
215
+ {
216
+ name: 'list_my_tasks',
217
+ description:
218
+ "List the user's scheduled tasks — id, title, status, schedule, next run. A task with `awaitingApprovalSince` has an approval request waiting for the user in their room — tell them and point them there. Optionally filter by status.",
219
+ schema: listInput,
220
+ },
221
+ );
222
+ }
223
+
224
+ // ── get_task ────────────────────────────────────────────────────────────────
225
+
226
+ function getTask(): PluginTool {
227
+ return tool(
228
+ async (rawArgs, ctx) => {
229
+ const { taskId } = taskIdSchema.parse(rawArgs);
230
+ const tasks = requireTasks(ctx);
231
+ if (!tasks) return TASKS_UNAVAILABLE;
232
+ try {
233
+ const record = await tasks.get(taskId);
234
+ if (!record) return { ok: false, error: 'Task not found.' };
235
+ return {
236
+ ok: true,
237
+ ...summarizeRecord(record),
238
+ intent: record.intent,
239
+ lastRunAt: record.lastRunAt ?? null,
240
+ lastResult: record.lastResult ?? null,
241
+ consecutiveFailures: record.consecutiveFailures,
242
+ };
243
+ } catch (err) {
244
+ return failure(err);
245
+ }
246
+ },
247
+ {
248
+ name: 'get_task',
249
+ description:
250
+ 'Fetch one task: full intent body, status, schedule, last run result, and the consecutive-failure count if it has been failing.',
251
+ schema: taskIdSchema,
252
+ },
253
+ );
254
+ }
255
+
256
+ // ── update_task ─────────────────────────────────────────────────────────────
257
+
258
+ const updateInput = z.object({
259
+ taskId: taskIdSchema.shape.taskId,
260
+ title: z.string().min(1).max(120).optional(),
261
+ intent: taskInputShape.intent.optional(),
262
+ schedule: TaskScheduleSchema.optional(),
263
+ approval: z
264
+ .enum(['never', 'before-action'])
265
+ .optional()
266
+ .describe(APPROVAL_FIELD_DESCRIPTION),
267
+ });
268
+
269
+ function updateTask(): PluginTool {
270
+ return tool(
271
+ async (rawArgs, ctx) => {
272
+ const args = updateInput.parse(rawArgs);
273
+ const tasks = requireTasks(ctx);
274
+ if (!tasks) return TASKS_UNAVAILABLE;
275
+ const { taskId, ...patch } = args;
276
+ try {
277
+ const record = await tasks.update(taskId, patch);
278
+ return { ok: true, ...summarizeRecord(record) };
279
+ } catch (err) {
280
+ return failure(err);
281
+ }
282
+ },
283
+ {
284
+ name: 'update_task',
285
+ description:
286
+ 'Patch a task — title, schedule, approval mode, or intent (what the task does). Changing the intent changes what every future run executes, so show the user the revised intent and get their confirmation first. Changing the schedule reschedules the next run automatically.',
287
+ schema: updateInput,
288
+ },
289
+ );
290
+ }
291
+
292
+ // ── pause / resume / cancel ─────────────────────────────────────────────────
293
+
294
+ function setStatusTool(
295
+ name: string,
296
+ action: 'pause' | 'resume' | 'cancel',
297
+ description: string,
298
+ ): PluginTool {
299
+ return tool(
300
+ async (rawArgs, ctx) => {
301
+ const { taskId } = taskIdSchema.parse(rawArgs);
302
+ const tasks = requireTasks(ctx);
303
+ if (!tasks) return TASKS_UNAVAILABLE;
304
+ try {
305
+ const record = await tasks[action](taskId);
306
+ return {
307
+ ok: true,
308
+ taskId: record.id,
309
+ status: record.status,
310
+ nextRunAt: record.nextRunAt ?? null,
311
+ };
312
+ } catch (err) {
313
+ return failure(err);
314
+ }
315
+ },
316
+ { name, description, schema: taskIdSchema },
317
+ );
318
+ }
319
+
320
+ // ── resolve_task_approval ───────────────────────────────────────────────────
321
+
322
+ const resolveApprovalInput = z.object({
323
+ taskId: taskIdSchema.shape.taskId,
324
+ outcome: z.enum(['approved', 'declined']),
325
+ note: z
326
+ .string()
327
+ .optional()
328
+ .describe(
329
+ "Anything the user asked to change with their approval (e.g. 'fix the title first') — it is passed into the run's instructions.",
330
+ ),
331
+ });
332
+
333
+ function resolveTaskApproval(): PluginTool {
334
+ return tool(
335
+ async (rawArgs, ctx) => {
336
+ const args = resolveApprovalInput.parse(rawArgs);
337
+ const tasks = requireTasks(ctx);
338
+ if (!tasks) return TASKS_UNAVAILABLE;
339
+ try {
340
+ const result = await tasks.resolveApproval(
341
+ args.taskId,
342
+ args.outcome === 'approved' ? 'approve' : 'reject',
343
+ args.note,
344
+ );
345
+ if (!result.resolved) {
346
+ return { ok: false, error: 'No approval is pending for this task.' };
347
+ }
348
+ return {
349
+ ok: true,
350
+ taskId: args.taskId,
351
+ outcome: args.outcome,
352
+ note:
353
+ args.outcome === 'approved'
354
+ ? "Decision recorded and the run was executed — its result was delivered to the user's room. Confirm to the user."
355
+ : 'Decision recorded — the pending run was dropped and nothing was executed.',
356
+ };
357
+ } catch (err) {
358
+ return failure(err);
359
+ }
360
+ },
361
+ {
362
+ name: 'resolve_task_approval',
363
+ description:
364
+ "Record the user's decision on a task run that is waiting for approval. 'approved' EXECUTES the run immediately (the result is delivered to the user's room); 'declined' drops it without running. Call this when the user answers a pending approval request — pass their requested tweaks in `note`.",
365
+ schema: resolveApprovalInput,
366
+ },
367
+ );
368
+ }
369
+
370
+ // ── suggest_spec_fix ────────────────────────────────────────────────────────
371
+
372
+ function suggestSpecFix(): PluginTool {
373
+ return tool(
374
+ async (rawArgs, ctx) => {
375
+ const { taskId } = taskIdSchema.parse(rawArgs);
376
+ const tasks = requireTasks(ctx);
377
+ if (!tasks) return TASKS_UNAVAILABLE;
378
+ try {
379
+ const record = await tasks.get(taskId);
380
+ if (!record) return { ok: false, error: 'Task not found.' };
381
+ const lastFailure =
382
+ record.lastResult && !record.lastResult.ok
383
+ ? record.lastResult
384
+ : undefined;
385
+ if (!lastFailure && record.consecutiveFailures === 0) {
386
+ return {
387
+ ok: true,
388
+ proposal: null,
389
+ note: 'No recent failures recorded — nothing to fix.',
390
+ };
391
+ }
392
+ return {
393
+ ok: true,
394
+ taskId,
395
+ title: record.title,
396
+ currentIntent: record.intent,
397
+ lastError: lastFailure?.summary ?? 'unknown',
398
+ lastFailedAt: lastFailure?.at ?? null,
399
+ consecutiveFailures: record.consecutiveFailures,
400
+ instruction:
401
+ 'Propose a concise revision of currentIntent that addresses lastError and explain the change to the user. Once they confirm: apply it with update_task (and resume_task if the task is failed).',
402
+ };
403
+ } catch (err) {
404
+ return failure(err);
405
+ }
406
+ },
407
+ {
408
+ name: 'suggest_spec_fix',
409
+ description:
410
+ 'For a failing task: returns the current intent plus the last error so you can propose a revised intent to the user. Apply the fix with update_task ONLY after the user agrees — never auto-apply.',
411
+ schema: taskIdSchema,
412
+ },
413
+ );
414
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Scheduled-tasks plugin for the Workers runtime.
3
+ *
4
+ * The heavy lifting — storage in the user's own SQLite file, Durable Object
5
+ * alarm scheduling, run execution, delivery, the approval flow — lives in the
6
+ * HOST (`src/tasks/scheduler.ts`, wired by `UserOracleDO`) and is exposed to
7
+ * plugins as `ctx.tasks` (`OracleTasksSurface`). This plugin contributes:
8
+ *
9
+ * - the 10 main-agent tools (preview / create / list / get / update /
10
+ * pause / resume / cancel / resolve_task_approval / suggest_spec_fix),
11
+ * each driving `ctx.tasks`;
12
+ * - the approval-gate middleware that hints the model when a
13
+ * `before-action` run is waiting on the user's decision.
14
+ *
15
+ * Tasks are available whenever the host provides `ctx.tasks` (always on
16
+ * Workers), so `autoDetect` is unconditionally true; on a host without a
17
+ * scheduler every tool degrades to a clear "not available" error.
18
+ *
19
+ * `TASKS_MAX_PER_USER` (default 50) and `TASKS_MIN_CRON_INTERVAL_SEC`
20
+ * (default 300) are declared here so operators can tune them through the
21
+ * Worker env; the host forwards them into `createTaskScheduler` via
22
+ * `TaskSchedulerHost.maxTasksPerUser` / `.minCronIntervalSec`.
23
+ *
24
+ * Middlewares are built once at boot while the tasks surface is per-user, so
25
+ * the plugin stashes each request's `ctx.tasks` keyed by the authenticated
26
+ * user DID (`getRequestTools` runs on every agent build, before any model
27
+ * call) and the middleware looks the surface up per turn.
28
+ */
29
+ import { z } from 'zod';
30
+ import { defineOraclePlugin } from '../../plugin-api/define-plugin';
31
+ import type { OraclePlugin } from '../../plugin-api/oracle-plugin';
32
+ import type { OracleTasksSurface } from '../../plugin-api/types';
33
+ import { createTaskApprovalGateMiddleware } from './middleware';
34
+ import { tasksManifest } from './manifest';
35
+ import { createTaskTools } from './tasks-tools';
36
+
37
+ export const tasksConfigSchema = z.object({
38
+ TASKS_MAX_PER_USER: z.coerce.number().int().positive().default(50),
39
+ TASKS_MIN_CRON_INTERVAL_SEC: z.coerce.number().int().positive().default(300),
40
+ });
41
+
42
+ export const TASKS_PLUGIN_NAME = 'tasks';
43
+
44
+ export function createTasksPlugin(): OraclePlugin {
45
+ // ctx.tasks per user DID, refreshed on every agent build. One plugin
46
+ // instance can serve several users' Durable Objects in the same isolate,
47
+ // so the key is the authenticated DID from the request context.
48
+ const surfaces = new Map<string, OracleTasksSurface>();
49
+
50
+ return defineOraclePlugin({
51
+ name: TASKS_PLUGIN_NAME,
52
+ version: '1.0.0',
53
+ manifest: tasksManifest,
54
+ configSchema: tasksConfigSchema,
55
+ autoDetectHint:
56
+ 'always on — the user Durable Object provides the scheduler (ctx.tasks)',
57
+ autoDetect: () => true,
58
+ getTools: () => createTaskTools(),
59
+ getRequestTools: (rtCtx) => {
60
+ if (rtCtx.tasks) surfaces.set(rtCtx.user.did, rtCtx.tasks);
61
+ else surfaces.delete(rtCtx.user.did);
62
+ return [];
63
+ },
64
+ getMiddlewares: (ctx) => [
65
+ createTaskApprovalGateMiddleware({
66
+ surfaceFor: (userDid) => surfaces.get(userDid),
67
+ logger: ctx.logger,
68
+ }),
69
+ ],
70
+ });
71
+ }
72
+
73
+ /** Ready-to-register instance (safe to share — state is keyed per user DID). */
74
+ export const TasksPlugin: OraclePlugin = createTasksPlugin();
@@ -0,0 +1,133 @@
1
+ import type { RuntimeContext } from '../plugin-api/types';
2
+ import { UcanMintUnavailableError } from '../plugin-api/ucan-errors';
3
+
4
+ /**
5
+ * Shared, non-throwing UCAN failure handling for plugin auth helpers
6
+ * (composio / memory, and any future minting plugin). Verbatim port of the
7
+ * Node runtime's `plugins/ucan-failure.ts`.
8
+ *
9
+ * Every plugin that mints a service-targeted invocation runs the same two
10
+ * risky steps:
11
+ *
12
+ * 1. `runCtx.ucan.resolveServiceDid(url)` — a did:web lookup that can throw
13
+ * on network/DNS failure or return `null` when the document is missing.
14
+ * 2. `runCtx.ucan.mintInvocation(...)` — which the runtime adapter throws
15
+ * from when the underlying service returns `null` (no signing key, no
16
+ * cached delegation, or an internal mint/sign failure).
17
+ *
18
+ * The plugin contract is safe degradation: the capability is simply absent for
19
+ * the turn and the agent keeps running. These helpers make sure a failure
20
+ * never escapes the plugin boundary, and that the log line distinguishes the
21
+ * EXPECTED "this user just hasn't delegated to us" case (info/debug) from a
22
+ * genuine ERROR ("a delegation exists but mint/resolve blew up", warn).
23
+ */
24
+
25
+ /** Why `mintInvocation` failed to produce a usable invocation. */
26
+ type MintFailureKind =
27
+ | 'no-signing-key' // oracle has no Ed25519 key — UCAN minting is disabled
28
+ | 'no-delegation' // expected: this user has not delegated to the oracle
29
+ | 'error'; // unexpected throw — delegation likely present but minting failed
30
+
31
+ /**
32
+ * Classify a thrown value from `mintInvocation` into expected vs. error. The
33
+ * adapter throws a typed `UcanMintUnavailableError` for the EXPECTED cases
34
+ * (no signing key / no cached delegation); anything else is an unexpected
35
+ * failure. We refine the expected case into `no-signing-key` vs
36
+ * `no-delegation` purely for the log message.
37
+ */
38
+ function classifyMintError(
39
+ runCtx: RuntimeContext,
40
+ error: unknown,
41
+ ): MintFailureKind {
42
+ if (!(error instanceof UcanMintUnavailableError)) return 'error';
43
+ return runCtx.ucan.hasSigningKey() ? 'no-delegation' : 'no-signing-key';
44
+ }
45
+
46
+ /** Human-readable detail string for an unknown thrown value. */
47
+ function describe(error: unknown): string {
48
+ return error instanceof Error
49
+ ? `${error.name}: ${error.message}`
50
+ : String(error);
51
+ }
52
+
53
+ /**
54
+ * Log an expected (non-error) UCAN degradation. Prefers the optional `debug`
55
+ * channel; falls back to `log` when the logger doesn't expose `debug`, so the
56
+ * line never lands on the `warn`/`error` channels reserved for real failures.
57
+ */
58
+ function logExpected(runCtx: RuntimeContext, message: string): void {
59
+ if (runCtx.logger.debug) {
60
+ runCtx.logger.debug(message);
61
+ } else {
62
+ runCtx.logger.log(message);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Resolve a service DID without throwing. Returns `null` on a missing document
68
+ * (expected) or on a resolution error (logged at `warn`). The boolean-less
69
+ * `null` return keeps the existing caller contract intact.
70
+ */
71
+ export async function resolveServiceDidSafely(
72
+ runCtx: RuntimeContext,
73
+ serviceUrl: string,
74
+ plugin: string,
75
+ ): Promise<string | null> {
76
+ try {
77
+ const did = await runCtx.ucan.resolveServiceDid(serviceUrl);
78
+ if (!did) {
79
+ logExpected(
80
+ runCtx,
81
+ `[${plugin}] UCAN unavailable: service DID could not be resolved for ${serviceUrl} (no document / no id). Degrading without auth.`,
82
+ );
83
+ return null;
84
+ }
85
+ return did;
86
+ } catch (error) {
87
+ runCtx.logger.warn(
88
+ `[${plugin}] UCAN error: resolving service DID for ${serviceUrl} threw — ${describe(error)}. Degrading without auth.`,
89
+ );
90
+ return null;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Mint an invocation without throwing. Returns the token on success, or `null`
96
+ * on any failure — logging the EXPECTED cases (no signing key / no delegation)
97
+ * at `debug` and genuine errors (delegation present, mint/sign failed) at
98
+ * `warn` with the underlying message.
99
+ */
100
+ export async function mintInvocationSafely(
101
+ runCtx: RuntimeContext,
102
+ target: { did: string; capability: string },
103
+ plugin: string,
104
+ opts?: { skipCache?: boolean; can?: string },
105
+ ): Promise<string | null> {
106
+ try {
107
+ // Forward `opts` only when supplied so the no-opts call signature stays
108
+ // `mintInvocation(target)` — passing an explicit `undefined` second arg
109
+ // would be a needless behavioural change for callers that don't opt out
110
+ // of caching (memory / skills).
111
+ const invocation = opts
112
+ ? await runCtx.ucan.mintInvocation(target, opts)
113
+ : await runCtx.ucan.mintInvocation(target);
114
+ return invocation && invocation.length > 0 ? invocation : null;
115
+ } catch (error) {
116
+ const kind = classifyMintError(runCtx, error);
117
+ if (kind === 'error') {
118
+ runCtx.logger.warn(
119
+ `[${plugin}] UCAN error: minting '${target.capability}' invocation failed — ${describe(error)}. Degrading without auth.`,
120
+ );
121
+ } else {
122
+ const reason =
123
+ kind === 'no-signing-key'
124
+ ? 'oracle has no signing key'
125
+ : 'no delegation present for this user';
126
+ logExpected(
127
+ runCtx,
128
+ `[${plugin}] UCAN unavailable: ${reason} — skipping '${target.capability}' invocation. Degrading without auth.`,
129
+ );
130
+ }
131
+ return null;
132
+ }
133
+ }
@@ -0,0 +1,22 @@
1
+ export {
2
+ FORMALITY_LEVELS,
3
+ USER_PREFS_STATE_KEY,
4
+ UserPreferencesSchema,
5
+ type StoredUserPreferences,
6
+ } from './schema';
7
+ export {
8
+ UserPreferencesStore,
9
+ type RoomStateAccess,
10
+ type UserPreferencesStoreOptions,
11
+ } from './user-preferences-store';
12
+ export {
13
+ createSetUserPreferencesTool,
14
+ SET_USER_PREFERENCES_DESCRIPTION,
15
+ SET_USER_PREFERENCES_TOOL_NAME,
16
+ setUserPreferencesSchema,
17
+ } from './user-preferences-tool';
18
+ export {
19
+ UserPreferencesPlugin,
20
+ type PreferencesGateway,
21
+ type UserPreferencesPluginOptions,
22
+ } from './user-preferences.plugin';
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The stored preferences record — identical to the Node runtime's
3
+ * `UserPreferencesSchema` so both runtimes accept each other's payloads.
4
+ */
5
+ import { z } from 'zod';
6
+
7
+ /** Room-state key under `ixo.room.state` (shared with the Node runtime). */
8
+ export const USER_PREFS_STATE_KEY = 'user_prefs';
9
+
10
+ export const FORMALITY_LEVELS = [
11
+ 'casual',
12
+ 'friendly',
13
+ 'neutral',
14
+ 'semi-formal',
15
+ 'professional',
16
+ 'formal',
17
+ 'technical',
18
+ 'academic',
19
+ ] as const;
20
+
21
+ export const UserPreferencesSchema = z.object({
22
+ agentName: z.string().max(80).optional(),
23
+ userName: z.string().max(80).optional(),
24
+ language: z.string().max(20).optional(), // BCP-47-ish, free-form
25
+ tone: z.string().optional(), // e.g. "casual", "playful"
26
+ formality: z.enum(FORMALITY_LEVELS).optional(),
27
+ customInstructions: z.string().max(2000).optional(),
28
+ updatedAt: z.iso.datetime().optional(), // the store sets it on every write
29
+ });
30
+
31
+ export type StoredUserPreferences = z.infer<typeof UserPreferencesSchema>;