offrouter-core 0.2.0 → 0.2.2
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/dist/audit.d.ts +137 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +320 -0
- package/dist/audit.js.map +1 -0
- package/dist/auth/credential-chain.d.ts +84 -0
- package/dist/auth/credential-chain.d.ts.map +1 -0
- package/dist/auth/credential-chain.js +150 -0
- package/dist/auth/credential-chain.js.map +1 -0
- package/dist/auth/index.d.ts +10 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +7 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/keychain.d.ts +60 -0
- package/dist/auth/keychain.d.ts.map +1 -0
- package/dist/auth/keychain.js +104 -0
- package/dist/auth/keychain.js.map +1 -0
- package/dist/auth/oauth-pkce.d.ts +138 -0
- package/dist/auth/oauth-pkce.d.ts.map +1 -0
- package/dist/auth/oauth-pkce.js +375 -0
- package/dist/auth/oauth-pkce.js.map +1 -0
- package/dist/auth/oauth-server.d.ts +36 -0
- package/dist/auth/oauth-server.d.ts.map +1 -0
- package/dist/auth/oauth-server.js +210 -0
- package/dist/auth/oauth-server.js.map +1 -0
- package/dist/config.d.ts +76 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +355 -0
- package/dist/config.js.map +1 -0
- package/dist/delegation.d.ts +123 -0
- package/dist/delegation.d.ts.map +1 -0
- package/dist/delegation.js +455 -0
- package/dist/delegation.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/policy.d.ts +45 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +318 -0
- package/dist/policy.js.map +1 -0
- package/{src/protocol.ts → dist/protocol.d.ts} +2 -2
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +6 -0
- package/dist/protocol.js.map +1 -0
- package/dist/providers/adapter.d.ts +137 -0
- package/dist/providers/adapter.d.ts.map +1 -0
- package/dist/providers/adapter.js +163 -0
- package/dist/providers/adapter.js.map +1 -0
- package/dist/providers/catalog-data.d.ts +128 -0
- package/dist/providers/catalog-data.d.ts.map +1 -0
- package/dist/providers/catalog-data.js +397 -0
- package/dist/providers/catalog-data.js.map +1 -0
- package/dist/providers/catalog.d.ts +63 -0
- package/dist/providers/catalog.d.ts.map +1 -0
- package/dist/providers/catalog.js +394 -0
- package/dist/providers/catalog.js.map +1 -0
- package/dist/providers/fake.d.ts +46 -0
- package/dist/providers/fake.d.ts.map +1 -0
- package/dist/providers/fake.js +234 -0
- package/dist/providers/fake.js.map +1 -0
- package/dist/providers/openai-compatible.d.ts +65 -0
- package/dist/providers/openai-compatible.d.ts.map +1 -0
- package/dist/providers/openai-compatible.js +434 -0
- package/dist/providers/openai-compatible.js.map +1 -0
- package/dist/proxy/responses-mapper.d.ts +366 -0
- package/dist/proxy/responses-mapper.d.ts.map +1 -0
- package/dist/proxy/responses-mapper.js +517 -0
- package/dist/proxy/responses-mapper.js.map +1 -0
- package/dist/proxy/responses-server.d.ts +29 -0
- package/dist/proxy/responses-server.d.ts.map +1 -0
- package/dist/proxy/responses-server.js +360 -0
- package/dist/proxy/responses-server.js.map +1 -0
- package/dist/router.d.ts +18 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +296 -0
- package/dist/router.js.map +1 -0
- package/dist/schemas.d.ts +560 -0
- package/dist/schemas.d.ts.map +1 -0
- package/{src/schemas.ts → dist/schemas.js} +83 -103
- package/dist/schemas.js.map +1 -0
- package/dist/secrets.d.ts +112 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.js +326 -0
- package/dist/secrets.js.map +1 -0
- package/dist/types.d.ts +117 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/usage-file.d.ts +59 -0
- package/dist/usage-file.d.ts.map +1 -0
- package/dist/usage-file.js +316 -0
- package/dist/usage-file.js.map +1 -0
- package/dist/usage.d.ts +235 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +517 -0
- package/dist/usage.js.map +1 -0
- package/package.json +9 -4
- package/src/audit.test.ts +0 -302
- package/src/audit.ts +0 -553
- package/src/auth/credential-chain.test.ts +0 -326
- package/src/auth/credential-chain.ts +0 -235
- package/src/auth/index.ts +0 -45
- package/src/auth/keychain.test.ts +0 -265
- package/src/auth/keychain.ts +0 -168
- package/src/auth/oauth-pkce.test.ts +0 -329
- package/src/auth/oauth-pkce.ts +0 -571
- package/src/auth/oauth-server.test.ts +0 -83
- package/src/auth/oauth-server.ts +0 -296
- package/src/config.test.ts +0 -479
- package/src/config.ts +0 -505
- package/src/delegation.test.ts +0 -368
- package/src/delegation.ts +0 -605
- package/src/index.ts +0 -280
- package/src/policy.test.ts +0 -634
- package/src/policy.ts +0 -420
- package/src/providers/adapter.test.ts +0 -293
- package/src/providers/adapter.ts +0 -328
- package/src/providers/catalog-data.test.ts +0 -258
- package/src/providers/catalog-data.ts +0 -498
- package/src/providers/catalog.test.ts +0 -84
- package/src/providers/catalog.ts +0 -483
- package/src/providers/fake.ts +0 -312
- package/src/providers/openai-compatible.test.ts +0 -366
- package/src/providers/openai-compatible.ts +0 -554
- package/src/proxy/responses-mapper.test.ts +0 -290
- package/src/proxy/responses-mapper.ts +0 -736
- package/src/proxy/responses-server.test.ts +0 -322
- package/src/proxy/responses-server.ts +0 -469
- package/src/router.test.ts +0 -699
- package/src/router.ts +0 -352
- package/src/schemas.test.ts +0 -291
- package/src/secrets.test.ts +0 -271
- package/src/secrets.ts +0 -461
- package/src/types.ts +0 -173
- package/src/usage-file.test.ts +0 -243
- package/src/usage-file.ts +0 -435
- package/src/usage.test.ts +0 -335
- package/src/usage.ts +0 -859
- package/tsconfig.json +0 -9
package/src/delegation.ts
DELETED
|
@@ -1,605 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal in-process delegation runtime for OffRouter V1.
|
|
3
|
-
*
|
|
4
|
-
* - Routes with digest/preview only until policy allows a provider.
|
|
5
|
-
* - Full prompt text is sent to ProviderAdapter only after allow.
|
|
6
|
-
* - Active tasks live in memory in this process.
|
|
7
|
-
* - Cancellation only affects active same-process tasks.
|
|
8
|
-
*/
|
|
9
|
-
import { randomBytes } from "node:crypto";
|
|
10
|
-
import { InMemoryAuditStore, type AuditStore } from "./audit.js";
|
|
11
|
-
import type { PolicyConfig } from "./policy.js";
|
|
12
|
-
import type { ProviderAdapter } from "./providers/adapter.js";
|
|
13
|
-
import { isProviderError } from "./providers/adapter.js";
|
|
14
|
-
import { routeRequest } from "./router.js";
|
|
15
|
-
import type {
|
|
16
|
-
PolicyDenial,
|
|
17
|
-
ProviderCandidate,
|
|
18
|
-
RouteDecision,
|
|
19
|
-
RouteRequest,
|
|
20
|
-
} from "./types.js";
|
|
21
|
-
|
|
22
|
-
export type DelegationMode = "oneshot" | "stream" | "background";
|
|
23
|
-
export type DelegationOutput = "text" | "json" | "patch";
|
|
24
|
-
export type DelegationTaskStatus =
|
|
25
|
-
"queued" | "running" | "completed" | "cancelled" | "failed";
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Delegation carries full prompt text only after the caller is past policy.
|
|
29
|
-
* The runtime still re-checks policy before invoke and rejects when blocked.
|
|
30
|
-
*/
|
|
31
|
-
export interface DelegationRequest {
|
|
32
|
-
taskId: string;
|
|
33
|
-
mode: DelegationMode;
|
|
34
|
-
output: DelegationOutput;
|
|
35
|
-
/** Full user prompt; never audited raw. */
|
|
36
|
-
prompt: string;
|
|
37
|
-
route: RouteRequest;
|
|
38
|
-
model?: string;
|
|
39
|
-
providerId?: string;
|
|
40
|
-
maxOutputTokens?: number;
|
|
41
|
-
temperature?: number;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export type DelegationEvent =
|
|
45
|
-
| { type: "start"; taskId: string; callId: string; model: string }
|
|
46
|
-
| { type: "text-delta"; taskId: string; callId: string; text: string }
|
|
47
|
-
| {
|
|
48
|
-
type: "usage";
|
|
49
|
-
taskId: string;
|
|
50
|
-
callId: string;
|
|
51
|
-
inputTokens?: number;
|
|
52
|
-
outputTokens?: number;
|
|
53
|
-
totalTokens?: number;
|
|
54
|
-
}
|
|
55
|
-
| {
|
|
56
|
-
type: "done";
|
|
57
|
-
taskId: string;
|
|
58
|
-
callId: string;
|
|
59
|
-
finishReason?: string;
|
|
60
|
-
text?: string;
|
|
61
|
-
}
|
|
62
|
-
| {
|
|
63
|
-
type: "error";
|
|
64
|
-
taskId: string;
|
|
65
|
-
callId?: string;
|
|
66
|
-
message: string;
|
|
67
|
-
code?: string;
|
|
68
|
-
}
|
|
69
|
-
| { type: "cancelled"; taskId: string; callId?: string };
|
|
70
|
-
|
|
71
|
-
export interface DelegationTask {
|
|
72
|
-
taskId: string;
|
|
73
|
-
callId: string;
|
|
74
|
-
status: DelegationTaskStatus;
|
|
75
|
-
mode: DelegationMode;
|
|
76
|
-
requestId: string;
|
|
77
|
-
providerId?: string;
|
|
78
|
-
model?: string;
|
|
79
|
-
createdAt: string;
|
|
80
|
-
updatedAt: string;
|
|
81
|
-
text?: string;
|
|
82
|
-
error?: string;
|
|
83
|
-
events: DelegationEvent[];
|
|
84
|
-
decision?: RouteDecision;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export interface DelegationResult {
|
|
88
|
-
taskId: string;
|
|
89
|
-
status: DelegationTaskStatus;
|
|
90
|
-
text?: string;
|
|
91
|
-
error?: string;
|
|
92
|
-
events: DelegationEvent[];
|
|
93
|
-
decision?: RouteDecision;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export interface DelegationRuntimeOptions {
|
|
97
|
-
adapters: Record<string, ProviderAdapter>;
|
|
98
|
-
candidates: ProviderCandidate[];
|
|
99
|
-
policy: PolicyConfig;
|
|
100
|
-
audit?: AuditStore;
|
|
101
|
-
now?: () => Date;
|
|
102
|
-
/** Default true: store redacted prompt preview on audit append. */
|
|
103
|
-
includePromptPreview?: boolean;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export class DelegationPolicyError extends Error {
|
|
107
|
-
readonly decision: RouteDecision;
|
|
108
|
-
|
|
109
|
-
constructor(message: string, decision: RouteDecision) {
|
|
110
|
-
super(message);
|
|
111
|
-
this.name = "DelegationPolicyError";
|
|
112
|
-
this.decision = decision;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function newCallId(): string {
|
|
117
|
-
return `call_${randomBytes(8).toString("hex")}`;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export class DelegationRuntime {
|
|
121
|
-
private readonly adapters: Record<string, ProviderAdapter>;
|
|
122
|
-
private readonly candidates: ProviderCandidate[];
|
|
123
|
-
private readonly policy: PolicyConfig;
|
|
124
|
-
private readonly audit: AuditStore;
|
|
125
|
-
private readonly now: () => Date;
|
|
126
|
-
private readonly includePromptPreview: boolean;
|
|
127
|
-
private readonly tasks = new Map<string, DelegationTask>();
|
|
128
|
-
private readonly controllers = new Map<string, AbortController>();
|
|
129
|
-
private readonly waiters = new Map<string, Promise<DelegationTask>>();
|
|
130
|
-
|
|
131
|
-
constructor(options: DelegationRuntimeOptions) {
|
|
132
|
-
this.adapters = options.adapters;
|
|
133
|
-
this.candidates = options.candidates;
|
|
134
|
-
this.policy = options.policy;
|
|
135
|
-
this.audit = options.audit ?? new InMemoryAuditStore();
|
|
136
|
-
this.now = options.now ?? (() => new Date());
|
|
137
|
-
this.includePromptPreview = options.includePromptPreview ?? true;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
getTask(taskId: string): DelegationTask | undefined {
|
|
141
|
-
const t = this.tasks.get(taskId);
|
|
142
|
-
return t ? cloneTask(t) : undefined;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
listTasks(): DelegationTask[] {
|
|
146
|
-
return [...this.tasks.values()].map(cloneTask);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* One-shot: run to completion and return aggregated text + events.
|
|
151
|
-
*/
|
|
152
|
-
async delegate(request: DelegationRequest): Promise<DelegationResult> {
|
|
153
|
-
const events: DelegationEvent[] = [];
|
|
154
|
-
for await (const event of this.#run(request, "oneshot")) {
|
|
155
|
-
events.push(event);
|
|
156
|
-
}
|
|
157
|
-
const task = this.tasks.get(request.taskId);
|
|
158
|
-
if (!task) {
|
|
159
|
-
throw new Error(`delegation task ${request.taskId} missing after run`);
|
|
160
|
-
}
|
|
161
|
-
if (task.status === "failed") {
|
|
162
|
-
// still return a result object for inspectability when policy error throws
|
|
163
|
-
}
|
|
164
|
-
return {
|
|
165
|
-
taskId: task.taskId,
|
|
166
|
-
status: task.status,
|
|
167
|
-
text: task.text,
|
|
168
|
-
error: task.error,
|
|
169
|
-
events: [...task.events],
|
|
170
|
-
decision: task.decision,
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Streaming async iterator of DelegationEvents.
|
|
176
|
-
*/
|
|
177
|
-
async *stream(
|
|
178
|
-
request: DelegationRequest,
|
|
179
|
-
): AsyncGenerator<DelegationEvent, void, undefined> {
|
|
180
|
-
yield* this.#run(request, "stream");
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Start background work; returns immediately with a running/queued task.
|
|
185
|
-
*/
|
|
186
|
-
async startBackground(request: DelegationRequest): Promise<DelegationTask> {
|
|
187
|
-
const existing = this.tasks.get(request.taskId);
|
|
188
|
-
if (existing && existing.status === "running") {
|
|
189
|
-
return cloneTask(existing);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const stamped = this.#stampTask(request, "background");
|
|
193
|
-
this.tasks.set(request.taskId, stamped);
|
|
194
|
-
|
|
195
|
-
let releaseRunning!: () => void;
|
|
196
|
-
const runningGate = new Promise<void>((resolve) => {
|
|
197
|
-
releaseRunning = resolve;
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
const runPromise = (async () => {
|
|
201
|
-
try {
|
|
202
|
-
for await (const event of this.#run(request, "background", {
|
|
203
|
-
preRegistered: true,
|
|
204
|
-
onRunning: releaseRunning,
|
|
205
|
-
})) {
|
|
206
|
-
void event;
|
|
207
|
-
// drain
|
|
208
|
-
}
|
|
209
|
-
} catch {
|
|
210
|
-
// policy/provider failure already reflected on task; still open the gate
|
|
211
|
-
releaseRunning();
|
|
212
|
-
} finally {
|
|
213
|
-
releaseRunning();
|
|
214
|
-
}
|
|
215
|
-
return this.tasks.get(request.taskId)!;
|
|
216
|
-
})();
|
|
217
|
-
|
|
218
|
-
this.waiters.set(request.taskId, runPromise);
|
|
219
|
-
// Wait until policy passed (or failed) and task entered a terminal/running state.
|
|
220
|
-
await runningGate;
|
|
221
|
-
const current = this.tasks.get(request.taskId);
|
|
222
|
-
return cloneTask(current ?? stamped);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
async wait(taskId: string): Promise<DelegationTask> {
|
|
226
|
-
const pending = this.waiters.get(taskId);
|
|
227
|
-
if (pending) {
|
|
228
|
-
const task = await pending;
|
|
229
|
-
return cloneTask(task);
|
|
230
|
-
}
|
|
231
|
-
const task = this.tasks.get(taskId);
|
|
232
|
-
if (!task) {
|
|
233
|
-
throw new Error(`unknown delegation task ${taskId}`);
|
|
234
|
-
}
|
|
235
|
-
if (
|
|
236
|
-
task.status === "completed" ||
|
|
237
|
-
task.status === "cancelled" ||
|
|
238
|
-
task.status === "failed"
|
|
239
|
-
) {
|
|
240
|
-
return cloneTask(task);
|
|
241
|
-
}
|
|
242
|
-
// Still running without waiter - poll briefly
|
|
243
|
-
for (let i = 0; i < 200; i++) {
|
|
244
|
-
await sleep(10);
|
|
245
|
-
const t = this.tasks.get(taskId);
|
|
246
|
-
if (
|
|
247
|
-
t &&
|
|
248
|
-
(t.status === "completed" ||
|
|
249
|
-
t.status === "cancelled" ||
|
|
250
|
-
t.status === "failed")
|
|
251
|
-
) {
|
|
252
|
-
return cloneTask(t);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
throw new Error(`timeout waiting for task ${taskId}`);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
async cancel(taskId: string): Promise<DelegationTask | undefined> {
|
|
259
|
-
const task = this.tasks.get(taskId);
|
|
260
|
-
if (!task) return undefined;
|
|
261
|
-
|
|
262
|
-
if (
|
|
263
|
-
task.status === "completed" ||
|
|
264
|
-
task.status === "cancelled" ||
|
|
265
|
-
task.status === "failed"
|
|
266
|
-
) {
|
|
267
|
-
return cloneTask(task);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
this.controllers.get(taskId)?.abort();
|
|
271
|
-
const adapter =
|
|
272
|
-
task.providerId != null ? this.adapters[task.providerId] : undefined;
|
|
273
|
-
if (adapter && task.callId) {
|
|
274
|
-
await adapter.cancel(task.callId);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
task.status = "cancelled";
|
|
278
|
-
task.updatedAt = this.now().toISOString();
|
|
279
|
-
const event: DelegationEvent = {
|
|
280
|
-
type: "cancelled",
|
|
281
|
-
taskId,
|
|
282
|
-
callId: task.callId,
|
|
283
|
-
};
|
|
284
|
-
task.events.push(event);
|
|
285
|
-
|
|
286
|
-
await this.audit.markCancelled(task.requestId, {
|
|
287
|
-
reason: "delegation_cancelled",
|
|
288
|
-
now: this.now,
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
return cloneTask(task);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
async *#run(
|
|
295
|
-
request: DelegationRequest,
|
|
296
|
-
forcedMode: DelegationMode,
|
|
297
|
-
options: { preRegistered?: boolean; onRunning?: () => void } = {},
|
|
298
|
-
): AsyncGenerator<DelegationEvent, void, undefined> {
|
|
299
|
-
const mode = forcedMode;
|
|
300
|
-
const taskId = request.taskId;
|
|
301
|
-
let task = this.tasks.get(taskId);
|
|
302
|
-
|
|
303
|
-
if (!options.preRegistered || !task) {
|
|
304
|
-
task = this.#stampTask(request, mode);
|
|
305
|
-
this.tasks.set(taskId, task);
|
|
306
|
-
} else {
|
|
307
|
-
task.mode = mode;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Policy/routing with digest-only route request - do not rely on full prompt.
|
|
311
|
-
let decision = routeRequest(request.route, this.candidates, this.policy);
|
|
312
|
-
decision = applyRouteOverrideDenial(request, decision);
|
|
313
|
-
task.decision = decision;
|
|
314
|
-
|
|
315
|
-
await this.audit.appendDecision(request.route, decision, {
|
|
316
|
-
includePromptPreview: this.includePromptPreview,
|
|
317
|
-
now: this.now,
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
if (decision.blocked || !decision.route) {
|
|
321
|
-
task.status = "failed";
|
|
322
|
-
task.error =
|
|
323
|
-
decision.explanation || decision.reason || "policy blocked delegation";
|
|
324
|
-
task.updatedAt = this.now().toISOString();
|
|
325
|
-
const event: DelegationEvent = {
|
|
326
|
-
type: "error",
|
|
327
|
-
taskId,
|
|
328
|
-
message: task.error,
|
|
329
|
-
code: "policy_blocked",
|
|
330
|
-
};
|
|
331
|
-
task.events.push(event);
|
|
332
|
-
options.onRunning?.();
|
|
333
|
-
yield event;
|
|
334
|
-
throw new DelegationPolicyError(task.error, decision);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const providerId = decision.route.provider;
|
|
338
|
-
const model = decision.route.model;
|
|
339
|
-
const adapter = this.adapters[providerId];
|
|
340
|
-
if (!adapter) {
|
|
341
|
-
task.status = "failed";
|
|
342
|
-
task.error = `no adapter registered for provider ${providerId}`;
|
|
343
|
-
task.updatedAt = this.now().toISOString();
|
|
344
|
-
const event: DelegationEvent = {
|
|
345
|
-
type: "error",
|
|
346
|
-
taskId,
|
|
347
|
-
message: task.error,
|
|
348
|
-
code: "adapter_missing",
|
|
349
|
-
};
|
|
350
|
-
task.events.push(event);
|
|
351
|
-
yield event;
|
|
352
|
-
throw new Error(task.error);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
const callId = task.callId || newCallId();
|
|
356
|
-
task.callId = callId;
|
|
357
|
-
task.providerId = providerId;
|
|
358
|
-
task.model = model;
|
|
359
|
-
task.status = "running";
|
|
360
|
-
task.updatedAt = this.now().toISOString();
|
|
361
|
-
options.onRunning?.();
|
|
362
|
-
|
|
363
|
-
const controller = new AbortController();
|
|
364
|
-
this.controllers.set(taskId, controller);
|
|
365
|
-
|
|
366
|
-
if (controller.signal.aborted || statusOf(task) === "cancelled") {
|
|
367
|
-
yield* this.#markCancelled(task);
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
const textParts: string[] = [];
|
|
372
|
-
try {
|
|
373
|
-
// Full prompt is only forwarded after policy allowed the provider.
|
|
374
|
-
for await (const pev of adapter.invoke({
|
|
375
|
-
callId,
|
|
376
|
-
model,
|
|
377
|
-
messages: [{ role: "user", content: request.prompt }],
|
|
378
|
-
stream: mode !== "oneshot",
|
|
379
|
-
maxOutputTokens: request.maxOutputTokens,
|
|
380
|
-
temperature: request.temperature,
|
|
381
|
-
signal: controller.signal,
|
|
382
|
-
})) {
|
|
383
|
-
// Honor mid-flight cancellation.
|
|
384
|
-
if (statusOf(task) === "cancelled" || controller.signal.aborted) {
|
|
385
|
-
yield* this.#markCancelled(task);
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
const mapped = mapProviderEvent(taskId, pev);
|
|
390
|
-
if (mapped) {
|
|
391
|
-
task.events.push(mapped);
|
|
392
|
-
if (mapped.type === "text-delta") {
|
|
393
|
-
textParts.push(mapped.text);
|
|
394
|
-
}
|
|
395
|
-
yield mapped;
|
|
396
|
-
if (mapped.type === "error") {
|
|
397
|
-
if (mapped.code === "cancelled") {
|
|
398
|
-
task.status = "cancelled";
|
|
399
|
-
task.updatedAt = this.now().toISOString();
|
|
400
|
-
await this.audit.markCancelled(task.requestId, {
|
|
401
|
-
reason: "provider_cancelled",
|
|
402
|
-
now: this.now,
|
|
403
|
-
});
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
task.status = "failed";
|
|
407
|
-
task.error = mapped.message;
|
|
408
|
-
task.updatedAt = this.now().toISOString();
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
if (mapped.type === "done") {
|
|
412
|
-
task.text = textParts.join("");
|
|
413
|
-
task.status = "completed";
|
|
414
|
-
task.updatedAt = this.now().toISOString();
|
|
415
|
-
await this.audit.markCompleted(task.requestId, { now: this.now });
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// Exhausted without done
|
|
422
|
-
if (statusOf(task) === "running") {
|
|
423
|
-
task.text = textParts.join("");
|
|
424
|
-
task.status = "completed";
|
|
425
|
-
task.updatedAt = this.now().toISOString();
|
|
426
|
-
const done: DelegationEvent = {
|
|
427
|
-
type: "done",
|
|
428
|
-
taskId,
|
|
429
|
-
callId,
|
|
430
|
-
text: task.text,
|
|
431
|
-
};
|
|
432
|
-
task.events.push(done);
|
|
433
|
-
yield done;
|
|
434
|
-
await this.audit.markCompleted(task.requestId, { now: this.now });
|
|
435
|
-
}
|
|
436
|
-
} catch (err) {
|
|
437
|
-
if (statusOf(task) === "cancelled" || controller.signal.aborted) {
|
|
438
|
-
yield* this.#markCancelled(task);
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
if (isProviderError(err) && err.code === "cancelled") {
|
|
442
|
-
yield* this.#markCancelled(task);
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
const message =
|
|
446
|
-
err instanceof Error ? err.message : "provider invoke failed";
|
|
447
|
-
task.status = "failed";
|
|
448
|
-
task.error = message;
|
|
449
|
-
task.updatedAt = this.now().toISOString();
|
|
450
|
-
const event: DelegationEvent = {
|
|
451
|
-
type: "error",
|
|
452
|
-
taskId,
|
|
453
|
-
callId,
|
|
454
|
-
message,
|
|
455
|
-
code: isProviderError(err) ? err.code : "unknown",
|
|
456
|
-
};
|
|
457
|
-
task.events.push(event);
|
|
458
|
-
yield event;
|
|
459
|
-
} finally {
|
|
460
|
-
this.controllers.delete(taskId);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
#stampTask(request: DelegationRequest, mode: DelegationMode): DelegationTask {
|
|
465
|
-
const ts = this.now().toISOString();
|
|
466
|
-
return {
|
|
467
|
-
taskId: request.taskId,
|
|
468
|
-
callId: newCallId(),
|
|
469
|
-
status: "queued",
|
|
470
|
-
mode,
|
|
471
|
-
requestId: request.route.requestId,
|
|
472
|
-
createdAt: ts,
|
|
473
|
-
updatedAt: ts,
|
|
474
|
-
events: [],
|
|
475
|
-
};
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
async *#markCancelled(
|
|
479
|
-
task: DelegationTask,
|
|
480
|
-
): AsyncGenerator<DelegationEvent, void, undefined> {
|
|
481
|
-
if (task.status !== "cancelled") {
|
|
482
|
-
task.status = "cancelled";
|
|
483
|
-
task.updatedAt = this.now().toISOString();
|
|
484
|
-
const event: DelegationEvent = {
|
|
485
|
-
type: "cancelled",
|
|
486
|
-
taskId: task.taskId,
|
|
487
|
-
callId: task.callId,
|
|
488
|
-
};
|
|
489
|
-
task.events.push(event);
|
|
490
|
-
await this.audit.markCancelled(task.requestId, {
|
|
491
|
-
reason: "delegation_cancelled",
|
|
492
|
-
now: this.now,
|
|
493
|
-
});
|
|
494
|
-
yield event;
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
function mapProviderEvent(
|
|
500
|
-
taskId: string,
|
|
501
|
-
event: import("./providers/adapter.js").ProviderEvent,
|
|
502
|
-
): DelegationEvent | undefined {
|
|
503
|
-
switch (event.type) {
|
|
504
|
-
case "start":
|
|
505
|
-
return {
|
|
506
|
-
type: "start",
|
|
507
|
-
taskId,
|
|
508
|
-
callId: event.callId,
|
|
509
|
-
model: event.model,
|
|
510
|
-
};
|
|
511
|
-
case "text-delta":
|
|
512
|
-
return {
|
|
513
|
-
type: "text-delta",
|
|
514
|
-
taskId,
|
|
515
|
-
callId: event.callId,
|
|
516
|
-
text: event.text,
|
|
517
|
-
};
|
|
518
|
-
case "usage":
|
|
519
|
-
return {
|
|
520
|
-
type: "usage",
|
|
521
|
-
taskId,
|
|
522
|
-
callId: event.callId,
|
|
523
|
-
inputTokens: event.inputTokens,
|
|
524
|
-
outputTokens: event.outputTokens,
|
|
525
|
-
totalTokens: event.totalTokens,
|
|
526
|
-
};
|
|
527
|
-
case "done":
|
|
528
|
-
return {
|
|
529
|
-
type: "done",
|
|
530
|
-
taskId,
|
|
531
|
-
callId: event.callId,
|
|
532
|
-
finishReason: event.finishReason,
|
|
533
|
-
};
|
|
534
|
-
case "error":
|
|
535
|
-
return {
|
|
536
|
-
type: "error",
|
|
537
|
-
taskId,
|
|
538
|
-
callId: event.callId,
|
|
539
|
-
message: event.error.message,
|
|
540
|
-
code: event.error.code,
|
|
541
|
-
};
|
|
542
|
-
case "tool-call":
|
|
543
|
-
return undefined;
|
|
544
|
-
default:
|
|
545
|
-
return undefined;
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
function statusOf(task: DelegationTask): DelegationTaskStatus {
|
|
550
|
-
return task.status;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
function applyRouteOverrideDenial(
|
|
554
|
-
request: DelegationRequest,
|
|
555
|
-
decision: RouteDecision,
|
|
556
|
-
): RouteDecision {
|
|
557
|
-
if (!decision.route) return decision;
|
|
558
|
-
|
|
559
|
-
const denials: PolicyDenial[] = [];
|
|
560
|
-
if (
|
|
561
|
-
request.providerId !== undefined &&
|
|
562
|
-
request.providerId !== decision.route.provider
|
|
563
|
-
) {
|
|
564
|
-
denials.push({
|
|
565
|
-
code: "provider_denied",
|
|
566
|
-
target: "provider",
|
|
567
|
-
id: request.providerId,
|
|
568
|
-
message: `Requested provider ${request.providerId} does not match policy-selected provider ${decision.route.provider}.`,
|
|
569
|
-
});
|
|
570
|
-
}
|
|
571
|
-
if (request.model !== undefined && request.model !== decision.route.model) {
|
|
572
|
-
denials.push({
|
|
573
|
-
code: "provider_denied",
|
|
574
|
-
target: "model",
|
|
575
|
-
id: request.model,
|
|
576
|
-
message: `Requested model ${request.model} does not match policy-selected model ${decision.route.model}.`,
|
|
577
|
-
});
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
if (denials.length === 0) return decision;
|
|
581
|
-
|
|
582
|
-
return {
|
|
583
|
-
...decision,
|
|
584
|
-
route: null,
|
|
585
|
-
reason: "provider_override_denied",
|
|
586
|
-
explanation: denials.map((d) => d.message).join(" "),
|
|
587
|
-
policyDenials: [...(decision.policyDenials ?? []), ...denials],
|
|
588
|
-
blocked: true,
|
|
589
|
-
needsConfiguration: false,
|
|
590
|
-
confidence: 1,
|
|
591
|
-
auditSummary: `Blocked provider override profile=${request.route.harness.profile} digest=${request.route.task.promptDigest}`,
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
function cloneTask(task: DelegationTask): DelegationTask {
|
|
596
|
-
return {
|
|
597
|
-
...task,
|
|
598
|
-
events: [...task.events],
|
|
599
|
-
decision: task.decision ? { ...task.decision } : undefined,
|
|
600
|
-
};
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
function sleep(ms: number): Promise<void> {
|
|
604
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
605
|
-
}
|