@frockbot/plugin-shell 0.0.0 → 0.1.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/frockbot.json +68 -0
- package/package.json +87 -6
- package/src/agent.test.ts +372 -0
- package/src/agent.ts +335 -0
- package/src/approvals.test.ts +224 -0
- package/src/approvals.ts +530 -0
- package/src/backend-assignment.test.ts +161 -0
- package/src/backend-assignment.ts +274 -0
- package/src/backend-authoring.test.ts +518 -0
- package/src/backend-authoring.ts +531 -0
- package/src/backend-bot-identity.test.ts +215 -0
- package/src/backend-completion.test.ts +289 -0
- package/src/backend-completion.ts +95 -0
- package/src/backend-composition.ts +242 -0
- package/src/backend-computer.ts +76 -0
- package/src/backend-configuration.test.ts +1757 -0
- package/src/backend-contracts.test.ts +189 -0
- package/src/backend-contracts.ts +44 -0
- package/src/backend-debug.test.ts +202 -0
- package/src/backend-execution.ts +55 -0
- package/src/backend-flock.ts +96 -0
- package/src/backend-image.test.ts +115 -0
- package/src/backend-image.ts +180 -0
- package/src/backend-isolate.test.ts +238 -0
- package/src/backend-isolate.ts +409 -0
- package/src/backend-machine.ts +144 -0
- package/src/backend-memory.ts +89 -0
- package/src/backend-recovery-integration.test.ts +1575 -0
- package/src/backend-recovery.ts +106 -0
- package/src/backend-routines.ts +375 -0
- package/src/backend-runner.ts +251 -0
- package/src/backend-skills.test.ts +126 -0
- package/src/backend-skills.ts +198 -0
- package/src/backend-stop.test.ts +356 -0
- package/src/backend-subagents.ts +459 -0
- package/src/backend.ts +6035 -0
- package/src/client/FrockBotApp.vue +1026 -0
- package/src/client/SendPayloadView.vue +337 -0
- package/src/client/composer-draft.test.ts +31 -0
- package/src/client/composer-draft.ts +35 -0
- package/src/client/cordis-client-shim.d.ts +15 -0
- package/src/client/index.test.ts +2548 -0
- package/src/client/index.ts +2346 -0
- package/src/client/model-presentation.test.ts +35 -0
- package/src/client/model-presentation.ts +19 -0
- package/src/client/notify.test.ts +89 -0
- package/src/client/notify.ts +101 -0
- package/src/client/skill-invocation.test.ts +143 -0
- package/src/client/skill-invocation.ts +175 -0
- package/src/client/styles.css +1043 -0
- package/src/composition-views.ts +118 -0
- package/src/debug-protocol.test.ts +80 -0
- package/src/debug-protocol.ts +165 -0
- package/src/env.d.ts +10 -0
- package/src/history.test.ts +163 -0
- package/src/history.ts +108 -0
- package/src/host.ts +20 -0
- package/src/index.ts +2 -0
- package/src/manifest.ts +3 -0
- package/src/run-cursor.ts +28 -0
- package/src/run-protocol.test.ts +1281 -0
- package/src/run-protocol.ts +1417 -0
- package/src/settings-links.test.ts +106 -0
- package/src/settings-links.ts +289 -0
- package/src/shared.ts +338 -0
- package/src/skill-protocol.ts +117 -0
- package/src/terminal-records.test.ts +217 -0
- package/src/terminal-records.ts +150 -0
- package/src/unread.test.ts +362 -0
- package/src/unread.ts +675 -0
- package/tsconfig.json +18 -0
- package/vite.config.ts +32 -0
- package/README.md +0 -3
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// The Bot Durable Object's half of the image-generation seam.
|
|
2
|
+
//
|
|
3
|
+
// The Image Package consumes a narrow `ImageModelV1` — one method, answering
|
|
4
|
+
// raw image bytes — and knows nothing about Workers AI. This module is the
|
|
5
|
+
// adapter: it takes the `AI` binding off the Durable Object's environment,
|
|
6
|
+
// normalizes the three shapes a Workers AI text-to-image model answers in, and
|
|
7
|
+
// hands the Package a seam with no platform vocabulary in it. "Electron,
|
|
8
|
+
// Cloudflare, provider SDK, and Computer implementation types remain inside
|
|
9
|
+
// their adapters."
|
|
10
|
+
//
|
|
11
|
+
// The binding is optional, exactly as `PACKAGE_BUNDLER` is. A deployment with
|
|
12
|
+
// no Workers AI binding still mounts the Package, and `generate_image` then
|
|
13
|
+
// refuses visibly on the Turn that calls it — which is a better answer than a
|
|
14
|
+
// tool that silently vanishes from the catalog, and a far better one than a
|
|
15
|
+
// `TypeError` inside the Agent loop.
|
|
16
|
+
//
|
|
17
|
+
// HIBERNATION. Nothing here reaches the Computer. The image lands in object
|
|
18
|
+
// storage through `WORKSPACE_FILES`, with its generation recorded in this
|
|
19
|
+
// Durable Object, so `generate_image` works with the Computer hibernated.
|
|
20
|
+
import type { WorkspaceFilesV1 } from "@frockbot/kernel-contracts";
|
|
21
|
+
import type { ImageRuntimeHostV1 } from "@frockbot/plugin-image/agent";
|
|
22
|
+
import type {
|
|
23
|
+
ImageModelInputV1,
|
|
24
|
+
ImageModelV1,
|
|
25
|
+
} from "@frockbot/plugin-image/model";
|
|
26
|
+
|
|
27
|
+
/** The Bot and User a generated image is attributed to. */
|
|
28
|
+
export interface BotImageIdentity {
|
|
29
|
+
userId: string;
|
|
30
|
+
botId: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The run, Turn and Session a generated image records as its provenance. */
|
|
34
|
+
export interface BotImageTurn {
|
|
35
|
+
runId: string;
|
|
36
|
+
turnId: string;
|
|
37
|
+
sessionId: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The Workers AI binding, declared structurally so this module names no
|
|
42
|
+
* Cloudflare type it does not have to. `run` is the whole of what an image
|
|
43
|
+
* model needs from it.
|
|
44
|
+
*/
|
|
45
|
+
export interface WorkersAiBindingV1 {
|
|
46
|
+
run(model: string, input: Record<string, unknown>): Promise<unknown>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The narrow slice of the Durable Object environment this module reads. Named
|
|
51
|
+
* as its own type so the binding's absence is a typed state, not a cast.
|
|
52
|
+
*/
|
|
53
|
+
export interface BotImageEnv {
|
|
54
|
+
AI?: WorkersAiBindingV1;
|
|
55
|
+
WORKSPACE_FILES?: WorkspaceFilesV1;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function decodeBase64(text: string): Uint8Array {
|
|
59
|
+
const binary = atob(text);
|
|
60
|
+
const bytes = new Uint8Array(binary.length);
|
|
61
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
62
|
+
bytes[index] = binary.charCodeAt(index);
|
|
63
|
+
}
|
|
64
|
+
return bytes;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function readStream(
|
|
68
|
+
stream: ReadableStream<Uint8Array>,
|
|
69
|
+
): Promise<ArrayBuffer> {
|
|
70
|
+
const chunks: Uint8Array[] = [];
|
|
71
|
+
let total = 0;
|
|
72
|
+
const reader = stream.getReader();
|
|
73
|
+
for (;;) {
|
|
74
|
+
const { done, value } = await reader.read();
|
|
75
|
+
if (done) break;
|
|
76
|
+
if (value) {
|
|
77
|
+
chunks.push(value);
|
|
78
|
+
total += value.byteLength;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const joined = new Uint8Array(total);
|
|
82
|
+
let offset = 0;
|
|
83
|
+
for (const chunk of chunks) {
|
|
84
|
+
joined.set(chunk, offset);
|
|
85
|
+
offset += chunk.byteLength;
|
|
86
|
+
}
|
|
87
|
+
return joined.buffer as ArrayBuffer;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The bytes inside whatever a Workers AI text-to-image model answered.
|
|
92
|
+
*
|
|
93
|
+
* The catalog does not agree with itself: `flux-1-schnell` and the FLUX.2
|
|
94
|
+
* models answer `{ image: "<base64>" }`, the Stable Diffusion models answer a
|
|
95
|
+
* binary `ReadableStream`, and the binding may hand back an `ArrayBuffer`
|
|
96
|
+
* directly. Every one of those is an image; none of them is the Package's
|
|
97
|
+
* problem.
|
|
98
|
+
*/
|
|
99
|
+
export async function decodeWorkersAiImageV1(
|
|
100
|
+
answer: unknown,
|
|
101
|
+
): Promise<ArrayBuffer> {
|
|
102
|
+
if (answer instanceof ArrayBuffer) return answer;
|
|
103
|
+
if (ArrayBuffer.isView(answer)) {
|
|
104
|
+
const view = answer as ArrayBufferView;
|
|
105
|
+
return view.buffer.slice(
|
|
106
|
+
view.byteOffset,
|
|
107
|
+
view.byteOffset + view.byteLength,
|
|
108
|
+
) as ArrayBuffer;
|
|
109
|
+
}
|
|
110
|
+
if (answer instanceof ReadableStream) {
|
|
111
|
+
return await readStream(answer as ReadableStream<Uint8Array>);
|
|
112
|
+
}
|
|
113
|
+
if (answer instanceof Response) {
|
|
114
|
+
return await answer.arrayBuffer();
|
|
115
|
+
}
|
|
116
|
+
if (answer && typeof answer === "object") {
|
|
117
|
+
const image = (answer as { image?: unknown }).image;
|
|
118
|
+
if (typeof image === "string" && image.length > 0) {
|
|
119
|
+
return decodeBase64(image).buffer as ArrayBuffer;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
throw new Error("the image model answered something that is not an image");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The Workers AI binding as an {@link ImageModelV1}.
|
|
127
|
+
*
|
|
128
|
+
* `width` and `height` are passed through: the Stable Diffusion models accept
|
|
129
|
+
* them, and the FLUX models ignore unknown fields rather than refusing. The
|
|
130
|
+
* Package never trusts them to have been honoured — it reads the dimensions
|
|
131
|
+
* back out of the bytes.
|
|
132
|
+
*/
|
|
133
|
+
export function createWorkersAiImageModelV1(
|
|
134
|
+
binding: WorkersAiBindingV1,
|
|
135
|
+
): ImageModelV1 {
|
|
136
|
+
return {
|
|
137
|
+
async run(model: string, input: ImageModelInputV1): Promise<ArrayBuffer> {
|
|
138
|
+
const answer = await binding.run(model, {
|
|
139
|
+
prompt: input.prompt,
|
|
140
|
+
width: input.width,
|
|
141
|
+
height: input.height,
|
|
142
|
+
});
|
|
143
|
+
return await decodeWorkersAiImageV1(answer);
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The image-generation seam one admitted Turn runs under, or `undefined` when
|
|
150
|
+
* the Bot's Workspace file surface is unavailable.
|
|
151
|
+
*
|
|
152
|
+
* The Workspace is the hard requirement, not the model: an image with nowhere
|
|
153
|
+
* durable to land is not a capability, while a missing model binding is a
|
|
154
|
+
* refusal the Bot can read and report. So a host with a Workspace and no `AI`
|
|
155
|
+
* still mounts the Package, with `model` absent.
|
|
156
|
+
*/
|
|
157
|
+
export function createBotImageHost(
|
|
158
|
+
identity: BotImageIdentity,
|
|
159
|
+
turn: BotImageTurn,
|
|
160
|
+
env: object,
|
|
161
|
+
modelId?: string,
|
|
162
|
+
): ImageRuntimeHostV1 | undefined {
|
|
163
|
+
// SAFETY: the Workspace file surface is constructed onto the Durable Object
|
|
164
|
+
// environment rather than declared in the generated `Env`, because it is not
|
|
165
|
+
// a Worker binding. Absence is a supported state, not an error.
|
|
166
|
+
const bindings = env as BotImageEnv;
|
|
167
|
+
const files = bindings.WORKSPACE_FILES;
|
|
168
|
+
if (!files) return undefined;
|
|
169
|
+
return {
|
|
170
|
+
owner: { userId: identity.userId, botId: identity.botId },
|
|
171
|
+
writer: {
|
|
172
|
+
sessionId: turn.sessionId,
|
|
173
|
+
turnId: turn.turnId,
|
|
174
|
+
runId: turn.runId,
|
|
175
|
+
},
|
|
176
|
+
files,
|
|
177
|
+
...(bindings.AI ? { model: createWorkersAiImageModelV1(bindings.AI) } : {}),
|
|
178
|
+
...(modelId ? { modelId } : {}),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type {
|
|
3
|
+
LlmStreamEvent,
|
|
4
|
+
NormalizedModelRequest,
|
|
5
|
+
} from "@frockbot/kernel-contracts";
|
|
6
|
+
import {
|
|
7
|
+
createIsolateCapabilityHost,
|
|
8
|
+
isolateModelEventStreamV1,
|
|
9
|
+
ISOLATE_MODEL_FAILURE_MESSAGE,
|
|
10
|
+
ISOLATE_MODEL_REQUEST_PREFIX,
|
|
11
|
+
matchingModelAssignmentV1,
|
|
12
|
+
type IsolateAssignmentV1,
|
|
13
|
+
type IsolateModelBindingV1,
|
|
14
|
+
type IsolateModelRequestRecordV1,
|
|
15
|
+
} from "./backend-isolate.ts";
|
|
16
|
+
|
|
17
|
+
const ASSIGNMENT: IsolateAssignmentV1 = {
|
|
18
|
+
assignmentId: "model-assignment",
|
|
19
|
+
packageId: "provider-ollama-cloud",
|
|
20
|
+
capabilityId: "ollama-cloud-models",
|
|
21
|
+
kind: "model",
|
|
22
|
+
connectionId: "connection-1",
|
|
23
|
+
providerModelId: "glm-5.3-flash:cloud",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const BINDING: IsolateModelBindingV1 = {
|
|
27
|
+
assignmentId: "model-assignment",
|
|
28
|
+
packageId: "provider-ollama-cloud",
|
|
29
|
+
capabilityId: "ollama-cloud-models",
|
|
30
|
+
connectionId: "connection-1",
|
|
31
|
+
provider: "ollama-cloud",
|
|
32
|
+
providerModelId: "glm-5.3-flash:cloud",
|
|
33
|
+
connectionGeneration: "generation-1",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function request(
|
|
37
|
+
overrides: Partial<NormalizedModelRequest> = {},
|
|
38
|
+
): NormalizedModelRequest {
|
|
39
|
+
return {
|
|
40
|
+
requestId: "request-1",
|
|
41
|
+
provider: "ollama-cloud",
|
|
42
|
+
model: "glm-5.3-flash:cloud",
|
|
43
|
+
system: "",
|
|
44
|
+
messages: [{ role: "user", content: "hello" }],
|
|
45
|
+
tools: [],
|
|
46
|
+
...overrides,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function memoryStorage() {
|
|
51
|
+
const values = new Map<string, unknown>();
|
|
52
|
+
return {
|
|
53
|
+
values,
|
|
54
|
+
put: (key: string, value: unknown) => {
|
|
55
|
+
values.set(key, structuredClone(value));
|
|
56
|
+
return Promise.resolve();
|
|
57
|
+
},
|
|
58
|
+
get: <T>(key: string) => Promise.resolve(values.get(key) as T | undefined),
|
|
59
|
+
list: <T>(options: { prefix: string }) =>
|
|
60
|
+
Promise.resolve(
|
|
61
|
+
new Map(
|
|
62
|
+
[...values.entries()].filter(([key]) =>
|
|
63
|
+
key.startsWith(options.prefix),
|
|
64
|
+
) as [string, T][],
|
|
65
|
+
),
|
|
66
|
+
),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function host(
|
|
71
|
+
options: {
|
|
72
|
+
binding?: IsolateModelBindingV1;
|
|
73
|
+
stream?: (request: NormalizedModelRequest) => AsyncIterable<LlmStreamEvent>;
|
|
74
|
+
} = {},
|
|
75
|
+
) {
|
|
76
|
+
const storage = memoryStorage();
|
|
77
|
+
const forwarded: NormalizedModelRequest[] = [];
|
|
78
|
+
let minted = 0;
|
|
79
|
+
const binding = options.binding ?? BINDING;
|
|
80
|
+
return {
|
|
81
|
+
storage,
|
|
82
|
+
forwarded,
|
|
83
|
+
host: createIsolateCapabilityHost({
|
|
84
|
+
storage,
|
|
85
|
+
botId: "bot-1",
|
|
86
|
+
packageId: "bot-authored",
|
|
87
|
+
generationId: "generation-1",
|
|
88
|
+
assignments: [ASSIGNMENT],
|
|
89
|
+
modelBinding: binding,
|
|
90
|
+
modelPath: {
|
|
91
|
+
stream: (value) => {
|
|
92
|
+
forwarded.push(structuredClone(value));
|
|
93
|
+
return (
|
|
94
|
+
options.stream?.(value) ??
|
|
95
|
+
(async function* () {
|
|
96
|
+
yield { type: "text-delta", text: "hi" } as LlmStreamEvent;
|
|
97
|
+
})()
|
|
98
|
+
);
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
newId: () => `minted-${(minted += 1)}`,
|
|
102
|
+
now: () => new Date("2026-08-31T00:00:00.000Z"),
|
|
103
|
+
}),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
describe("an isolate model request is bound to its Assignment", () => {
|
|
108
|
+
test("matches only the exact assigned provider and model", () => {
|
|
109
|
+
expect(
|
|
110
|
+
matchingModelAssignmentV1([ASSIGNMENT], BINDING, request()),
|
|
111
|
+
).toMatchObject({ assignmentId: "model-assignment" });
|
|
112
|
+
expect(
|
|
113
|
+
matchingModelAssignmentV1(
|
|
114
|
+
[ASSIGNMENT],
|
|
115
|
+
BINDING,
|
|
116
|
+
request({ provider: "foundation" }),
|
|
117
|
+
),
|
|
118
|
+
).toBeUndefined();
|
|
119
|
+
expect(
|
|
120
|
+
matchingModelAssignmentV1(
|
|
121
|
+
[ASSIGNMENT],
|
|
122
|
+
BINDING,
|
|
123
|
+
request({ model: "some-other-model" }),
|
|
124
|
+
),
|
|
125
|
+
).toBeUndefined();
|
|
126
|
+
// No durable binding at all: an enabled Assignment on its own authorizes
|
|
127
|
+
// nothing.
|
|
128
|
+
expect(
|
|
129
|
+
matchingModelAssignmentV1([ASSIGNMENT], undefined, request()),
|
|
130
|
+
).toBeUndefined();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("ignores a Bot-supplied model binding", () => {
|
|
134
|
+
expect(
|
|
135
|
+
matchingModelAssignmentV1(
|
|
136
|
+
[ASSIGNMENT],
|
|
137
|
+
BINDING,
|
|
138
|
+
request({
|
|
139
|
+
provider: "foundation",
|
|
140
|
+
model: "deterministic-v1",
|
|
141
|
+
modelBinding: { connectionId: "connection-1" },
|
|
142
|
+
}),
|
|
143
|
+
),
|
|
144
|
+
).toBeUndefined();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("a request for an unassigned provider is a pending decision", async () => {
|
|
148
|
+
const subject = host();
|
|
149
|
+
const outcome = await subject.host.invokeModel(
|
|
150
|
+
request({ provider: "foundation", model: "deterministic-v1" }),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
expect(outcome).toMatchObject({ status: "pending-user-decision" });
|
|
154
|
+
expect(await subject.host.recordedModelRequests()).toHaveLength(0);
|
|
155
|
+
expect(subject.forwarded).toHaveLength(0);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("forwards the authority's binding, never the Bot's", async () => {
|
|
159
|
+
const subject = host();
|
|
160
|
+
await subject.host.invokeModel(
|
|
161
|
+
request({
|
|
162
|
+
modelBinding: {
|
|
163
|
+
connectionId: "another-users-connection",
|
|
164
|
+
connectionGeneration: "forged",
|
|
165
|
+
},
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
expect(subject.forwarded[0]?.modelBinding).toEqual({
|
|
170
|
+
connectionId: "connection-1",
|
|
171
|
+
connectionGeneration: "generation-1",
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("the isolate model request record", () => {
|
|
177
|
+
test("two invocations reusing one Bot request id produce two records", async () => {
|
|
178
|
+
const subject = host();
|
|
179
|
+
|
|
180
|
+
await subject.host.invokeModel(request());
|
|
181
|
+
await subject.host.invokeModel(request());
|
|
182
|
+
|
|
183
|
+
const recorded = await subject.host.recordedModelRequests();
|
|
184
|
+
expect(recorded).toHaveLength(2);
|
|
185
|
+
expect(recorded.map((entry) => entry.requestId)).toEqual([
|
|
186
|
+
"request-1",
|
|
187
|
+
"request-1",
|
|
188
|
+
]);
|
|
189
|
+
expect(new Set(recorded.map((entry) => entry.recordId)).size).toBe(2);
|
|
190
|
+
expect(
|
|
191
|
+
[...subject.storage.values.keys()].filter((key) =>
|
|
192
|
+
key.startsWith(ISOLATE_MODEL_REQUEST_PREFIX),
|
|
193
|
+
),
|
|
194
|
+
).toHaveLength(2);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("refuses an unbounded Bot request id", async () => {
|
|
198
|
+
const subject = host();
|
|
199
|
+
await expect(
|
|
200
|
+
subject.host.invokeModel(request({ requestId: "r".repeat(257) })),
|
|
201
|
+
).rejects.toThrow("requestId is not bounded");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("records the request that was forwarded", async () => {
|
|
205
|
+
const subject = host();
|
|
206
|
+
await subject.host.invokeModel(request());
|
|
207
|
+
const recorded = (await subject.host.recordedModelRequests())[0] as
|
|
208
|
+
IsolateModelRequestRecordV1 | undefined;
|
|
209
|
+
expect(recorded?.request.modelBinding).toEqual({
|
|
210
|
+
connectionId: "connection-1",
|
|
211
|
+
connectionGeneration: "generation-1",
|
|
212
|
+
});
|
|
213
|
+
expect(recorded?.capabilityId).toBe("ollama-cloud-models");
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("provider errors crossing into Bot code", () => {
|
|
218
|
+
test("are normalized before the isolate can read them", async () => {
|
|
219
|
+
const stream = isolateModelEventStreamV1(
|
|
220
|
+
(async function* (): AsyncIterable<LlmStreamEvent> {
|
|
221
|
+
yield { type: "text-delta", text: "partial" };
|
|
222
|
+
throw new Error(
|
|
223
|
+
"ollama.com refused key sk-live-123 for account acct-9",
|
|
224
|
+
);
|
|
225
|
+
})(),
|
|
226
|
+
);
|
|
227
|
+
const reader = stream.getReader();
|
|
228
|
+
await reader.read();
|
|
229
|
+
|
|
230
|
+
const failure = await reader.read().then(
|
|
231
|
+
() => undefined,
|
|
232
|
+
(error: unknown) => (error instanceof Error ? error.message : ""),
|
|
233
|
+
);
|
|
234
|
+
expect(failure).toBe(ISOLATE_MODEL_FAILURE_MESSAGE);
|
|
235
|
+
expect(failure).not.toContain("sk-live-123");
|
|
236
|
+
expect(failure).not.toContain("ollama.com");
|
|
237
|
+
});
|
|
238
|
+
});
|