@ixo/oracle-runtime-workers 0.18.0 → 0.20.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/package.json +4 -4
- package/src/channels/auth.ts +30 -8
- package/src/channels/contract.ts +25 -12
- package/src/channels/failures.ts +3 -3
- package/src/channels/turns.ts +10 -10
- package/src/core/capability-router.ts +1 -0
- package/src/core/env.ts +32 -0
- package/src/core/main-agent.ts +20 -0
- package/src/core/middlewares/tool-marks.ts +2 -1
- package/src/core/prompt-composer.ts +3 -1
- package/src/delivery/profile.ts +4 -1
- package/src/delivery/schema.ts +1 -1
- package/src/do/contracts.ts +43 -5
- package/src/do/room-mirror.ts +1 -1
- package/src/do/user-oracle-do.ts +328 -14
- package/src/memory/session-history-indexer.ts +3 -0
- package/src/memory/user-context-fetcher.ts +977 -0
- package/src/memory/user-context-invalidation.ts +66 -0
- package/src/shell/app.ts +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ixo/oracle-runtime-workers",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "QiForge oracle runtime for Cloudflare Workers — Durable Object per user (SQLite in WASM over DO storage), Matrix E2EE gateway Durable Object, Hono HTTP/SSE shell.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -58,9 +58,9 @@
|
|
|
58
58
|
"wa-sqlite": "1.0.0",
|
|
59
59
|
"yjs": "13.6.32",
|
|
60
60
|
"zod": "^4.4.3",
|
|
61
|
-
"@ixo/
|
|
62
|
-
"@ixo/
|
|
63
|
-
"@ixo/
|
|
61
|
+
"@ixo/common": "1.7.0",
|
|
62
|
+
"@ixo/ixo-transaction": "0.1.0",
|
|
63
|
+
"@ixo/ucan": "2.2.1"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@cloudflare/vitest-pool-workers": "^0.22.0",
|
package/src/channels/auth.ts
CHANGED
|
@@ -41,7 +41,8 @@ export const ChannelInvoke = defineCapability({
|
|
|
41
41
|
|
|
42
42
|
export interface ChannelAuthConfig {
|
|
43
43
|
oracleDid: string;
|
|
44
|
-
|
|
44
|
+
/** Every channel service DID allowed to invoke this deployment. */
|
|
45
|
+
channelDids: readonly string[];
|
|
45
46
|
didResolver: DIDKeyResolver;
|
|
46
47
|
}
|
|
47
48
|
|
|
@@ -85,12 +86,16 @@ export async function authenticateChannel(
|
|
|
85
86
|
);
|
|
86
87
|
const chain = result.proofChain;
|
|
87
88
|
const userDid = chain?.[0];
|
|
89
|
+
// The invoker is the one listed channel service that signed this
|
|
90
|
+
// invocation; the user's grant must be addressed to that same service.
|
|
91
|
+
const channelDid = result.invoker;
|
|
88
92
|
if (
|
|
89
93
|
!result.ok ||
|
|
90
94
|
!userDid?.startsWith('did:ixo:') ||
|
|
91
95
|
chain?.length !== 2 ||
|
|
92
|
-
|
|
93
|
-
|
|
96
|
+
!channelDid ||
|
|
97
|
+
!config.channelDids.includes(channelDid) ||
|
|
98
|
+
chain[1] !== channelDid
|
|
94
99
|
)
|
|
95
100
|
throw new ChannelError(
|
|
96
101
|
401,
|
|
@@ -106,7 +111,7 @@ export async function authenticateChannel(
|
|
|
106
111
|
!('issuer' in proof) ||
|
|
107
112
|
proof.proofs.length !== 0 ||
|
|
108
113
|
proof.issuer.did() !== userDid ||
|
|
109
|
-
proof.audience.did() !==
|
|
114
|
+
proof.audience.did() !== channelDid ||
|
|
110
115
|
proof.capabilities.length !== 1 ||
|
|
111
116
|
proof.capabilities[0]?.can !== 'ixo:channel/invoke' ||
|
|
112
117
|
proof.capabilities[0]?.with !== `ixo:channel:${input.bindingId}` ||
|
|
@@ -159,7 +164,7 @@ export async function authenticateChannel(
|
|
|
159
164
|
return {
|
|
160
165
|
userDid,
|
|
161
166
|
channel: {
|
|
162
|
-
callerDid:
|
|
167
|
+
callerDid: channelDid,
|
|
163
168
|
provider: input.provider,
|
|
164
169
|
bindingId: input.bindingId,
|
|
165
170
|
bindingRevision: input.bindingRevision,
|
|
@@ -176,13 +181,30 @@ const channelWebResolver = createWebDIDResolver({
|
|
|
176
181
|
cacheTtlMs: IXO_DID_RESOLUTION_CACHE_TTL_MS,
|
|
177
182
|
});
|
|
178
183
|
|
|
184
|
+
/**
|
|
185
|
+
* `CHANNEL_SERVICE_DID` is one DID or a comma-separated list (one channel
|
|
186
|
+
* service per messaging provider, or a rotation overlap): entries are
|
|
187
|
+
* trimmed, empty ones dropped and duplicates removed, order kept.
|
|
188
|
+
*/
|
|
189
|
+
export function parseChannelServiceDids(raw: string | undefined): string[] {
|
|
190
|
+
return [
|
|
191
|
+
...new Set(
|
|
192
|
+
(raw ?? '')
|
|
193
|
+
.split(',')
|
|
194
|
+
.map((did) => did.trim())
|
|
195
|
+
.filter((did) => did !== ''),
|
|
196
|
+
),
|
|
197
|
+
];
|
|
198
|
+
}
|
|
199
|
+
|
|
179
200
|
export function channelAuthConfig(
|
|
180
201
|
env: Pick<
|
|
181
202
|
OracleWorkerEnv,
|
|
182
203
|
'CHANNEL_SERVICE_DID' | 'BLOCKSYNC_GRAPHQL_URL' | 'ORACLE_DID'
|
|
183
204
|
>,
|
|
184
205
|
): ChannelAuthConfig {
|
|
185
|
-
|
|
206
|
+
const channelDids = parseChannelServiceDids(env.CHANNEL_SERVICE_DID);
|
|
207
|
+
if (channelDids.length === 0)
|
|
186
208
|
throw new ChannelError(
|
|
187
209
|
503,
|
|
188
210
|
'Channels are not configured',
|
|
@@ -194,10 +216,10 @@ export function channelAuthConfig(
|
|
|
194
216
|
]);
|
|
195
217
|
return {
|
|
196
218
|
oracleDid: env.ORACLE_DID,
|
|
197
|
-
|
|
219
|
+
channelDids,
|
|
198
220
|
didResolver: async (did) => {
|
|
199
221
|
if (
|
|
200
|
-
did
|
|
222
|
+
channelDids.includes(did) ||
|
|
201
223
|
did === env.ORACLE_DID ||
|
|
202
224
|
did.startsWith('did:ixo:')
|
|
203
225
|
)
|
package/src/channels/contract.ts
CHANGED
|
@@ -1,22 +1,34 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { ReplyPlan } from '../delivery/types';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* A channel provider id (`whatsapp`, later others): lower-case, starts with a
|
|
6
|
+
* letter, at most 32 characters. The runtime does not enumerate providers;
|
|
7
|
+
* the gateway and the Auth Hub do. An unknown provider is delivered with the
|
|
8
|
+
* default delivery profile.
|
|
9
|
+
*/
|
|
10
|
+
export const ChannelProviderId = z.string().regex(/^[a-z][a-z0-9_-]{0,31}$/);
|
|
11
|
+
|
|
4
12
|
export const ChannelTurnBody = z.strictObject({
|
|
5
|
-
provider:
|
|
13
|
+
provider: ChannelProviderId,
|
|
6
14
|
bindingId: z.string().regex(/^chb_[A-Za-z0-9_-]{1,120}$/),
|
|
7
15
|
bindingRevision: z.number().int().positive().safe(),
|
|
8
16
|
requestId: z.string().min(1).max(180),
|
|
9
17
|
remoteMessageRef: z.string().regex(/^hmac:[a-f0-9]{64}$/),
|
|
10
18
|
sessionId: z.string().min(1).max(255).optional(),
|
|
11
19
|
message: z.string().min(1).max(16_000),
|
|
12
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Which kind of oracle the gateway believes it addresses. `companion` is
|
|
22
|
+
* the legacy value; the gateway sends `oracle`. Both are accepted.
|
|
23
|
+
*/
|
|
24
|
+
context: z.strictObject({ kind: z.enum(['companion', 'oracle']) }),
|
|
13
25
|
});
|
|
14
26
|
|
|
15
27
|
export type ChannelTurnInput = z.infer<typeof ChannelTurnBody>;
|
|
16
28
|
|
|
17
29
|
export const ChannelOrigin = z.strictObject({
|
|
18
30
|
v: z.literal(1),
|
|
19
|
-
transport:
|
|
31
|
+
transport: ChannelProviderId,
|
|
20
32
|
binding_id: ChannelTurnBody.shape.bindingId,
|
|
21
33
|
remote_ref: ChannelTurnBody.shape.remoteMessageRef,
|
|
22
34
|
});
|
|
@@ -39,7 +51,7 @@ export function channelOrigin(
|
|
|
39
51
|
|
|
40
52
|
export interface ChannelIdentity {
|
|
41
53
|
callerDid: string;
|
|
42
|
-
provider:
|
|
54
|
+
provider: string;
|
|
43
55
|
bindingId: string;
|
|
44
56
|
bindingRevision: number;
|
|
45
57
|
}
|
|
@@ -82,7 +94,7 @@ export type ChannelErrorStatus =
|
|
|
82
94
|
* The machine-readable reason a channel turn was refused. The gateway maps
|
|
83
95
|
* a refusal by `code` first and by HTTP status second. 428 (Precondition
|
|
84
96
|
* Required) means a precondition on the user's side is missing: fix it
|
|
85
|
-
* (authorize the oracle again, finish setting up the
|
|
97
|
+
* (authorize the oracle again, finish setting up the user's oracle room) and
|
|
86
98
|
* retry; it is never a transient outage.
|
|
87
99
|
*
|
|
88
100
|
* | code | status | meaning |
|
|
@@ -93,16 +105,17 @@ export type ChannelErrorStatus =
|
|
|
93
105
|
* | `identity_mismatch` | 403 | the channel identity does not match the request |
|
|
94
106
|
* | `identity_required` | 403 | the binding check got no channel identity |
|
|
95
107
|
* | `binding_inactive` | 403 | the Auth Hub reports the binding inactive |
|
|
96
|
-
* | `channel_grant_not_tool_authority`| 403 | a channel request carried a delegation header |
|
|
97
|
-
* | `
|
|
108
|
+
* | `channel_grant_not_tool_authority`| 403 | a channel request carried a delegation header |
|
|
109
|
+
* | `storage_unauthorized` | 403 | the user's file storage rejected the oracle (final) |
|
|
110
|
+
* | `session_not_found` | 404 | the session is not the user's oracle session |
|
|
98
111
|
* | `request_conflict` | 409 | the request id already belongs to another message |
|
|
99
112
|
* | `session_conflict` | 409 | another session is bound, or the named one is gone: |
|
|
100
113
|
* | | | resend the same request without `sessionId` |
|
|
101
|
-
* | `room_not_encrypted` | 409 | the
|
|
102
|
-
* | `room_unavailable` | 409 | the oracle cannot use the
|
|
114
|
+
* | `room_not_encrypted` | 409 | the user's oracle room is not end-to-end encrypted |
|
|
115
|
+
* | `room_unavailable` | 409 | the oracle cannot use the user's oracle room (final) |
|
|
103
116
|
* | `response_expired` | 410 | the run was pruned; the request cannot run again |
|
|
104
117
|
* | `request_too_large` | 413 | the body is over 64 kB |
|
|
105
|
-
* | `room_not_ready` | 428 | the user has no
|
|
118
|
+
* | `room_not_ready` | 428 | the user has no oracle room yet |
|
|
106
119
|
* | `delegation_required` | 428 | no usable delegation to this oracle (see below) |
|
|
107
120
|
* | `rate_limited` | 429 | too many requests for this user |
|
|
108
121
|
* | `not_configured` | 503 | channels or binding validation are not configured |
|
|
@@ -161,7 +174,7 @@ export class ChannelError extends Error {
|
|
|
161
174
|
* one at once).
|
|
162
175
|
*/
|
|
163
176
|
export class StaleSessionError extends ChannelError {
|
|
164
|
-
constructor(message = '
|
|
177
|
+
constructor(message = 'Oracle session not found') {
|
|
165
178
|
super(404, message, 'session_not_found');
|
|
166
179
|
this.name = 'StaleSessionError';
|
|
167
180
|
}
|
|
@@ -195,7 +208,7 @@ export interface ChannelDelegation {
|
|
|
195
208
|
export function delegationRequiredError(): ChannelError {
|
|
196
209
|
return new ChannelError(
|
|
197
210
|
428,
|
|
198
|
-
'
|
|
211
|
+
'Delegation required: the user must authorize this oracle again',
|
|
199
212
|
'delegation_required',
|
|
200
213
|
);
|
|
201
214
|
}
|
package/src/channels/failures.ts
CHANGED
|
@@ -41,7 +41,7 @@ export function isRoomForbidden(error: unknown): boolean {
|
|
|
41
41
|
* The final refusal for a failure retrying cannot fix, or null when the
|
|
42
42
|
* failure stays a transient `503 unavailable`:
|
|
43
43
|
*
|
|
44
|
-
* - the
|
|
44
|
+
* - the user's oracle room refused the oracle → `409 room_unavailable`;
|
|
45
45
|
* - the user's file storage rejected the oracle's credentials while the
|
|
46
46
|
* owner copy loaded (`VFS_AUTH_FAILED`; `NO_VFS_DELEGATION` is a
|
|
47
47
|
* `428 delegation_required` and is handled before this) →
|
|
@@ -51,13 +51,13 @@ export function classifyChannelFailure(error: unknown): ChannelError | null {
|
|
|
51
51
|
if (parseOwnerCopyFailure(error)?.code === 'VFS_AUTH_FAILED')
|
|
52
52
|
return new ChannelError(
|
|
53
53
|
403,
|
|
54
|
-
"The user's file storage rejected the oracle; the
|
|
54
|
+
"The user's file storage rejected the oracle; the oracle cannot load the user's data",
|
|
55
55
|
'storage_unauthorized',
|
|
56
56
|
);
|
|
57
57
|
if (isRoomForbidden(error))
|
|
58
58
|
return new ChannelError(
|
|
59
59
|
409,
|
|
60
|
-
|
|
60
|
+
"The oracle cannot use the user's oracle room (it is not a member or was refused)",
|
|
61
61
|
'room_unavailable',
|
|
62
62
|
);
|
|
63
63
|
return null;
|
package/src/channels/turns.ts
CHANGED
|
@@ -23,11 +23,11 @@ interface ReceiptRow extends Record<string, string | number | null> {
|
|
|
23
23
|
/** 1 once the user message was mirrored (or there is none to mirror). */
|
|
24
24
|
user_mirrored: number;
|
|
25
25
|
/**
|
|
26
|
-
* 0 while the reply is owed, 1 once it was mirrored into the
|
|
26
|
+
* 0 while the reply is owed, 1 once it was mirrored into the user's oracle
|
|
27
27
|
* room, 2 when the run ended without a reply to mirror.
|
|
28
28
|
*/
|
|
29
29
|
reply_mirrored: number;
|
|
30
|
-
/** 1 while a mirror of this request is owed to the
|
|
30
|
+
/** 1 while a mirror of this request is owed to the user's oracle room. */
|
|
31
31
|
mirror_pending: number;
|
|
32
32
|
/** Failed mirror attempts since the last success. */
|
|
33
33
|
mirror_failures: number;
|
|
@@ -71,7 +71,7 @@ export interface ChannelTurnsHost {
|
|
|
71
71
|
/**
|
|
72
72
|
* Throws `StaleSessionError` when the object holds no such session (it
|
|
73
73
|
* was deleted), any other `ChannelError` when it is not the user's
|
|
74
|
-
* session in the encrypted
|
|
74
|
+
* session in the encrypted oracle room.
|
|
75
75
|
*/
|
|
76
76
|
assertSession(identity: TurnIdentity, sessionId: string): Promise<void>;
|
|
77
77
|
/**
|
|
@@ -86,7 +86,7 @@ export interface ChannelTurnsHost {
|
|
|
86
86
|
begin(runId: string, request: TurnRequest): Promise<RunRecord>;
|
|
87
87
|
/** The turn a run was admitted with, read back from its stored request. */
|
|
88
88
|
requestOf(record: RunRecord): TurnRequest;
|
|
89
|
-
/** Post one message into the
|
|
89
|
+
/** Post one message into the user's oracle room; rejects when it could not. */
|
|
90
90
|
mirror(
|
|
91
91
|
request: TurnRequest,
|
|
92
92
|
text: string,
|
|
@@ -119,7 +119,7 @@ type Received =
|
|
|
119
119
|
* (gateway RPCs and a homeserver read);
|
|
120
120
|
* 3. under the lock again, SQLite only: bind the session and begin the run.
|
|
121
121
|
*
|
|
122
|
-
* The
|
|
122
|
+
* The user's oracle room mirrors (the user message, then the reply) are owed by
|
|
123
123
|
* the receipt and sent in the background, never awaited by a turn or a poll.
|
|
124
124
|
*/
|
|
125
125
|
export class ChannelTurns {
|
|
@@ -257,7 +257,7 @@ export class ChannelTurns {
|
|
|
257
257
|
if (stale)
|
|
258
258
|
throw new ChannelError(
|
|
259
259
|
409,
|
|
260
|
-
'The
|
|
260
|
+
'The oracle session of this channel no longer exists; send the request again without sessionId',
|
|
261
261
|
'session_conflict',
|
|
262
262
|
);
|
|
263
263
|
throw error;
|
|
@@ -294,7 +294,7 @@ export class ChannelTurns {
|
|
|
294
294
|
}
|
|
295
295
|
|
|
296
296
|
/**
|
|
297
|
-
* A run ended: mirror what it still owes the
|
|
297
|
+
* A run ended: mirror what it still owes the user's oracle room (its reply)
|
|
298
298
|
* in the background. The receipt records each mirror, so nothing is sent
|
|
299
299
|
* twice; a failed one is retried by the housekeeping alarm.
|
|
300
300
|
*/
|
|
@@ -318,7 +318,7 @@ export class ChannelTurns {
|
|
|
318
318
|
|
|
319
319
|
/**
|
|
320
320
|
* The session was deleted: release every binding that pointed at it, so
|
|
321
|
-
* the binding's next message opens a new
|
|
321
|
+
* the binding's next message opens a new oracle session. Returns
|
|
322
322
|
* whether a binding was released.
|
|
323
323
|
*/
|
|
324
324
|
forgetSession(sessionId: string): Promise<boolean> {
|
|
@@ -621,7 +621,7 @@ export class ChannelTurns {
|
|
|
621
621
|
}
|
|
622
622
|
return this.respond(input, receipt, session, record);
|
|
623
623
|
}
|
|
624
|
-
// The run owes its user message to the
|
|
624
|
+
// The run owes its user message to the user's oracle room from here on;
|
|
625
625
|
// the mirror pass sends it, the turn never waits for it.
|
|
626
626
|
await this.db.run(
|
|
627
627
|
`UPDATE channel_requests SET session_id = ?, user_mirrored = 0, reply_mirrored = 0,
|
|
@@ -710,7 +710,7 @@ export class ChannelTurns {
|
|
|
710
710
|
}
|
|
711
711
|
|
|
712
712
|
/**
|
|
713
|
-
* One mirror pass over the receipts that owe the
|
|
713
|
+
* One mirror pass over the receipts that owe the user's oracle room a
|
|
714
714
|
* message. Sessions are mirrored in parallel; within a session strictly
|
|
715
715
|
* in receipt order, the user message before its reply, and a mirror that
|
|
716
716
|
* fails holds the session's later ones back until it succeeds or is
|
|
@@ -222,6 +222,7 @@ export function createCapabilityRouter(
|
|
|
222
222
|
`capability=${verdict.capability ?? '-'} ` +
|
|
223
223
|
`capabilityConfidence=${verdict.capabilityConfidence?.toFixed(2) ?? '-'} ` +
|
|
224
224
|
`wouldPreload=${list(verdict.preload)} reason=${verdict.reason} ` +
|
|
225
|
+
`calibrationProfile=${verdict.calibrationProfileRef ?? '-'} ` +
|
|
225
226
|
`latencyMs=${now() - started} provider=${evaluation.provider} model=${evaluation.model}`,
|
|
226
227
|
);
|
|
227
228
|
turn.onShadowVerdict?.(verdict.preload);
|
package/src/core/env.ts
CHANGED
|
@@ -12,6 +12,10 @@ import {
|
|
|
12
12
|
MAX_ARTIFACT_SWEEP_INTERVAL_HOURS,
|
|
13
13
|
MAX_ARTIFACT_TTL_DAYS,
|
|
14
14
|
} from '../artifacts/config';
|
|
15
|
+
import {
|
|
16
|
+
USER_CONTEXT_BLOCKING_CAP_MS_DEFAULT,
|
|
17
|
+
USER_CONTEXT_CACHE_TTL_MS_DEFAULT,
|
|
18
|
+
} from '../memory/user-context-fetcher';
|
|
15
19
|
|
|
16
20
|
/**
|
|
17
21
|
* Tier-0 (core) environment for the Workers runtime. Mirrors
|
|
@@ -292,6 +296,34 @@ export const baseEnvSchema = z.object({
|
|
|
292
296
|
.max(MAX_ARTIFACT_SWEEP_INTERVAL_HOURS)
|
|
293
297
|
.optional(),
|
|
294
298
|
|
|
299
|
+
// --- memory prefetch (src/memory/user-context-fetcher.ts) ----------------
|
|
300
|
+
/**
|
|
301
|
+
* First-turn user-context prefetch: the six memory-engine queries
|
|
302
|
+
* (identity, work, goals, interests, relationships, recent) that fill the
|
|
303
|
+
* prompt's memory-context block. Only runs where `MEMORY_ENGINE_URL` is
|
|
304
|
+
* set. `off` disables the prefetch; the memory tools keep working.
|
|
305
|
+
*/
|
|
306
|
+
USER_CONTEXT_PREFETCH: z.enum(['on', 'off']).default('on'),
|
|
307
|
+
/**
|
|
308
|
+
* How long a first turn waits for the prefetch (ms) before the model call
|
|
309
|
+
* goes ahead without it; the request keeps running and warms the cache.
|
|
310
|
+
* `0` never waits: the prefetch always runs in the background and the
|
|
311
|
+
* turn uses what is cached or checkpointed.
|
|
312
|
+
*/
|
|
313
|
+
USER_CONTEXT_PREFETCH_CAP_MS: z.coerce
|
|
314
|
+
.number()
|
|
315
|
+
.int()
|
|
316
|
+
.min(0)
|
|
317
|
+
.max(30_000)
|
|
318
|
+
.default(USER_CONTEXT_BLOCKING_CAP_MS_DEFAULT),
|
|
319
|
+
/** How long a room's prefetched context is reused (ms). */
|
|
320
|
+
USER_CONTEXT_CACHE_TTL_MS: z.coerce
|
|
321
|
+
.number()
|
|
322
|
+
.int()
|
|
323
|
+
.min(1_000)
|
|
324
|
+
.max(3_600_000)
|
|
325
|
+
.default(USER_CONTEXT_CACHE_TTL_MS_DEFAULT),
|
|
326
|
+
|
|
295
327
|
// --- misc ---------------------------------------------------------------
|
|
296
328
|
LOG_LEVEL: z.string().default('info'),
|
|
297
329
|
CORS_ORIGIN: z.string().default('*'),
|
package/src/core/main-agent.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type AgentMiddleware,
|
|
7
7
|
type StructuredTool,
|
|
8
8
|
} from 'langchain';
|
|
9
|
+
import { z } from 'zod';
|
|
9
10
|
import type {
|
|
10
11
|
PluginContext,
|
|
11
12
|
PluginManifest,
|
|
@@ -120,6 +121,24 @@ function selectByVisibility(
|
|
|
120
121
|
});
|
|
121
122
|
}
|
|
122
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Declares the `userContext` channel (the memory engine's prefetched context
|
|
126
|
+
* the prompt's "What you know about the user" block renders). `createAgent`
|
|
127
|
+
* builds its channels from zod or `StateSchema` state schemas only — the
|
|
128
|
+
* `MainAgentGraphState` annotation passed as `stateSchema` is not one — so
|
|
129
|
+
* without this a `userContext` in the graph input would be dropped instead
|
|
130
|
+
* of checkpointed. A plain field is a last-value channel: a turn that writes
|
|
131
|
+
* it replaces the thread's previous context; one that does not keeps it.
|
|
132
|
+
*/
|
|
133
|
+
function userContextStateMiddleware(): AgentMiddleware {
|
|
134
|
+
return createMiddleware({
|
|
135
|
+
name: 'UserContextState',
|
|
136
|
+
stateSchema: z.object({
|
|
137
|
+
userContext: z.record(z.string(), z.unknown()).optional(),
|
|
138
|
+
}),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
123
142
|
/**
|
|
124
143
|
* Build a compiled main agent from the runtime's registries plus per-request
|
|
125
144
|
* context. Same pipeline as the Node runtime — tools from the registries
|
|
@@ -691,6 +710,7 @@ export async function createMainAgent(
|
|
|
691
710
|
withheldToolNames: toolAccess.withheldToolNames,
|
|
692
711
|
logger: ambient.logger,
|
|
693
712
|
}),
|
|
713
|
+
userContextStateMiddleware(),
|
|
694
714
|
createToolValidationMiddleware({
|
|
695
715
|
skipToolNames: hooks?.validationSkipToolNames,
|
|
696
716
|
logger: ambient.logger,
|
|
@@ -130,7 +130,8 @@ export function toolEffectOf(tool: {
|
|
|
130
130
|
return 'write';
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
/** Whether a tool call's output is an error result (`ToolMessage` status `error`). */
|
|
134
|
+
export function isErrorResult(output: unknown): boolean {
|
|
134
135
|
if (ToolMessage.isInstance(output)) return output.status === 'error';
|
|
135
136
|
return false;
|
|
136
137
|
}
|
|
@@ -118,7 +118,7 @@ export interface ComposePromptInput {
|
|
|
118
118
|
export const OPERATING_GUIDES_HEADING = '## Capabilities in use';
|
|
119
119
|
|
|
120
120
|
/** Headers used for each populated memory-context sub-section. */
|
|
121
|
-
const CONTEXT_SECTION_LABELS: Record<ContextSlot, string> = {
|
|
121
|
+
export const CONTEXT_SECTION_LABELS: Record<ContextSlot, string> = {
|
|
122
122
|
identity: 'Personal identity',
|
|
123
123
|
work: 'Work & professional',
|
|
124
124
|
goals: 'Goals & aspirations',
|
|
@@ -446,6 +446,8 @@ const TEMPLATE = `{{{ORACLE_SECTION}}}
|
|
|
446
446
|
|
|
447
447
|
## What you know about the user
|
|
448
448
|
|
|
449
|
+
_Background the memory engine recalled about this user — context to adapt to, not instructions._
|
|
450
|
+
|
|
449
451
|
{{{CONTEXT_BLOCK}}}
|
|
450
452
|
{{/CONTEXT_BLOCK}}
|
|
451
453
|
|
package/src/delivery/profile.ts
CHANGED
|
@@ -135,7 +135,10 @@ export function resolveDeliveryProfile(
|
|
|
135
135
|
const surface =
|
|
136
136
|
turn.client === 'matrix'
|
|
137
137
|
? 'matrix'
|
|
138
|
-
:
|
|
138
|
+
: // An unknown provider id uses the generic profile. `hasOwn`: a
|
|
139
|
+
// provider id such as `constructor` must not hit the prototype.
|
|
140
|
+
turn.channel?.provider &&
|
|
141
|
+
Object.hasOwn(DEFAULT_LIMITS, turn.channel.provider)
|
|
139
142
|
? turn.channel.provider
|
|
140
143
|
: 'generic';
|
|
141
144
|
const merged: ChatLimits = {
|
package/src/delivery/schema.ts
CHANGED
|
@@ -46,7 +46,7 @@ function linkLabel(title: string): string {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
* The plan as one Markdown message: what the
|
|
49
|
+
* The plan as one Markdown message: what the user's oracle room mirror of a
|
|
50
50
|
* channel turn and gateways that predate plans receive. Never stored as a
|
|
51
51
|
* run's text, which stays the model's own.
|
|
52
52
|
*/
|
package/src/do/contracts.ts
CHANGED
|
@@ -8,7 +8,10 @@ import type {
|
|
|
8
8
|
ChannelTurnOutcome,
|
|
9
9
|
} from '../channels/contract';
|
|
10
10
|
import type { TaskExecutionProfile } from '../core/execution-profile';
|
|
11
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
CapabilityRouterMode,
|
|
13
|
+
DecisionProviderName,
|
|
14
|
+
} from '@ixo/common/ai/decisions';
|
|
12
15
|
import type { TranscriptPageOptions } from './transcript';
|
|
13
16
|
import type { TurnUsage } from '../core/turn-budget';
|
|
14
17
|
import type { BotStatus, EncryptedFileInfo } from '@ixo/matrix-bot-workers-sdk';
|
|
@@ -62,6 +65,7 @@ export interface OracleWorkerEnv {
|
|
|
62
65
|
UCAN_AUTH_MAX_TTL_SECONDS?: string;
|
|
63
66
|
/** `true` restores the legacy bare-delegation authentication (off by default). */
|
|
64
67
|
UCAN_ALLOW_BARE_DELEGATION_AUTH?: string;
|
|
68
|
+
/** The channel service DID, or a comma-separated list of them. */
|
|
65
69
|
CHANNEL_SERVICE_DID?: string;
|
|
66
70
|
AUTH_HUB?: Fetcher;
|
|
67
71
|
AUTH_HUB_URL?: string;
|
|
@@ -273,10 +277,16 @@ export interface OracleWorkerEnv {
|
|
|
273
277
|
NEBIUS_API_KEY?: string;
|
|
274
278
|
|
|
275
279
|
// --- decisions ----------------------------------------------------------
|
|
276
|
-
/** Bounded semantic Decision provider; unset leaves Decisions unconfigured. `openrouter-jev` reuses `OPEN_ROUTER_API_KEY`. */
|
|
277
|
-
DECISION_PROVIDER?:
|
|
278
|
-
/**
|
|
280
|
+
/** Bounded semantic Decision provider; unset leaves Decisions unconfigured. `openrouter-jev` reuses `OPEN_ROUTER_API_KEY`; the Databricks lanes need `DATABRICKS_WORKSPACE_URL` + `DATABRICKS_TOKEN`. */
|
|
281
|
+
DECISION_PROVIDER?: DecisionProviderName;
|
|
282
|
+
/** Model id override (`typesafe/jev` on Workers AI, `typesafe/jev-1.13` on OpenRouter, the Unity Catalog model service `system.ai.openjev-qwen35-4b` for `databricks-systemone`); not used by `databricks-ai-decide`. */
|
|
279
283
|
DECISION_MODEL?: string;
|
|
284
|
+
/** Databricks workspace origin (`https://<host>`, no path or query) for both Databricks lanes. */
|
|
285
|
+
DATABRICKS_WORKSPACE_URL?: string;
|
|
286
|
+
/** Secret. Databricks workspace token for both Databricks lanes (sent as HTTP Basic `token:<token>`). */
|
|
287
|
+
DATABRICKS_TOKEN?: string;
|
|
288
|
+
/** `ai_decide` function API version for `databricks-ai-decide` (default and only accepted value `1.0`). */
|
|
289
|
+
DATABRICKS_AI_DECIDE_VERSION?: string;
|
|
280
290
|
/** Cloudflare account for `cloudflare-jev` over the REST API; not needed when the `AI` binding is declared. */
|
|
281
291
|
CLOUDFLARE_ACCOUNT_ID?: string;
|
|
282
292
|
/** Workers AI token for `cloudflare-jev` over the REST API; not needed when the `AI` binding is declared. */
|
|
@@ -354,7 +364,8 @@ export interface TurnIdentity {
|
|
|
354
364
|
/** Request the shell / gateway sends to `UserOracleDO` to run one turn. */
|
|
355
365
|
export interface TurnRequest {
|
|
356
366
|
channel?: {
|
|
357
|
-
provider
|
|
367
|
+
/** A provider id (`/^[a-z][a-z0-9_-]{0,31}$/`), e.g. `whatsapp`. */
|
|
368
|
+
provider: string;
|
|
358
369
|
bindingId: string;
|
|
359
370
|
remoteMessageRef: string;
|
|
360
371
|
};
|
|
@@ -426,6 +437,31 @@ export interface SessionContextStatus {
|
|
|
426
437
|
lastEventAt?: string;
|
|
427
438
|
}
|
|
428
439
|
|
|
440
|
+
/**
|
|
441
|
+
* The first-turn user-context prefetch for one room (`UserContextFetcher`):
|
|
442
|
+
* whether it runs here, the room's cache, and the last gather's outcome.
|
|
443
|
+
*/
|
|
444
|
+
export interface UserContextPrefetchStatus {
|
|
445
|
+
/** `USER_CONTEXT_PREFETCH` is on and `MEMORY_ENGINE_URL` is set. */
|
|
446
|
+
enabled: boolean;
|
|
447
|
+
/** The room asked about: the session's room, else the user's oracle room. */
|
|
448
|
+
room?: string;
|
|
449
|
+
cached: boolean;
|
|
450
|
+
/** Age of the cached answer. */
|
|
451
|
+
ageMs?: number;
|
|
452
|
+
/** A gather for the room is running right now. */
|
|
453
|
+
inFlight: boolean;
|
|
454
|
+
lastOutcome?: 'cached' | 'fresh' | 'timeout' | 'failed' | 'skipped';
|
|
455
|
+
/** Why the last outcome was `failed` or `skipped`. */
|
|
456
|
+
lastReason?: string;
|
|
457
|
+
/** Duration of the last `fresh` gather. */
|
|
458
|
+
lastMs?: number;
|
|
459
|
+
/** Per slot of the last `fresh` gather: `f12/e8/p3/c1` or `missing`. */
|
|
460
|
+
lastSlots?: Record<string, string>;
|
|
461
|
+
/** When the last outcome was recorded (ISO). */
|
|
462
|
+
lastAt?: string;
|
|
463
|
+
}
|
|
464
|
+
|
|
429
465
|
export interface ContextStatus {
|
|
430
466
|
model: string;
|
|
431
467
|
byoProvider?: string;
|
|
@@ -448,6 +484,8 @@ export interface ContextStatus {
|
|
|
448
484
|
};
|
|
449
485
|
/** Present when a session was asked for; null when that session is unknown. */
|
|
450
486
|
session?: SessionContextStatus | null;
|
|
487
|
+
/** The user-context prefetch for the session's room (else the user's room). */
|
|
488
|
+
userContext: UserContextPrefetchStatus;
|
|
451
489
|
}
|
|
452
490
|
|
|
453
491
|
/**
|