@ixo/oracle-runtime-workers 0.25.0 → 0.27.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 (36) hide show
  1. package/package.json +3 -2
  2. package/src/attachments/extract.ts +108 -14
  3. package/src/attachments/index.ts +15 -0
  4. package/src/attachments/pdf-text.ts +99 -0
  5. package/src/attachments/pipeline.ts +47 -4
  6. package/src/attachments/sanitize.ts +14 -0
  7. package/src/attachments/view-cache.ts +95 -0
  8. package/src/attachments/view.ts +308 -3
  9. package/src/channels/auth.ts +2 -1
  10. package/src/channels/contract.ts +104 -0
  11. package/src/channels/replies.ts +58 -0
  12. package/src/channels/routing.ts +222 -0
  13. package/src/channels/turns.ts +1252 -16
  14. package/src/core/env.ts +17 -0
  15. package/src/do/contracts.ts +19 -0
  16. package/src/do/room-mirror.ts +8 -1
  17. package/src/do/run-coordinator.ts +27 -0
  18. package/src/do/transcript.ts +14 -11
  19. package/src/do/turn-messages.ts +54 -0
  20. package/src/do/user-oracle-do.ts +321 -135
  21. package/src/memory/session-history-indexer.ts +442 -103
  22. package/src/memory/user-context-fetcher.ts +13 -0
  23. package/src/memory/user-context-invalidation.ts +8 -2
  24. package/src/plugins/action-proposals/evidence.ts +7 -8
  25. package/src/plugins/memory/document-chunker.ts +313 -0
  26. package/src/plugins/memory/document-source.ts +229 -0
  27. package/src/plugins/memory/memory-engine-client.ts +352 -0
  28. package/src/plugins/memory/memory.plugin.ts +25 -2
  29. package/src/plugins/memory/oracle-document-tools.ts +677 -0
  30. package/src/plugins/tasks/middleware.ts +4 -9
  31. package/src/plugins/vfs/index.ts +11 -2
  32. package/src/plugins/vfs/vfs-content.ts +86 -22
  33. package/src/plugins/vfs/vfs.plugin.ts +28 -19
  34. package/src/shell/app.ts +70 -0
  35. package/src/sqlite/serialization.ts +20 -0
  36. package/src/sqlite/sessions-store.ts +43 -0
package/src/core/env.ts CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  decisionProviderEnvShape,
4
4
  } from '@ixo/common/ai/decisions';
5
5
  import { z } from 'zod';
6
+ import { channelRoutingEnvShape } from '../channels/routing';
6
7
  import type { OraclePlugin } from '../plugin-api/oracle-plugin';
7
8
  import type { Logger } from '../plugin-api/types';
8
9
  import { NOOP_LOGGER } from './utils';
@@ -16,6 +17,7 @@ import {
16
17
  USER_CONTEXT_BLOCKING_CAP_MS_DEFAULT,
17
18
  USER_CONTEXT_CACHE_TTL_MS_DEFAULT,
18
19
  } from '../memory/user-context-fetcher';
20
+ import { CHANNEL_HISTORY_INDEX_DEBOUNCE_MS_DEFAULT } from '../memory/session-history-indexer';
19
21
 
20
22
  /**
21
23
  * Tier-0 (core) environment for the Workers runtime. Mirrors
@@ -303,6 +305,7 @@ export const baseEnvSchema = z.object({
303
305
  // runtime consumer of Decisions. It predicts the on-demand plugin a message
304
306
  // needs and preloads it for the turn; it needs a `DECISION_PROVIDER`.
305
307
  ...capabilityRouterEnvShape,
308
+ ...channelRoutingEnvShape,
306
309
 
307
310
  // --- durable runs (docs/plans/durable-runs.md) ---------------------------
308
311
  // Parsed by `runDurabilityConfig` (src/do/run-store.ts) with defaults:
@@ -393,6 +396,20 @@ export const baseEnvSchema = z.object({
393
396
  .min(1_000)
394
397
  .max(3_600_000)
395
398
  .default(USER_CONTEXT_CACHE_TTL_MS_DEFAULT),
399
+ /**
400
+ * How long a channel session must be quiet after its last finished run
401
+ * before its new history is indexed into the memory engine (ms): a burst
402
+ * of messages is one upload, and one prefetch-cache invalidation. `0`
403
+ * indexes after every finished run. At most 10 min: the wait is an
404
+ * in-memory timer that keeps the user object resident, and a Durable
405
+ * Object keeps pending work alive for 15 min at most.
406
+ */
407
+ CHANNEL_HISTORY_INDEX_DEBOUNCE_MS: z.coerce
408
+ .number()
409
+ .int()
410
+ .min(0)
411
+ .max(600_000)
412
+ .default(CHANNEL_HISTORY_INDEX_DEBOUNCE_MS_DEFAULT),
396
413
 
397
414
  // --- misc ---------------------------------------------------------------
398
415
  LOG_LEVEL: z.string().default('info'),
@@ -1,3 +1,9 @@
1
+ import type {
2
+ ChannelControlInput,
3
+ ChannelControlOutcome,
4
+ ChannelReplyContext,
5
+ ChannelReplyReference,
6
+ } from '../channels/contract';
1
7
  import type {
2
8
  BeginTopicStep,
3
9
  TopicReceiptReferences,
@@ -314,6 +320,10 @@ export interface OracleWorkerEnv {
314
320
  * `on` preloads the predicted on-demand plugin's tools for the turn.
315
321
  */
316
322
  CAPABILITY_ROUTER?: CapabilityRouterMode;
323
+ /** Applies exclusively to authenticated channel admission; Portal/Matrix turns never route. */
324
+ CHANNEL_SEMANTIC_ROUTING?: 'off' | 'shadow' | 'on';
325
+ CHANNEL_SEMANTIC_ROUTING_BINDINGS?: string;
326
+ CHANNEL_SEMANTIC_ROUTING_MIN_PROBABILITY?: string;
317
327
 
318
328
  // --- langsmith tracing ---------------------------------------------------
319
329
  /** `'true'` traces every turn (explicit tracer — Workers has no env auto-attach). */
@@ -386,6 +396,10 @@ export interface TurnRequest {
386
396
  bindingId: string;
387
397
  bindingRevision?: number;
388
398
  remoteMessageRef: string;
399
+ replyContext?: ChannelReplyContext;
400
+ replyTo?: ChannelReplyReference;
401
+ /** Host-generated direct reply: no agent or tools run for an ambiguous destination. */
402
+ routingClarification?: string;
389
403
  };
390
404
  executionProfile?: TaskExecutionProfile;
391
405
  identity: TurnIdentity;
@@ -741,6 +755,11 @@ export interface UserOracleObject extends Rpc.DurableObjectBranded {
741
755
  operationId: string,
742
756
  command: TopicDeliverableCommand,
743
757
  ): Promise<TopicDeliverableResult>;
758
+ channelControl(
759
+ identity: TurnIdentity,
760
+ input: ChannelControlInput,
761
+ requestHash: string,
762
+ ): Promise<ChannelControlOutcome>;
744
763
  channelTurn(
745
764
  identity: TurnIdentity,
746
765
  input: ChannelTurnInput,
@@ -1,4 +1,7 @@
1
- import { channelOrigin } from '../channels/contract';
1
+ import {
2
+ channelOrigin,
3
+ type ChannelReplyReference,
4
+ } from '../channels/contract';
2
5
  /**
3
6
  * The room mirror of HTTP chats: every Portal turn is replayed into the
4
7
  * user's oracle room as a thread under the session's marker event, the user
@@ -46,6 +49,7 @@ export interface MirrorSend {
46
49
  provider: string;
47
50
  bindingId: string;
48
51
  remoteMessageRef: string;
52
+ replyTo?: ChannelReplyReference;
49
53
  };
50
54
  }
51
55
 
@@ -75,6 +79,9 @@ export function mirrorEventContent(send: MirrorSend) {
75
79
  ...(send.origin
76
80
  ? {
77
81
  'org.ixo.qi.origin': channelOrigin(send.origin),
82
+ ...(send.origin.replyTo
83
+ ? { 'org.ixo.qi.reply': send.origin.replyTo }
84
+ : {}),
78
85
  }
79
86
  : {}),
80
87
  };
@@ -453,6 +453,33 @@ export class RunCoordinator {
453
453
  return stopped;
454
454
  }
455
455
 
456
+ /** Stop exactly the run a channel control names; never a later turn. */
457
+ abortById(runId: string): Promise<void> {
458
+ return this.abortByIds([runId]);
459
+ }
460
+
461
+ /**
462
+ * Stop exactly the runs a channel control names, never a later turn.
463
+ * Every run is signalled before any is waited for, and the wait is one
464
+ * shared supersede grace: a control naming many runs must answer well
465
+ * inside the caller's timeout. A run may still be winding down when this
466
+ * resolves. Runs that are not live here (ended, never begun) are skipped.
467
+ */
468
+ async abortByIds(runIds: readonly string[]): Promise<void> {
469
+ const runs = [...new Set(runIds)].flatMap((runId) => {
470
+ const run = this.live.get(runId);
471
+ return run ? [run] : [];
472
+ });
473
+ if (runs.length === 0) return;
474
+ // Queued runs first, so an attempt that ends below dequeues none of them.
475
+ for (const run of runs)
476
+ if (run.record.status === 'queued')
477
+ void this.cancelQueued(run, 'aborted');
478
+ for (const run of runs)
479
+ if (run.record.status !== 'queued') this.abortRun(run, 'aborted');
480
+ await withinGrace(this.graceMs, Promise.all(runs.map((run) => run.done)));
481
+ }
482
+
456
483
  /**
457
484
  * End every run of the session: the executing attempt is aborted, queued
458
485
  * runs and runs waiting for a recovery attempt are closed, and messages
@@ -1,3 +1,4 @@
1
+ import { userContent } from '../channels/replies';
1
2
  /**
2
3
  * Transcript → client DTO. Port of `@ixo/common`'s
3
4
  * `transformGraphStateMessageToListMessageResponse` so `GET /messages/:id`
@@ -13,11 +14,7 @@ import {
13
14
  isAttachmentViewMessage,
14
15
  } from '../attachments/retention';
15
16
  import { SUMMARY_PREFIX } from '../core/middlewares/summarization';
16
- import {
17
- stripTurnTimeNote,
18
- TURN_TIME_NOTE_KWARG,
19
- } from '../core/turn-time-note';
20
- import { ChannelOrigin } from '../channels/contract';
17
+ import { ChannelOrigin, ChannelReplyReference } from '../channels/contract';
21
18
  import { CREATE_ARTIFACT_TOOL, createArtifactParts } from '../artifacts/tool';
22
19
  import { planText } from '../delivery/schema';
23
20
  import type {
@@ -59,7 +56,10 @@ export interface MessageDto {
59
56
  createdAt?: string;
60
57
  attachment?: AttachmentMeta;
61
58
  attachments?: AttachmentMeta[];
62
- metadata?: { 'org.ixo.qi.origin': ChannelOrigin };
59
+ metadata?: {
60
+ 'org.ixo.qi.origin': ChannelOrigin;
61
+ 'org.ixo.qi.reply'?: ChannelReplyReference;
62
+ };
63
63
  }
64
64
 
65
65
  export interface ListMessagesResponse {
@@ -131,10 +131,7 @@ export function contentToText(content: BaseMessage['content']): string {
131
131
  */
132
132
  export function visibleContent(message: BaseMessage): BaseMessage['content'] {
133
133
  if (message.type !== 'human') return message.content;
134
- return stripTurnTimeNote(
135
- message.content,
136
- message.additional_kwargs?.[TURN_TIME_NOTE_KWARG],
137
- );
134
+ return userContent(message.content, message.additional_kwargs);
138
135
  }
139
136
 
140
137
  /** `visibleContent` as text (titles, mirrors, indexing). */
@@ -216,7 +213,13 @@ export async function transformTranscript(
216
213
  if (attachments?.length) dto.attachments = attachments;
217
214
  if (message.type === 'human') {
218
215
  const origin = ChannelOrigin.safeParse(kw['org.ixo.qi.origin']);
219
- if (origin.success) dto.metadata = { 'org.ixo.qi.origin': origin.data };
216
+ if (origin.success) {
217
+ const reply = ChannelReplyReference.safeParse(kw['org.ixo.qi.reply']);
218
+ dto.metadata = {
219
+ 'org.ixo.qi.origin': origin.data,
220
+ ...(reply.success ? { 'org.ixo.qi.reply': reply.data } : {}),
221
+ };
222
+ }
220
223
  }
221
224
  acc.push(dto);
222
225
  for (const call of dto.toolCalls ?? [])
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Ordering of the messages a turn feeds the graph: hidden attachment context
3
+ * and the person's message.
4
+ */
5
+
6
+ import {
7
+ AIMessage,
8
+ type BaseMessage,
9
+ type HumanMessage,
10
+ } from '@langchain/core/messages';
11
+
12
+ import type { ExtractedAttachment } from '../attachments';
13
+
14
+ export interface TurnMessageParts {
15
+ human: HumanMessage;
16
+ /** Extraction-lane text: follows the HumanMessage (as on Node). */
17
+ extracted: readonly ExtractedAttachment[];
18
+ /**
19
+ * One line per natively-sent file naming its reference. These go BEFORE the
20
+ * HumanMessage so the turn never ends on an assistant message (providers
21
+ * reject or mis-handle an assistant-last conversation).
22
+ */
23
+ nativeNotes: readonly ExtractedAttachment[];
24
+ matrixRoom: boolean;
25
+ now?: () => Date;
26
+ }
27
+
28
+ function hiddenNote(
29
+ item: ExtractedAttachment,
30
+ matrixRoom: boolean,
31
+ now: () => Date,
32
+ ): AIMessage {
33
+ return new AIMessage({
34
+ content: item.text,
35
+ additional_kwargs: {
36
+ msgFromMatrixRoom: matrixRoom,
37
+ timestamp: now().toISOString(),
38
+ attachment: item.meta,
39
+ },
40
+ });
41
+ }
42
+
43
+ /**
44
+ * Native notes, then the HumanMessage, then the extraction-lane text. The
45
+ * hidden messages carry an `attachment` kwarg, which the transcript hides.
46
+ */
47
+ export function assembleTurnMessages(parts: TurnMessageParts): BaseMessage[] {
48
+ const now = parts.now ?? (() => new Date());
49
+ return [
50
+ ...parts.nativeNotes.map((n) => hiddenNote(n, parts.matrixRoom, now)),
51
+ parts.human,
52
+ ...parts.extracted.map((n) => hiddenNote(n, parts.matrixRoom, now)),
53
+ ];
54
+ }