feat: add realistic kitchen sink fixture flows

This commit is contained in:
Vincent Koc 2026-04-28 20:08:06 -07:00
parent 87d20d901e
commit d3a89c90ea
No known key found for this signature in database
5 changed files with 425 additions and 49 deletions

View File

@ -22,7 +22,10 @@ The fixture can be used dry, without an LLM:
```text
kitchen image generate a kitchen sink
kitchen image rate limit
kitchen image timeout
kitchen search kitchen sink provider routing
kitchen fetch kitchen://fixture/redirect
kitchen explain the fixture
```
@ -32,18 +35,23 @@ It also exposes provider and tool surfaces for live model routing:
commands, tools, providers, hooks, channel delivery, and tests.
- `kitchen_sink_image_job` returns a deterministic image job, waits 10 seconds
in real runtime execution, then returns the bundled `kitchen_sink_office.png`
image payload.
image payload with PNG dimensions, byte size, SHA-256 hash, seed, model, and
finish metadata.
- `kitchen-sink-image` is a registered image generation provider with aliases
`kitchen`, `kitchen-sink`, and `openclaw-kitchen-sink`.
`kitchen`, `kitchen-sink`, and `openclaw-kitchen-sink`; prompts containing
`rate limit`, `timeout`, or `fail` exercise deterministic provider error
paths.
- `kitchen-sink-media` describes images with deterministic fixture text.
- `kitchen-sink-search` and `kitchen-sink-fetch` provide credential-free web
tool fixtures.
tool fixtures with realistic status codes, request ids, result metadata,
redirects, headers, cache metadata, links, and markdown content.
- `kitchen-sink-channel` is a credential-free channel fixture that can resolve
local accounts, route outbound sessions, and deliver deterministic text/media
records.
local ready/disabled/misconfigured accounts, route outbound sessions, and
deliver deterministic text/media records.
- `kitchen-sink-llm` exposes a deterministic text-provider catalog row,
provider-owned stream function, and prompt guidance so live LLM providers can
discover the Kitchen Sink routes.
discover the Kitchen Sink routes; responses describe which real plugin
surface would handle image, search, fetch, and failure prompts.
- generated hooks classify Kitchen Sink prompts, tool calls, and provider
selections into shared scenario ids such as `image.generate`, `web.search`,
and `text.reply`.

View File

@ -53,6 +53,9 @@ const channel = findRegistration("registerChannel", "kitchen-sink-channel");
const channelAccount = channel.config.resolveAccount({}, "local");
assert.equal(channelAccount.configured, true);
assert.equal(channelAccount.enabled, true);
assert.equal(channelAccount.statusState, "ready");
assert.equal(channelAccount.health.ok, true);
assert.equal(channel.config.resolveAccount({ disabled: true }, "disabled").statusState, "disabled");
const channelDelivery = await channel.outbound.sendText({
cfg: {},
to: "kitchen demo",
@ -60,6 +63,8 @@ const channelDelivery = await channel.outbound.sendText({
});
assert.equal(channelDelivery.channel, "kitchen-sink-channel");
assert.equal(channelDelivery.conversationId, "kitchen-demo");
assert.equal(channelDelivery.deliveryStatus, "sent");
assert.equal(channelDelivery.transport, "kitchen-sink-local");
assert.equal(channelDelivery.meta.scenarioId, "image.generate");
const channelRoute = await channel.messaging.resolveOutboundSessionRoute({
cfg: {},
@ -92,7 +97,7 @@ const imageProvider = findRegistration("registerImageGenerationProvider", "kitch
assert.equal(imageProvider.defaultModel, "kitchen-sink-image-v1");
const sleeps = [];
const { PLUGIN_ID, runKitchenScenario } = await import("../src/scenarios.js");
const { PLUGIN_ID, runKitchenImageTool, runKitchenScenario } = await import("../src/scenarios.js");
const { createKitchenSinkRuntime } = await import("../src/kitchen-runtime.js");
const fastRuntime = createKitchenSinkRuntime({
delayMs: 10_000,
@ -107,10 +112,24 @@ assert.equal(imageResult.scenarioId, "image.generate");
assert.equal(imageResult.route, "provider:image");
assert.equal(imageResult.job.status, "completed");
assert.equal(imageResult.job.pluginId, "openclaw-kitchen-sink-fixture");
assert.equal(imageResult.job.progressPercent, 100);
assert.equal(imageResult.job.statusUrl, `kitchen://jobs/${imageResult.job.id}`);
assert.deepEqual(
imageResult.job.timeline.map((entry) => entry.status),
["queued", "running", "completed"],
);
assert.equal(imageResult.job.output.contentHash, "e126064123bb13d8");
assert.equal(imageResult.image.metadata.pluginId, "openclaw-kitchen-sink-fixture");
assert.equal(imageResult.image.metadata.assetId, "office-lobby-sink");
assert.equal(imageResult.image.metadata.assetName, "kitchen_sink_office.png");
assert.equal(imageResult.image.metadata.source, "bundled-real-image");
assert.equal(imageResult.image.metadata.model, "kitchen-sink-image-v1");
assert.equal(imageResult.image.metadata.width, 1024);
assert.equal(imageResult.image.metadata.height, 1024);
assert.equal(imageResult.image.metadata.sizeBytes, 948291);
assert.equal(imageResult.image.metadata.sha256, "e126064123bb13d8ee01a22c204e079bc22397c103ed1c3a191c60d5ae3319aa");
assert.equal(imageResult.image.metadata.contentHash, "e126064123bb13d8");
assert.equal(imageResult.image.metadata.finishReason, "success");
assert.equal(imageResult.image.mimeType, "image/png");
assert.equal(imageResult.image.fileName, `${imageResult.job.id}.png`);
assert.deepEqual(
@ -119,14 +138,35 @@ assert.deepEqual(
);
assert.ok(imageResult.image.dataUrl.startsWith("data:image/png;base64,"));
const failedImageResult = await fastRuntime.runImageJob({ prompt: "kitchen rate limit image" });
assert.deepEqual(sleeps, [10_000, 10_000]);
assert.equal(failedImageResult.job.status, "failed");
assert.deepEqual(
failedImageResult.job.timeline.map((entry) => entry.status),
["queued", "running", "failed"],
);
assert.equal(failedImageResult.error.code, "rate_limited");
assert.equal(failedImageResult.error.statusCode, 429);
assert.equal(failedImageResult.error.retryAfterMs, 30_000);
const failedToolResult = await runKitchenImageTool(fastRuntime, { prompt: "kitchen timeout image" });
assert.equal(failedToolResult.ok, false);
assert.equal(failedToolResult.error.code, "timeout");
assert.equal(failedToolResult.mediaUrl, undefined);
const scenarioResult = await runKitchenScenario(fastRuntime, {
scenario: "web.fetch",
url: "kitchen://fixture/readme",
url: "kitchen://fixture/redirect",
route: "test:scenario-engine",
});
assert.equal(PLUGIN_ID, "openclaw-kitchen-sink-fixture");
assert.equal(scenarioResult.scenarioId, "web.fetch");
assert.equal(scenarioResult.route, "test:scenario-engine");
assert.equal(scenarioResult.ok, true);
assert.equal(scenarioResult.statusCode, 200);
assert.equal(scenarioResult.finalUrl, "kitchen://fixture/readme");
assert.equal(scenarioResult.redirects.length, 1);
assert.equal(scenarioResult.headers["x-kitchen-sink-fixture"], "true");
assert.match(scenarioResult.content, /deterministic document/);
const mediaProvider = findRegistration("registerMediaUnderstandingProvider", "kitchen-sink-media");
@ -141,11 +181,20 @@ const searchTool = searchProvider.createTool({});
const searchResult = await searchTool.execute({ query: "kitchen sink image provider" });
assert.equal(searchResult.results.length, 3);
assert.equal(searchResult.provider, "kitchen-sink-search");
assert.equal(searchResult.ok, true);
assert.equal(searchResult.statusCode, 200);
assert.equal(searchResult.results[0].id, "ks-result-image-provider");
assert.equal(searchResult.results[0].metadata.provider, "kitchen-sink-image");
const emptySearchResult = await searchTool.execute({ query: "kitchen empty results" });
assert.equal(emptySearchResult.ok, true);
assert.equal(emptySearchResult.results.length, 0);
const textProvider = findRegistration("registerProvider", "kitchen-sink-llm");
const catalog = await textProvider.staticCatalog.run({ config: {}, env: {} });
assert.equal(catalog.provider.models[0].id, "kitchen-sink-text-v1");
assert.equal(catalog.provider.models[0].api, "kitchen-sink");
const authResult = await textProvider.auth[0].run();
assert.equal(authResult.profiles[0].id, "kitchen-sink-local");
const streamFn = textProvider.createStreamFn({});
const stream = streamFn(catalog.provider.models[0], {
messages: [{ role: "user", content: "kitchen explain text inference", timestamp: 0 }],
@ -157,6 +206,7 @@ for await (const event of stream) {
const streamMessage = await stream.result();
assert.deepEqual(streamEvents, ["start", "text_start", "text_delta", "text_end", "done"]);
assert.match(streamMessage.content[0].text, /kitchen explain text inference/);
assert.ok(streamMessage.usage.totalTokens > 0);
const imageTool = findRegistration("registerTool", "kitchen_sink_image_job");
assert.equal(typeof imageTool.execute, "function");

View File

@ -31,6 +31,7 @@ const requiredFiles = [
"openclaw.plugin.json",
"plugin-inspector.config.json",
"src/index.js",
"src/assets/kitchen_sink_office.png",
"src/kitchen-runtime.js",
"src/scenarios.js",
"src/setup.js",

View File

@ -181,10 +181,10 @@ function buildKitchenChannel() {
config: {
listAccountIds: () => [CHANNEL_ACCOUNT_ID],
defaultAccountId: () => CHANNEL_ACCOUNT_ID,
resolveAccount: (_cfg, accountId) => kitchenChannelAccount(accountId || CHANNEL_ACCOUNT_ID),
isEnabled: () => true,
isConfigured: () => true,
describeAccount: (account) => kitchenChannelAccount(account.accountId),
resolveAccount: (cfg, accountId) => kitchenChannelAccount(accountId || CHANNEL_ACCOUNT_ID, cfg),
isEnabled: (cfg) => cfg?.disabled !== true,
isConfigured: (cfg) => cfg?.configured !== false,
describeAccount: (account) => kitchenChannelAccount(account.accountId, account),
resolveDefaultTo: () => "kitchen",
},
status: {
@ -265,17 +265,28 @@ function buildKitchenImageProvider(runtime) {
scenario: "image.generate",
prompt: req?.prompt,
route: "provider:image",
model: req?.model,
});
if (result.error) {
throw kitchenProviderError(result);
}
return {
images: [stripDataUrl(result.image)],
model: req?.model || DEFAULT_IMAGE_MODEL,
metadata: {
kitchenSink: true,
job: result.job,
asset: result.image.metadata,
provider: IMAGE_PROVIDER_ID,
pluginId: PLUGIN_ID,
scenarioId: result.scenarioId,
route: result.route,
request: {
prompt: req?.prompt,
size: req?.size,
aspectRatio: req?.aspectRatio,
count: req?.count || 1,
},
},
};
},
@ -313,7 +324,14 @@ function buildKitchenTextProvider() {
hint: "Deterministic local fixture provider.",
kind: "custom",
run: async () => ({
profiles: [],
profiles: [
{
id: "kitchen-sink-local",
label: "Kitchen Sink Local",
configured: true,
source: "fixture",
},
],
defaultModel: `${TEXT_PROVIDER_ID}/${DEFAULT_TEXT_MODEL}`,
notes: ["Kitchen Sink LLM is deterministic and does not call a network service."],
}),
@ -538,6 +556,24 @@ function pluginConfigPath() {
return `plugins.${PLUGIN_ID}`;
}
function kitchenProviderError(result) {
const error = new Error(result.error.message);
error.name = "KitchenSinkProviderError";
error.code = result.error.code;
error.statusCode = result.error.statusCode;
error.retryable = result.error.retryable;
error.retryAfterMs = result.error.retryAfterMs;
error.metadata = {
kitchenSink: true,
job: result.job,
pluginId: PLUGIN_ID,
provider: IMAGE_PROVIDER_ID,
scenarioId: result.scenarioId,
route: result.route,
};
return error;
}
function optionalRegister(api, method, register) {
if (typeof api?.[method] !== "function") {
return;

View File

@ -1,3 +1,4 @@
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
export const PLUGIN_ID = "openclaw-kitchen-sink-fixture";
@ -16,6 +17,21 @@ const KITCHEN_SINK_OFFICE_IMAGE_FILE = "kitchen_sink_office.png";
const KITCHEN_SINK_OFFICE_IMAGE = readFileSync(
new URL(`./assets/${KITCHEN_SINK_OFFICE_IMAGE_FILE}`, import.meta.url),
);
const KITCHEN_SINK_OFFICE_SHA256 = sha256Hex(KITCHEN_SINK_OFFICE_IMAGE);
const KITCHEN_IMAGE_FIXTURES = [
{
id: "office-lobby-sink",
label: "Kitchen Sink Office",
assetName: KITCHEN_SINK_OFFICE_IMAGE_FILE,
buffer: KITCHEN_SINK_OFFICE_IMAGE,
sha256: KITCHEN_SINK_OFFICE_SHA256,
mimeType: "image/png",
width: 1024,
height: 1024,
description: "office lobby scene with a lobster-costumed figure holding a real sink",
source: "bundled-real-image",
},
];
export function createKitchenScenarioRuntime(options = {}) {
const runtime = {
@ -48,10 +64,43 @@ export async function runKitchenScenario(runtime, request = {}) {
const scenario = normalizeScenario(request.scenario);
if (scenario === "image.generate") {
const prompt = normalizePrompt(request.prompt, "a kitchen sink fixture image");
const job = createKitchenJob("image", prompt, runtime.now(), runtime.delayMs, scenario, request.route);
const queuedJob = createKitchenJob("image", prompt, runtime.now(), runtime.delayMs, scenario, request.route);
const runningJob = transitionKitchenJob(queuedJob, "running", runtime.now(), {
progressPercent: 50,
progressSummary: "Kitchen Sink image provider accepted the request.",
});
await runtime.sleep(runtime.delayMs);
const completedJob = { ...job, status: "completed", completedAt: runtime.now().toISOString() };
const image = createKitchenSinkImageAsset({ prompt, jobId: job.id, scenario });
const failure = classifyKitchenFailure(prompt);
if (failure) {
return {
scenarioId: scenario,
route: request.route || "provider:image",
job: transitionKitchenJob(runningJob, "failed", runtime.now(), {
error: failure,
progressPercent: 100,
progressSummary: failure.message,
}),
error: failure,
};
}
const image = createKitchenSinkImageAsset({
prompt,
jobId: queuedJob.id,
scenario,
model: request.model || DEFAULT_IMAGE_MODEL,
});
const completedAt = runtime.now();
const completedJob = transitionKitchenJob(runningJob, "completed", completedAt, {
completedAt: completedAt.toISOString(),
progressPercent: 100,
progressSummary: `Returned bundled ${image.metadata.assetName}.`,
output: {
fileName: image.fileName,
mimeType: image.mimeType,
sizeBytes: image.metadata.sizeBytes,
contentHash: image.metadata.contentHash,
},
});
return {
scenarioId: scenario,
route: request.route || "provider:image",
@ -88,27 +137,44 @@ export async function runKitchenScenario(runtime, request = {}) {
const prompt = normalizePrompt(request.prompt, "explain the kitchen sink fixture");
const job = createKitchenJob("text", prompt, runtime.now(), 0, scenario, request.route);
const text = kitchenTextResponse(prompt);
const completedAt = runtime.now();
return {
scenarioId: "text.reply",
route: request.route || "provider:text",
job: { ...job, status: "completed", completedAt: runtime.now().toISOString() },
text: kitchenTextResponse(prompt),
job: transitionKitchenJob(job, "completed", completedAt, {
completedAt: completedAt.toISOString(),
progressPercent: 100,
progressSummary: "Kitchen Sink text provider produced a deterministic reply.",
}),
text,
usage: estimateUsage(prompt, text),
};
}
export function createKitchenSinkImageAsset({ prompt, jobId, scenario = "image.generate" }) {
const buffer = Buffer.from(KITCHEN_SINK_OFFICE_IMAGE);
export function createKitchenSinkImageAsset({ prompt, jobId, scenario = "image.generate", model = DEFAULT_IMAGE_MODEL }) {
const fixture = selectKitchenImageFixture(prompt);
const buffer = Buffer.from(fixture.buffer);
const seed = stableHash(`${jobId}:${prompt}:${fixture.id}`);
return {
buffer,
mimeType: "image/png",
mimeType: fixture.mimeType,
fileName: `${jobId}.png`,
dataUrl: `data:image/png;base64,${buffer.toString("base64")}`,
revisedPrompt: `Kitchen sink office fixture image for: ${prompt}`,
dataUrl: `data:${fixture.mimeType};base64,${buffer.toString("base64")}`,
revisedPrompt: `Kitchen Sink office image fixture: ${prompt}`,
metadata: {
kitchenSink: true,
assetName: KITCHEN_SINK_OFFICE_IMAGE_FILE,
width: 1024,
height: 1024,
assetId: fixture.id,
assetName: fixture.assetName,
source: fixture.source,
model,
width: fixture.width,
height: fixture.height,
sizeBytes: buffer.byteLength,
sha256: fixture.sha256,
contentHash: fixture.sha256.slice(0, 16),
seed,
finishReason: "success",
pluginId: PLUGIN_ID,
scenarioId: scenario,
jobId,
@ -126,6 +192,7 @@ export function kitchenPromptGuidance() {
"Kitchen Sink fixture plugin:",
"- Use the kitchen_sink_image_job tool when the user asks for a kitchen sink image without selecting an image provider.",
"- Use provider kitchen-sink-image for image generation when the configured image provider is Kitchen Sink.",
"- Image prompts containing rate limit, timeout, or fail trigger deterministic failure paths for retry/error handling.",
"- Use kitchen_sink_search for deterministic search fixture queries.",
"- Use kitchen_sink_text for deterministic text fixture responses.",
];
@ -140,6 +207,8 @@ export function createKitchenChannelDelivery({ kind = "text", text = "", to = "k
conversationId: normalizedTo,
channelId: normalizedTo,
timestamp: Date.now(),
deliveryStatus: "sent",
transport: "kitchen-sink-local",
meta: {
kitchenSink: true,
pluginId: PLUGIN_ID,
@ -149,17 +218,29 @@ export function createKitchenChannelDelivery({ kind = "text", text = "", to = "k
};
}
export function kitchenChannelAccount(accountId = CHANNEL_ACCOUNT_ID) {
export function kitchenChannelAccount(accountId = CHANNEL_ACCOUNT_ID, config = {}) {
const normalizedAccountId = accountId || CHANNEL_ACCOUNT_ID;
const enabled = normalizedAccountId !== "disabled" && config?.disabled !== true;
const configured = normalizedAccountId !== "missing" && config?.configured !== false;
const ok = enabled && configured;
return {
accountId: accountId || CHANNEL_ACCOUNT_ID,
name: "Kitchen Sink Local",
enabled: true,
configured: true,
statusState: "fixture",
linked: true,
running: true,
connected: true,
accountId: normalizedAccountId,
name: normalizedAccountId === CHANNEL_ACCOUNT_ID ? "Kitchen Sink Local" : `Kitchen Sink ${normalizedAccountId}`,
enabled,
configured,
statusState: ok ? "ready" : enabled ? "needs_setup" : "disabled",
linked: configured,
running: ok,
connected: ok,
mode: "local",
health: {
ok,
checkedAt: "2026-04-28T00:00:00.000Z",
message: ok
? "Kitchen Sink local fixture account is ready."
: "Kitchen Sink local fixture account is intentionally unavailable.",
},
capabilities: ["text", "media", "threads", "dry-run"],
};
}
@ -202,32 +283,88 @@ export async function runKitchenImageTool(runtime, input) {
prompt: readPrompt(input),
route: "tool:kitchen_sink_image_job",
});
if (result.error) {
return {
...result,
ok: false,
};
}
return {
...result,
ok: true,
mediaUrl: result.image.dataUrl,
};
}
export async function runKitchenSearch(query) {
const normalized = normalizePrompt(query, "kitchen sink");
const requestId = `ks_search_${stableHash(normalized).slice(0, 10)}`;
const failure = classifyKitchenFailure(normalized);
if (failure) {
return {
provider: WEB_SEARCH_PROVIDER_ID,
requestId,
query: normalized,
ok: false,
statusCode: failure.statusCode,
latencyMs: 12,
error: failure,
results: [],
};
}
if (/\b(empty|no results|zero)\b/i.test(normalized)) {
return {
provider: WEB_SEARCH_PROVIDER_ID,
requestId,
query: normalized,
ok: true,
statusCode: 200,
latencyMs: 18,
results: [],
answer: "No Kitchen Sink fixture results matched the deterministic empty-result query.",
};
}
return {
provider: WEB_SEARCH_PROVIDER_ID,
requestId,
query: normalized,
ok: true,
statusCode: 200,
latencyMs: 24,
answer: `Kitchen Sink found fixture routes for "${normalized}".`,
results: [
{
id: "ks-result-image-provider",
title: "Kitchen Sink image fixture",
url: "https://github.com/openclaw/kitchen-sink#image-fixture",
displayUrl: "github.com/openclaw/kitchen-sink#image-fixture",
snippet: `Deterministic image job route for "${normalized}".`,
source: "kitchen-sink-docs",
score: 0.98,
faviconUrl: "https://github.githubassets.com/favicons/favicon.svg",
metadata: { route: "provider:image", provider: IMAGE_PROVIDER_ID },
},
{
id: "ks-result-dry-command",
title: "Kitchen Sink dry command route",
url: "https://github.com/openclaw/kitchen-sink#dry-command-route",
displayUrl: "github.com/openclaw/kitchen-sink#dry-command-route",
snippet: "The kitchen prefix works without live LLM credentials.",
source: "kitchen-sink-docs",
score: 0.91,
faviconUrl: "https://github.githubassets.com/favicons/favicon.svg",
metadata: { route: "prefix:kitchen", provider: "command" },
},
{
id: "ks-result-provider-route",
title: "Kitchen Sink provider route",
url: "https://github.com/openclaw/kitchen-sink#provider-route",
displayUrl: "github.com/openclaw/kitchen-sink#provider-route",
snippet: "The image, media, text, fetch, and search providers are registered by the plugin.",
source: "kitchen-sink-docs",
score: 0.87,
faviconUrl: "https://github.githubassets.com/favicons/favicon.svg",
metadata: { route: "provider:*", provider: PLUGIN_ID },
},
],
};
@ -235,25 +372,73 @@ export async function runKitchenSearch(query) {
export async function runKitchenFetch(url) {
const target = normalizePrompt(url, "kitchen://fixture/readme");
const failure = classifyKitchenFailure(target);
const finalUrl = /\bredirect\b/i.test(target) ? "kitchen://fixture/readme" : target;
const missing = /\b(404|missing|not found)\b/i.test(target);
const statusCode = failure?.statusCode || (missing ? 404 : 200);
const ok = statusCode >= 200 && statusCode < 400;
const title = failure
? "Kitchen Sink fixture error"
: missing
? "Kitchen Sink fixture not found"
: "Kitchen Sink fixture document";
const content = ok
? `Kitchen Sink fetched "${finalUrl}". This deterministic document proves plugin web-fetch routing without network access.`
: `Kitchen Sink could not fetch "${target}" in the deterministic fixture corpus.`;
return {
provider: WEB_FETCH_PROVIDER_ID,
requestId: `ks_fetch_${stableHash(target).slice(0, 10)}`,
ok,
statusCode,
url: target,
title: "Kitchen Sink fixture document",
content: `Kitchen Sink fetched "${target}". This deterministic document proves plugin web-fetch routing without network access.`,
finalUrl,
title,
contentType: "text/markdown; charset=utf-8",
headers: {
"cache-control": "max-age=3600",
"content-type": "text/markdown; charset=utf-8",
"x-kitchen-sink-fixture": "true",
},
redirects: finalUrl === target ? [] : [{ statusCode: 302, from: target, to: finalUrl }],
cache: { status: "HIT", maxAgeSeconds: 3600 },
links: [
{ href: "kitchen://fixture/image-provider", text: "Image provider fixture" },
{ href: "kitchen://fixture/search", text: "Search fixture" },
],
markdown: `# ${title}\n\n${content}\n`,
content,
...(ok ? {} : { error: failure || { code: "not_found", message: "Fixture document was not found.", retryable: false } }),
};
}
export function kitchenImageReply(result) {
if (result.error) {
return {
text: `kitchen image job ${result.job.id} failed: ${result.error.message}`,
presentation: {
title: "Kitchen Sink Image Failed",
tone: "danger",
blocks: [
{ type: "text", text: `job: ${result.job.id}` },
{ type: "context", text: `code=${result.error.code} retryable=${String(result.error.retryable)}` },
],
},
channelData: {
kitchenSink: result,
},
};
}
return {
text:
`kitchen image job ${result.job.id} completed after ${Math.round(result.job.delayMs / 1000)}s. ` +
`provider=${IMAGE_PROVIDER_ID} model=${DEFAULT_IMAGE_MODEL}`,
`provider=${IMAGE_PROVIDER_ID} model=${result.image.metadata.model} asset=${result.image.metadata.assetName}`,
mediaUrl: result.image.dataUrl,
presentation: {
title: "Kitchen Sink Image",
tone: "success",
blocks: [
{ type: "text", text: `job: ${result.job.id}` },
{ type: "text", text: `asset: ${result.image.metadata.assetName}` },
{ type: "context", text: result.image.revisedPrompt },
],
},
@ -289,14 +474,15 @@ export function kitchenTextModelDefinition() {
export function createKitchenTextStream(model, context) {
const stream = createAssistantMessageEventStream();
queueMicrotask(() => {
const text = kitchenTextResponse(extractLastUserPrompt(context));
const prompt = extractLastUserPrompt(context);
const text = kitchenTextResponse(prompt);
const message = {
role: "assistant",
content: [{ type: "text", text }],
api: model?.api || "kitchen-sink",
provider: TEXT_PROVIDER_ID,
model: model?.id || DEFAULT_TEXT_MODEL,
usage: zeroUsage(),
usage: estimateUsage(prompt, text),
stopReason: "stop",
timestamp: Date.now(),
};
@ -311,10 +497,32 @@ export function createKitchenTextStream(model, context) {
}
export function kitchenTextResponse(prompt) {
const normalized = normalizePrompt(prompt, "kitchen sink text inference");
if (/\b(image|picture|draw|generate)\b/i.test(normalized)) {
return [
"Kitchen Sink text fixture:",
`prompt="${normalized}"`,
`I would route this to ${IMAGE_PROVIDER_ID}/${DEFAULT_IMAGE_MODEL}, create a queued image job, wait for completion, then return the bundled kitchen_sink_office.png asset with PNG metadata.`,
].join(" ");
}
if (/\b(search|find|lookup|web)\b/i.test(normalized)) {
return [
"Kitchen Sink text fixture:",
`prompt="${normalized}"`,
`I would call ${WEB_SEARCH_PROVIDER_ID} for ranked fixture results and ${WEB_FETCH_PROVIDER_ID} for deterministic document fetches.`,
].join(" ");
}
if (/\b(rate limit|timeout|fail|error)\b/i.test(normalized)) {
return [
"Kitchen Sink text fixture:",
`prompt="${normalized}"`,
"Failure fixtures are available: rate limit returns 429 with retry metadata, timeout returns 504, and fail returns a deterministic provider error.",
].join(" ");
}
return [
"kitchen sink text fixture:",
`prompt="${prompt}"`,
"routes: direct prefix, registered tools, image provider, media understanding, web search, web fetch, and text provider catalog.",
"Kitchen Sink text fixture:",
`prompt="${normalized}"`,
"Available realistic surfaces: direct prefix, registered tools, image provider lifecycle, media understanding, web search, web fetch, channel health, hooks, detached tasks, and text provider catalog.",
].join(" ");
}
@ -352,6 +560,12 @@ export function stripDataUrl(image) {
}
export function renderSearchText(result) {
if (result.error) {
return `Kitchen Sink search failed: ${result.error.message}`;
}
if (result.results.length === 0) {
return result.answer || "Kitchen Sink search returned no results.";
}
return result.results.map((entry, index) => `${index + 1}. ${entry.title} - ${entry.snippet}`).join("\n");
}
@ -408,16 +622,38 @@ export function observeKitchenHook(name, event, context) {
function createKitchenJob(kind, prompt, date, delayMs, scenarioId, route) {
const id = `ks_${kind}_${stableHash(`${kind}:${prompt}`).slice(0, 10)}`;
const createdAt = date.toISOString();
return {
id,
kind,
status: "running",
status: "queued",
prompt,
delayMs,
createdAt: date.toISOString(),
createdAt,
queuedAt: createdAt,
lastEventAt: createdAt,
progressPercent: 0,
progressSummary: "Kitchen Sink job queued.",
pluginId: PLUGIN_ID,
scenarioId,
route: route || defaultRouteForScenario(scenarioId),
statusUrl: `kitchen://jobs/${id}`,
timeline: [{ status: "queued", at: createdAt, summary: "Kitchen Sink job queued." }],
};
}
function transitionKitchenJob(job, status, date, patch = {}) {
const at = date.toISOString();
const summary = patch.progressSummary || `Kitchen Sink job ${status}.`;
return {
...job,
...patch,
status,
startedAt: status === "running" ? at : job.startedAt,
completedAt: status === "completed" ? patch.completedAt || at : job.completedAt,
failedAt: status === "failed" ? at : job.failedAt,
lastEventAt: at,
timeline: [...(job.timeline || []), { status, at, summary }],
};
}
@ -503,13 +739,15 @@ function extractLastUserPrompt(context) {
return "kitchen sink text inference";
}
function zeroUsage() {
function estimateUsage(prompt = "", text = "") {
const input = estimateTokens(prompt);
const output = estimateTokens(text);
return {
input: 0,
output: 0,
input,
output,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
totalTokens: input + output,
cost: {
input: 0,
output: 0,
@ -520,6 +758,49 @@ function zeroUsage() {
};
}
function estimateTokens(text) {
return Math.max(1, Math.ceil(String(text).trim().split(/\s+/).filter(Boolean).length * 1.35));
}
function classifyKitchenFailure(prompt) {
const text = String(prompt ?? "").toLowerCase();
if (/\brate[ -]?limit|429|too many requests\b/.test(text)) {
return {
code: "rate_limited",
statusCode: 429,
message: "Kitchen Sink fixture simulated a provider rate limit.",
retryable: true,
retryAfterMs: 30_000,
};
}
if (/\btimeout|timed out|504\b/.test(text)) {
return {
code: "timeout",
statusCode: 504,
message: "Kitchen Sink fixture simulated an upstream timeout.",
retryable: true,
retryAfterMs: 5_000,
};
}
if (/\bfail|error|500\b/.test(text)) {
return {
code: "fixture_failed",
statusCode: 500,
message: "Kitchen Sink fixture simulated a provider failure.",
retryable: false,
};
}
return undefined;
}
function selectKitchenImageFixture(_prompt) {
return KITCHEN_IMAGE_FIXTURES[0];
}
function sha256Hex(buffer) {
return createHash("sha256").update(buffer).digest("hex");
}
function readString(input, key) {
if (input && typeof input === "object" && typeof input[key] === "string") {
return input[key];