@ixo/oracle-runtime-workers 0.10.0 → 0.11.1
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 +5 -2
- package/package.json +1 -1
- package/src/core/context-budget.ts +201 -0
- package/src/core/context-window.ts +289 -0
- package/src/core/env.ts +29 -0
- package/src/core/main-agent-types.ts +46 -0
- package/src/core/main-agent.ts +86 -9
- package/src/core/middlewares/context-guard.ts +270 -0
- package/src/core/middlewares/result-cap.ts +180 -0
- package/src/core/middlewares/summarization.ts +87 -6
- package/src/core/middlewares/tool-marks.ts +209 -0
- package/src/core/openrouter-pricing.ts +42 -1
- package/src/core/read-result-tool.ts +77 -0
- package/src/core/runtime-context.ts +7 -0
- package/src/core/sub-agent-fallback.ts +20 -3
- package/src/core/wrap-plugin-tool.ts +24 -3
- package/src/do/ambient.ts +17 -0
- package/src/do/contracts.ts +229 -0
- package/src/do/flush-schedule.ts +40 -0
- package/src/do/result-store-test-do.ts +88 -0
- package/src/do/result-store.ts +339 -0
- package/src/do/run-buffer.ts +204 -0
- package/src/do/run-coordinator.ts +572 -0
- package/src/do/run-request.ts +52 -0
- package/src/do/run-store-test-do.ts +130 -0
- package/src/do/run-store.ts +604 -0
- package/src/do/session-id-migration.ts +85 -0
- package/src/do/speaker-prefix.ts +37 -0
- package/src/do/sse-stream.ts +567 -400
- package/src/do/transcript.ts +238 -0
- package/src/do/turn-body.ts +10 -0
- package/src/do/user-oracle-do.ts +948 -162
- package/src/matrix/gateway-do.ts +230 -29
- package/src/matrix/group-chat-summarizer.ts +107 -0
- package/src/matrix/group-chat.ts +1163 -0
- package/src/matrix/inbox-store.ts +23 -3
- package/src/matrix/ingest.ts +54 -11
- package/src/matrix/reply-chain.ts +34 -25
- package/src/plugin-api/tool-helper.ts +9 -1
- package/src/plugin-api/types.ts +86 -0
- package/src/plugins/index.ts +3 -0
- package/src/plugins/matrix-group-chats/README.md +70 -0
- package/src/plugins/matrix-group-chats/index.ts +132 -0
- package/src/plugins/matrix-group-chats/tools.ts +172 -0
- package/src/plugins/tasks/tasks-tools.ts +4 -0
- package/src/shell/app.ts +66 -0
- package/src/sqlite/sqlite-saver.ts +81 -0
- package/src/tasks/scheduler.ts +134 -3
- package/src/tasks/store.ts +22 -0
package/src/tasks/scheduler.ts
CHANGED
|
@@ -103,6 +103,14 @@ export interface TaskSchedulerHost {
|
|
|
103
103
|
runTurn: (req: TurnRequest) => Promise<TurnResult>;
|
|
104
104
|
/** Ask the object to re-arm its alarm no later than `at` (ms epoch). */
|
|
105
105
|
requestAlarm: (at: number) => void;
|
|
106
|
+
/**
|
|
107
|
+
* Whether the durable turn run of a task run (`TurnRequest.taskRunId`) is
|
|
108
|
+
* still live in the object — recovering or executing — so an open
|
|
109
|
+
* `running` row is left alone at reconciliation instead of being closed
|
|
110
|
+
* as interrupted; the object hands the outcome over through
|
|
111
|
+
* `completeRecoveredRun` / `failRecoveredRun` when the run ends.
|
|
112
|
+
*/
|
|
113
|
+
turnRunLive?: (taskRunId: string) => boolean;
|
|
106
114
|
log: Logger;
|
|
107
115
|
/** Live-task cap (`TASKS_MAX_PER_USER`). Default 50. */
|
|
108
116
|
maxTasksPerUser?: number;
|
|
@@ -123,6 +131,16 @@ export interface TaskScheduler {
|
|
|
123
131
|
onAlarm(now: number): Promise<void>;
|
|
124
132
|
/** Runs no incarnation has finished (operator/debug). */
|
|
125
133
|
openRuns(): Promise<OpenTaskRun[]>;
|
|
134
|
+
/** A run this instance is executing or delivering right now. */
|
|
135
|
+
isRunActive(runId: string): boolean;
|
|
136
|
+
/**
|
|
137
|
+
* The turn of a `running` task run finished in a recovered durable run
|
|
138
|
+
* (no `executeRun` continuation is waiting): advance the schedule, store
|
|
139
|
+
* the result and deliver it, exactly as `executeRun` would have.
|
|
140
|
+
*/
|
|
141
|
+
completeRecoveredRun(runId: string, text: string): Promise<void>;
|
|
142
|
+
/** The recovered turn of a `running` task run ended without a result. */
|
|
143
|
+
failRecoveredRun(runId: string, detail: string): Promise<void>;
|
|
126
144
|
}
|
|
127
145
|
|
|
128
146
|
/** How much of a run's output is kept as `lastResult.summary`. */
|
|
@@ -228,6 +246,9 @@ class AlarmTaskScheduler implements TaskScheduler {
|
|
|
228
246
|
*/
|
|
229
247
|
private readonly activeRuns = new Map<string, string>();
|
|
230
248
|
|
|
249
|
+
/** Tasks whose open run is being recovered by the object (rebuilt per alarm). */
|
|
250
|
+
private readonly recoveringTasks = new Set<string>();
|
|
251
|
+
|
|
231
252
|
private hasActiveRun(taskId: string): boolean {
|
|
232
253
|
for (const owner of this.activeRuns.values())
|
|
233
254
|
if (owner === taskId) return true;
|
|
@@ -274,6 +295,102 @@ class AlarmTaskScheduler implements TaskScheduler {
|
|
|
274
295
|
return this.store.openRuns();
|
|
275
296
|
}
|
|
276
297
|
|
|
298
|
+
isRunActive(runId: string): boolean {
|
|
299
|
+
return this.activeRuns.has(runId);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async completeRecoveredRun(runId: string, text: string): Promise<void> {
|
|
303
|
+
const run = await this.store.getOpenRun(runId);
|
|
304
|
+
if (!run || run.state !== 'running') return;
|
|
305
|
+
const nowMs = Date.now();
|
|
306
|
+
const loaded = await this.store.get(run.taskId);
|
|
307
|
+
if (!loaded || loaded.status !== 'active') {
|
|
308
|
+
await this.store.updateRun(runId, {
|
|
309
|
+
state: 'failed',
|
|
310
|
+
ok: false,
|
|
311
|
+
finishedAt: new Date().toISOString(),
|
|
312
|
+
detail: `result dropped: task ${loaded?.status ?? 'deleted'} during the run`,
|
|
313
|
+
});
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const task: TaskRecord = loaded;
|
|
317
|
+
const trimmed = text.trim();
|
|
318
|
+
if (trimmed.length === 0) {
|
|
319
|
+
await this.recordFailure(
|
|
320
|
+
task,
|
|
321
|
+
nowMs,
|
|
322
|
+
run.startedAt,
|
|
323
|
+
new Error('Agent returned no output'),
|
|
324
|
+
{ runId, state: 'failed' },
|
|
325
|
+
);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
this.host.log.log(
|
|
329
|
+
`[tasks] run ${runId} of ${task.id}: turn recovered after a reset; delivering its result`,
|
|
330
|
+
);
|
|
331
|
+
// Same bookkeeping as `executeRun` after the turn: advance the schedule
|
|
332
|
+
// now so no later alarm fires this occurrence again, then deliver.
|
|
333
|
+
if (task.schedule.kind === 'once') {
|
|
334
|
+
delete task.nextRunAt;
|
|
335
|
+
} else if (task.approval !== 'before-action') {
|
|
336
|
+
const nextMs = computeNextRunAtMs(task.schedule, nowMs);
|
|
337
|
+
if (nextMs !== null) task.nextRunAt = new Date(nextMs).toISOString();
|
|
338
|
+
else delete task.nextRunAt;
|
|
339
|
+
}
|
|
340
|
+
task.updatedAt = new Date().toISOString();
|
|
341
|
+
const roomId = run.roomId ?? (await this.resolveDeliveryRoom(task));
|
|
342
|
+
await this.host.db.transaction(async () => {
|
|
343
|
+
await this.store.save(task);
|
|
344
|
+
await this.store.updateRun(runId, {
|
|
345
|
+
state: 'delivering',
|
|
346
|
+
...(roomId ? { roomId } : {}),
|
|
347
|
+
resultText: trimmed,
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
const refreshed = (await this.store.get(task.id)) ?? task;
|
|
351
|
+
const open: OpenTaskRun = {
|
|
352
|
+
runId,
|
|
353
|
+
taskId: refreshed.id,
|
|
354
|
+
startedAt: run.startedAt,
|
|
355
|
+
state: 'delivering',
|
|
356
|
+
txnId: run.txnId,
|
|
357
|
+
...(roomId ? { roomId } : {}),
|
|
358
|
+
resultText: trimmed,
|
|
359
|
+
attempts: 0,
|
|
360
|
+
};
|
|
361
|
+
this.activeRuns.set(runId, refreshed.id);
|
|
362
|
+
try {
|
|
363
|
+
await this.deliverOrDefer(refreshed, open, nowMs);
|
|
364
|
+
} finally {
|
|
365
|
+
this.activeRuns.delete(runId);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async failRecoveredRun(runId: string, detail: string): Promise<void> {
|
|
370
|
+
const run = await this.store.getOpenRun(runId);
|
|
371
|
+
if (!run || run.state !== 'running') return;
|
|
372
|
+
const task = await this.store.get(run.taskId);
|
|
373
|
+
if (!task) {
|
|
374
|
+
await this.store.updateRun(runId, {
|
|
375
|
+
state: 'interrupted',
|
|
376
|
+
ok: false,
|
|
377
|
+
finishedAt: new Date().toISOString(),
|
|
378
|
+
detail,
|
|
379
|
+
});
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
await this.recordFailure(
|
|
383
|
+
task,
|
|
384
|
+
Date.now(),
|
|
385
|
+
run.startedAt,
|
|
386
|
+
new Error(detail),
|
|
387
|
+
{
|
|
388
|
+
runId,
|
|
389
|
+
state: 'interrupted',
|
|
390
|
+
},
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
277
394
|
async onAlarm(now: number): Promise<void> {
|
|
278
395
|
// Unfinished runs first: what a previous incarnation left behind decides
|
|
279
396
|
// the fate of its task before the due scan can fire the task again.
|
|
@@ -288,9 +405,11 @@ class AlarmTaskScheduler implements TaskScheduler {
|
|
|
288
405
|
}
|
|
289
406
|
if (Date.parse(task.nextRunAt) > now) continue;
|
|
290
407
|
// A run of this task is executing in this instance right now (a long
|
|
291
|
-
// turn while the object's alarm fires for something else)
|
|
292
|
-
//
|
|
293
|
-
|
|
408
|
+
// turn while the object's alarm fires for something else), or its
|
|
409
|
+
// turn is being recovered by the object after a reset: its schedule
|
|
410
|
+
// advances when the turn ends, never start a second one.
|
|
411
|
+
if (this.hasActiveRun(task.id) || this.recoveringTasks.has(task.id))
|
|
412
|
+
continue;
|
|
294
413
|
try {
|
|
295
414
|
if (task.approval === 'before-action') {
|
|
296
415
|
await this.requestApproval(task, now);
|
|
@@ -319,6 +438,7 @@ class AlarmTaskScheduler implements TaskScheduler {
|
|
|
319
438
|
*/
|
|
320
439
|
private async reconcileOpenRuns(now: number): Promise<void> {
|
|
321
440
|
const open = await this.store.openRuns();
|
|
441
|
+
this.recoveringTasks.clear();
|
|
322
442
|
for (const run of open) {
|
|
323
443
|
if (this.activeRuns.has(run.runId)) continue; // alive in this instance
|
|
324
444
|
const task = await this.store.get(run.taskId);
|
|
@@ -338,6 +458,14 @@ class AlarmTaskScheduler implements TaskScheduler {
|
|
|
338
458
|
);
|
|
339
459
|
await this.deliverOrDefer(task, run, now);
|
|
340
460
|
} else {
|
|
461
|
+
// A durable turn run that is recovering (or executing) owns this
|
|
462
|
+
// row: it reports back through `completeRecoveredRun` /
|
|
463
|
+
// `failRecoveredRun` when it ends. Until then the task must not
|
|
464
|
+
// fire again (its schedule only advances after the turn).
|
|
465
|
+
if (this.host.turnRunLive?.(run.runId)) {
|
|
466
|
+
this.recoveringTasks.add(run.taskId);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
341
469
|
this.host.log.warn(
|
|
342
470
|
`[tasks] run ${run.runId} of ${run.taskId} started ${run.startedAt} never finished (the object was reset while it ran); closing it as interrupted, not re-running`,
|
|
343
471
|
);
|
|
@@ -748,6 +876,9 @@ class AlarmTaskScheduler implements TaskScheduler {
|
|
|
748
876
|
client: 'matrix',
|
|
749
877
|
roomId,
|
|
750
878
|
requestId: crypto.randomUUID(),
|
|
879
|
+
// Links the durable turn run to this task run: a reset mid-turn is
|
|
880
|
+
// recovered and delivered by the object instead of closed.
|
|
881
|
+
taskRunId: runId,
|
|
751
882
|
});
|
|
752
883
|
const text = result.text.trim();
|
|
753
884
|
if (text.length === 0) throw new Error('Agent returned no output');
|
package/src/tasks/store.ts
CHANGED
|
@@ -509,6 +509,28 @@ export class TasksStore {
|
|
|
509
509
|
}));
|
|
510
510
|
}
|
|
511
511
|
|
|
512
|
+
/** One open run by id (`running` or `delivering`), or undefined. */
|
|
513
|
+
async getOpenRun(runId: string): Promise<OpenTaskRun | undefined> {
|
|
514
|
+
await this.setup();
|
|
515
|
+
const row = await this.db.get<OpenRunRow>(
|
|
516
|
+
`SELECT run_id, task_id, started_at, state, txn_id, room_id, result_text, attempts, retry_at
|
|
517
|
+
FROM task_runs WHERE run_id = ? AND state IN ('running', 'delivering')`,
|
|
518
|
+
[runId],
|
|
519
|
+
);
|
|
520
|
+
if (!row) return undefined;
|
|
521
|
+
return {
|
|
522
|
+
runId: row.run_id,
|
|
523
|
+
taskId: row.task_id,
|
|
524
|
+
startedAt: row.started_at,
|
|
525
|
+
state: row.state === 'delivering' ? 'delivering' : 'running',
|
|
526
|
+
txnId: row.txn_id ?? `task-${row.run_id}`,
|
|
527
|
+
...(row.room_id ? { roomId: row.room_id } : {}),
|
|
528
|
+
...(row.result_text !== null ? { resultText: row.result_text } : {}),
|
|
529
|
+
attempts: Number(row.attempts ?? 0),
|
|
530
|
+
...(row.retry_at !== null ? { retryAt: Number(row.retry_at) } : {}),
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
512
534
|
/** Earliest pending delivery retry (ms epoch) over open runs, or null. */
|
|
513
535
|
async minRetryAt(): Promise<number | null> {
|
|
514
536
|
await this.setup();
|