host: expose harness state and actions over the world API
This commit is contained in:
+252
-72
@@ -306,12 +306,12 @@ function lazyStream(model3, setup, observer) {
|
||||
return outer;
|
||||
}
|
||||
function lazyApi(load, capabilities) {
|
||||
const api = {
|
||||
const api2 = {
|
||||
stream: (model3, context, options) => lazyStream(model3, async () => (await load()).stream(model3, context, options)),
|
||||
streamSimple: (model3, context, options) => lazyStream(model3, async () => (await load()).streamSimple(model3, context, options))
|
||||
};
|
||||
if (capabilities?.fetchDeferred) {
|
||||
api.fetchDeferred = (model3, handle, options) => lazyStream(model3, async () => {
|
||||
api2.fetchDeferred = (model3, handle, options) => lazyStream(model3, async () => {
|
||||
const implementation = await load();
|
||||
if (!implementation.fetchDeferred)
|
||||
throw new Error("API does not support deferred responses");
|
||||
@@ -319,14 +319,14 @@ function lazyApi(load, capabilities) {
|
||||
});
|
||||
}
|
||||
if (capabilities?.cancelDeferred) {
|
||||
api.cancelDeferred = async (model3, handle, options) => {
|
||||
api2.cancelDeferred = async (model3, handle, options) => {
|
||||
const implementation = await load();
|
||||
if (!implementation.cancelDeferred)
|
||||
throw new Error("API cannot cancel deferred responses");
|
||||
await implementation.cancelDeferred(model3, handle, options);
|
||||
};
|
||||
}
|
||||
return api;
|
||||
return api2;
|
||||
}
|
||||
var init_lazy = __esm({
|
||||
"node_modules/@earendil-works/pi-ai/dist/api/lazy.js"() {
|
||||
@@ -1400,8 +1400,8 @@ function createProvider(input) {
|
||||
}
|
||||
return provider3;
|
||||
}
|
||||
function hasApi(model3, api) {
|
||||
return isModelType(model3, "chat") && model3.api === api;
|
||||
function hasApi(model3, api2) {
|
||||
return isModelType(model3, "chat") && model3.api === api2;
|
||||
}
|
||||
function calculateCost(model3, usage) {
|
||||
const inputTokens = usage.input + usage.cacheRead + usage.cacheWrite;
|
||||
@@ -33243,32 +33243,32 @@ function memberSchemas(service, implementation) {
|
||||
}
|
||||
function validateImplementation(service, implementation) {
|
||||
const contract = service.contract;
|
||||
const fail = (message) => {
|
||||
const fail2 = (message) => {
|
||||
throw invalid("implementation", `${contract.name}: ${message}`);
|
||||
};
|
||||
if (implementation === null || typeof implementation !== "object")
|
||||
fail("factory must return an object");
|
||||
fail2("factory must return an object");
|
||||
let members;
|
||||
if (contract.scope === "remote") {
|
||||
const variant = implementation.variant;
|
||||
if (typeof variant !== "string" || contract.description.variants[variant] === void 0)
|
||||
fail(`unknown variant ${String(variant)}`);
|
||||
fail2(`unknown variant ${String(variant)}`);
|
||||
members = contract.description.variants[variant];
|
||||
for (const key of Object.keys(implementation))
|
||||
if (key !== "variant" && members[key] === void 0)
|
||||
fail(`${key} is not in variant ${String(variant)}`);
|
||||
fail2(`${key} is not in variant ${String(variant)}`);
|
||||
} else {
|
||||
members = contract.description.variants[LOCAL_VARIANT] ?? {};
|
||||
}
|
||||
for (const [name, description] of Object.entries(members)) {
|
||||
const value2 = implementation[name];
|
||||
if (description.kind === "method" && typeof value2 !== "function")
|
||||
fail(`${name} must be a method`);
|
||||
fail2(`${name} must be a method`);
|
||||
if (description.kind === "state" && !isSource(value2) && !isReadableLike(value2)) {
|
||||
fail(`${name} must be a cell, a derivation or a held object's state member`);
|
||||
fail2(`${name} must be a cell, a derivation or a held object's state member`);
|
||||
}
|
||||
if (description.kind === "opaque" && value2 === void 0)
|
||||
fail(`${name} is missing`);
|
||||
fail2(`${name} is missing`);
|
||||
}
|
||||
}
|
||||
var SelfImpl = class {
|
||||
@@ -35787,9 +35787,9 @@ var noop3 = () => {
|
||||
function deferred() {
|
||||
let resolve2 = noop3;
|
||||
let reject2 = noop3;
|
||||
const promise = new Promise((done, fail) => {
|
||||
const promise = new Promise((done, fail2) => {
|
||||
resolve2 = done;
|
||||
reject2 = fail;
|
||||
reject2 = fail2;
|
||||
});
|
||||
promise.catch(noop3);
|
||||
return { promise, resolve: resolve2, reject: reject2 };
|
||||
@@ -46642,7 +46642,7 @@ async function run(runtime, input, call, tool, args, context) {
|
||||
throw new Error(`Tool call ${call.id} has settled`);
|
||||
};
|
||||
let env;
|
||||
const api = {
|
||||
const api2 = {
|
||||
taskId: runtime.taskId,
|
||||
conversationId: runtime.conversationId,
|
||||
callId: call.id,
|
||||
@@ -46733,7 +46733,7 @@ async function run(runtime, input, call, tool, args, context) {
|
||||
env = await runtime.env(context);
|
||||
const startedAt = performance.now();
|
||||
try {
|
||||
result = await tool.execute(args, api, context);
|
||||
result = await tool.execute(args, api2, context);
|
||||
} finally {
|
||||
durationMs = Math.round(performance.now() - startedAt);
|
||||
}
|
||||
@@ -50743,10 +50743,10 @@ __export(tools_exports, {
|
||||
});
|
||||
|
||||
// node_modules/@earendil-works/pi-durable/dist/tools/env.js
|
||||
function requireEnv(api) {
|
||||
if (api.env === void 0)
|
||||
function requireEnv(api2) {
|
||||
if (api2.env === void 0)
|
||||
throw new Error("No execution environment is configured");
|
||||
return api.env;
|
||||
return api2.env;
|
||||
}
|
||||
|
||||
// node_modules/@earendil-works/pi-durable/dist/tools/bash.js
|
||||
@@ -50775,19 +50775,19 @@ function validateTimeout(timeout) {
|
||||
throw new Error(`Invalid timeout: maximum is ${MAX_TIMEOUT_SECONDS} seconds`);
|
||||
}
|
||||
}
|
||||
async function prepareExecution(command, options, api, context) {
|
||||
async function prepareExecution(command, options, api2, context) {
|
||||
const execution = {
|
||||
command: options?.commandPrefix ? `${options.commandPrefix}
|
||||
${command}` : command,
|
||||
cwd: requireEnv(api).cwd,
|
||||
cwd: requireEnv(api2).cwd,
|
||||
env: {},
|
||||
inheritEnv: true
|
||||
};
|
||||
await options?.prepare?.(execution, api, context);
|
||||
await options?.prepare?.(execution, api2, context);
|
||||
return execution;
|
||||
}
|
||||
async function runCommand(commands, execution, timeout, api, context) {
|
||||
const env = requireEnv(api);
|
||||
async function runCommand(commands, execution, timeout, api2, context) {
|
||||
const env = requireEnv(api2);
|
||||
let result;
|
||||
for (const command of commands) {
|
||||
result = await env.exec(command, {
|
||||
@@ -50795,10 +50795,10 @@ async function runCommand(commands, execution, timeout, api, context) {
|
||||
env: execution.env,
|
||||
inheritEnv: execution.inheritEnv,
|
||||
...timeout === void 0 ? {} : { timeout },
|
||||
onOutput: (text2, _context, info) => api.output(text2, info.skipped),
|
||||
onOutput: (text2, _context, info) => api2.output(text2, info.skipped),
|
||||
spill: { afterBytes: DEFAULT_MAX_BYTES, afterLines: DEFAULT_MAX_LINES },
|
||||
// An environment may then omit output outside the retained tail and report the omission.
|
||||
...api.outputWindow === void 0 ? {} : { window: api.outputWindow }
|
||||
...api2.outputWindow === void 0 ? {} : { window: api2.outputWindow }
|
||||
}, context);
|
||||
if (result.ok || result.error.code !== "spawn_error")
|
||||
break;
|
||||
@@ -50807,7 +50807,7 @@ async function runCommand(commands, execution, timeout, api, context) {
|
||||
throw new Error("No command to run");
|
||||
const spillPath = result.ok ? result.value.spillPath : result.error.spillPath;
|
||||
if (spillPath !== void 0) {
|
||||
api.diagnostic({ severity: "info", code: "full_output", message: `Full output: ${spillPath}` });
|
||||
api2.diagnostic({ severity: "info", code: "full_output", message: `Full output: ${spillPath}` });
|
||||
}
|
||||
if (!result.ok) {
|
||||
if (result.error.code === "aborted" && context.abortSignal?.aborted)
|
||||
@@ -50819,7 +50819,7 @@ async function runCommand(commands, execution, timeout, api, context) {
|
||||
throw result.error;
|
||||
}
|
||||
const exitCode = result.value.exitCode;
|
||||
const retained = api.retainedOutput();
|
||||
const retained = api2.retainedOutput();
|
||||
const structuredOutput = {
|
||||
output: retained.text,
|
||||
truncated: retained.truncated,
|
||||
@@ -50838,10 +50838,10 @@ function createBashTool(options) {
|
||||
parameters: bashSchema,
|
||||
structuredOutputSchema: shellStructuredOutputSchema,
|
||||
outputLimits: { retain: "tail" },
|
||||
async execute(args, api, context) {
|
||||
async execute(args, api2, context) {
|
||||
validateTimeout(args.timeout);
|
||||
const execution = await prepareExecution(args.command, options, api, context);
|
||||
return runCommand([execution.command], execution, args.timeout, api, context);
|
||||
const execution = await prepareExecution(args.command, options, api2, context);
|
||||
return runCommand([execution.command], execution, args.timeout, api2, context);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -50855,13 +50855,13 @@ function createPowerShellTool(options) {
|
||||
parameters: powershellSchema,
|
||||
structuredOutputSchema: shellStructuredOutputSchema,
|
||||
outputLimits: { retain: "tail" },
|
||||
async execute(args, api, context) {
|
||||
async execute(args, api2, context) {
|
||||
validateTimeout(args.timeout);
|
||||
const execution = await prepareExecution(args.command, options, api, context);
|
||||
const execution = await prepareExecution(args.command, options, api2, context);
|
||||
const script = `${UTF8_OUTPUT}
|
||||
${execution.command}`;
|
||||
const commands = programs.map((program) => [program, ...POWERSHELL_ARGS, script]);
|
||||
return runCommand(commands, execution, args.timeout, api, context);
|
||||
return runCommand(commands, execution, args.timeout, api2, context);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -51735,9 +51735,9 @@ function createEditTool() {
|
||||
description: "Edit a single file using exact text replacement. Every edits[].oldText must match a unique, non-overlapping region of the original file. If two changes affect the same block or nearby lines, merge them into one edit instead of emitting overlapping edits. Do not include large unchanged regions just to connect distant changes.",
|
||||
parameters: editSchema,
|
||||
prepareArguments: prepareEditArguments,
|
||||
async execute(args, api, context) {
|
||||
async execute(args, api2, context) {
|
||||
const { path: path3, edits } = validateEditInput(args);
|
||||
const env = requireEnv(api);
|
||||
const env = requireEnv(api2);
|
||||
const absolutePath = await resolveToolPath(env, path3, context);
|
||||
return withFileMutationQueue(env, absolutePath, async () => {
|
||||
if (context.abortSignal?.aborted)
|
||||
@@ -51993,9 +51993,9 @@ function createReadTool(options = {}) {
|
||||
description: `Read the contents of a file. Supports text files and images (${formats2}). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete.`,
|
||||
parameters: readSchema,
|
||||
structuredOutputSchema: readStructuredOutputSchema,
|
||||
async execute(args, api, context) {
|
||||
async execute(args, api2, context) {
|
||||
const { path: path3, offset, limit: limit2 } = args;
|
||||
const env = requireEnv(api);
|
||||
const env = requireEnv(api2);
|
||||
const absolutePath = await resolveReadToolPath(env, path3, context);
|
||||
const reader = getOrThrow(await env.openBinaryReader(absolutePath, void 0, context));
|
||||
try {
|
||||
@@ -52005,7 +52005,7 @@ function createReadTool(options = {}) {
|
||||
size: before.size,
|
||||
read: async (position, length) => getOrThrow(await reader.read(position, length, context))
|
||||
});
|
||||
const result = mimeType === void 0 ? await readText(reader, path3, offset, limit2, context) : await readImage(reader, before, path3, mimeType, options.images, await imageModel(api, context), context);
|
||||
const result = mimeType === void 0 ? await readText(reader, path3, offset, limit2, context) : await readImage(reader, before, path3, mimeType, options.images, await imageModel(api2, context), context);
|
||||
const after = getOrThrow(await reader.info(context));
|
||||
const unchanged = after.size === before.size && after.mtimeMs === before.mtimeMs;
|
||||
if (unchanged || mimeType === void 0 && after.size > before.size)
|
||||
@@ -52060,9 +52060,9 @@ async function readImage(reader, info, path3, mimeType, processor, { vision, lim
|
||||
const block = { type: "image", data: image.data, mimeType: image.mimeType };
|
||||
return { output: [block], structuredOutput: block, diagnostics };
|
||||
}
|
||||
async function imageModel(api, context) {
|
||||
const ref = (await api.agent(context)).model;
|
||||
const model3 = ref === void 0 ? void 0 : api.models.getModel(ref.provider, ref.modelId);
|
||||
async function imageModel(api2, context) {
|
||||
const ref = (await api2.agent(context)).model;
|
||||
const model3 = ref === void 0 ? void 0 : api2.models.getModel(ref.provider, ref.modelId);
|
||||
const resize = model3?.inputLimits?.images?.resize;
|
||||
return {
|
||||
vision: model3?.input.includes("image") ?? true,
|
||||
@@ -52160,9 +52160,9 @@ function createWriteTool() {
|
||||
name: "write",
|
||||
description: "Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.",
|
||||
parameters: writeSchema,
|
||||
async execute(args, api, context) {
|
||||
async execute(args, api2, context) {
|
||||
const { path: path3, content } = args;
|
||||
const env = requireEnv(api);
|
||||
const env = requireEnv(api2);
|
||||
const absolutePath = await resolveToolPath(env, path3, context);
|
||||
return withFileMutationQueue(env, absolutePath, async () => {
|
||||
if (context.abortSignal?.aborted)
|
||||
@@ -52188,7 +52188,7 @@ var CodingTools = createCodingTools();
|
||||
// environment.ts
|
||||
var CodingTools2 = { ...CodingTools, tools: CodingTools.tools.map((tool) => ({
|
||||
...tool,
|
||||
execute: (args, api, context) => tool.execute(args, { ...api, env: api.env.forTool(api.callId, tool.name) }, context)
|
||||
execute: (args, api2, context) => tool.execute(args, { ...api2, env: api2.env.forTool(api2.callId, tool.name) }, context)
|
||||
})) };
|
||||
var Started = defineDoc({
|
||||
kind: "board.started",
|
||||
@@ -53338,13 +53338,24 @@ async function startRequests(harness2, context) {
|
||||
}
|
||||
|
||||
// input.ts
|
||||
async function serve(harness2, context, afterSubmit) {
|
||||
async function serve(harness2, context, afterSubmit, api2) {
|
||||
const pending = /* @__PURE__ */ new Set();
|
||||
const track2 = (work) => {
|
||||
const task = work.catch((error) => Deno.core.ops.op_output(`${String(error)}
|
||||
`)).finally(() => pending.delete(task));
|
||||
pending.add(task);
|
||||
};
|
||||
while (true) {
|
||||
const input = await Deno.core.ops.op_input();
|
||||
if (input === null || input.type === "stop") {
|
||||
await harness2.waitForIdle(context);
|
||||
await Promise.all(pending);
|
||||
return;
|
||||
}
|
||||
if (input.type === "api") {
|
||||
track2(api2.handle(input));
|
||||
continue;
|
||||
}
|
||||
const conversation = await harness2.conversation(input.conversation, context);
|
||||
if (!conversation) throw new Error(`Unknown conversation: ${input.conversation}`);
|
||||
const submission2 = await conversation.submit({
|
||||
@@ -53353,9 +53364,10 @@ async function serve(harness2, context, afterSubmit) {
|
||||
requestId: input.requestId
|
||||
}, context);
|
||||
if (afterSubmit) {
|
||||
const result = await submission2.wait(context);
|
||||
await afterSubmit();
|
||||
if (result.status !== "done") throw new Error(`Run ended: ${JSON.stringify(result)}`);
|
||||
track2(submission2.wait(context).then(async (result) => {
|
||||
await afterSubmit();
|
||||
if (result.status !== "done") throw new Error(`Run ended: ${JSON.stringify(result)}`);
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53615,11 +53627,11 @@ function splitStringByTokenSize(text2, minTokenSize, maxTokenSize) {
|
||||
}
|
||||
return chunks.length > 0 ? chunks : [""];
|
||||
}
|
||||
function cloneMessage(message, api, provider3, modelId) {
|
||||
function cloneMessage(message, api2, provider3, modelId) {
|
||||
const cloned = structuredClone(message);
|
||||
return {
|
||||
...cloned,
|
||||
api,
|
||||
api: api2,
|
||||
provider: provider3,
|
||||
model: modelId,
|
||||
timestamp: cloned.timestamp ?? Date.now(),
|
||||
@@ -53639,11 +53651,11 @@ function createDeferredMessage(model3, handle) {
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
function createErrorMessage(error, api, provider3, modelId) {
|
||||
function createErrorMessage(error, api2, provider3, modelId) {
|
||||
return {
|
||||
role: "assistant",
|
||||
content: [],
|
||||
api,
|
||||
api: api2,
|
||||
provider: provider3,
|
||||
model: modelId,
|
||||
usage: DEFAULT_USAGE,
|
||||
@@ -53752,7 +53764,7 @@ async function streamWithDeltas(stream2, message, minTokenSize, maxTokenSize, to
|
||||
stream2.end(message);
|
||||
}
|
||||
function createFauxCore(options) {
|
||||
const api = options.api ?? randomId(DEFAULT_API);
|
||||
const api2 = options.api ?? randomId(DEFAULT_API);
|
||||
const provider3 = options.provider ?? DEFAULT_PROVIDER;
|
||||
const minTokenSize = Math.max(1, Math.min(options.tokenSize?.min ?? DEFAULT_MIN_TOKEN_SIZE, options.tokenSize?.max ?? DEFAULT_MAX_TOKEN_SIZE));
|
||||
const maxTokenSize = Math.max(minTokenSize, options.tokenSize?.max ?? DEFAULT_MAX_TOKEN_SIZE);
|
||||
@@ -53775,7 +53787,7 @@ function createFauxCore(options) {
|
||||
const models2 = modelDefinitions.map((definition) => ({
|
||||
id: definition.id,
|
||||
name: definition.name ?? definition.id,
|
||||
api,
|
||||
api: api2,
|
||||
provider: provider3,
|
||||
baseUrl: DEFAULT_BASE_URL,
|
||||
reasoning: definition.reasoning ?? false,
|
||||
@@ -53787,7 +53799,7 @@ function createFauxCore(options) {
|
||||
}));
|
||||
const resolveResponse = async (step, context, streamOptions, requestModel) => {
|
||||
const resolved = typeof step === "function" ? await step(context, streamOptions, state2, requestModel) : step;
|
||||
return withUsageEstimate(cloneMessage(resolved, api, provider3, requestModel.id), context, streamOptions, promptCache);
|
||||
return withUsageEstimate(cloneMessage(resolved, api2, provider3, requestModel.id), context, streamOptions, promptCache);
|
||||
};
|
||||
const stream2 = (requestModel, context, streamOptions) => {
|
||||
const outer = createAssistantMessageEventStream();
|
||||
@@ -53797,7 +53809,7 @@ function createFauxCore(options) {
|
||||
try {
|
||||
await streamOptions?.onResponse?.({ status: 200, headers: {} }, requestModel);
|
||||
if (!step) {
|
||||
let message2 = createErrorMessage(new Error("No more faux responses queued"), api, provider3, requestModel.id);
|
||||
let message2 = createErrorMessage(new Error("No more faux responses queued"), api2, provider3, requestModel.id);
|
||||
message2 = withUsageEstimate(message2, context, streamOptions, promptCache);
|
||||
outer.push({ type: "error", reason: "error", error: message2 });
|
||||
outer.end(message2);
|
||||
@@ -53826,7 +53838,7 @@ function createFauxCore(options) {
|
||||
const message = await resolveResponse(step, context, streamOptions, requestModel);
|
||||
await streamWithDeltas(outer, message, minTokenSize, maxTokenSize, tokensPerSecond, streamOptions?.signal);
|
||||
} catch (error) {
|
||||
const message = createErrorMessage(error, api, provider3, requestModel.id);
|
||||
const message = createErrorMessage(error, api2, provider3, requestModel.id);
|
||||
outer.push({ type: "error", reason: "error", error: message });
|
||||
outer.end(message);
|
||||
}
|
||||
@@ -53856,12 +53868,12 @@ function createFauxCore(options) {
|
||||
try {
|
||||
entry2.final = await resolveResponse(entry2.step, entry2.context, submissionOptions, entry2.model);
|
||||
} catch (error) {
|
||||
entry2.final = createErrorMessage(error, api, provider3, entry2.model.id);
|
||||
entry2.final = createErrorMessage(error, api2, provider3, entry2.model.id);
|
||||
}
|
||||
}
|
||||
await streamWithDeltas(outer, entry2.final, minTokenSize, maxTokenSize, tokensPerSecond, fetchOptions?.signal);
|
||||
} catch (error) {
|
||||
const message = createErrorMessage(error, api, provider3, requestModel.id);
|
||||
const message = createErrorMessage(error, api2, provider3, requestModel.id);
|
||||
outer.push({ type: "error", reason: "error", error: message });
|
||||
outer.end(message);
|
||||
}
|
||||
@@ -53882,7 +53894,7 @@ function createFauxCore(options) {
|
||||
return models2.find((candidate) => candidate.id === requestedModelId);
|
||||
}
|
||||
return {
|
||||
api,
|
||||
api: api2,
|
||||
provider: provider3,
|
||||
models: models2,
|
||||
stream: stream2,
|
||||
@@ -54245,14 +54257,14 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
|
||||
}], close = [async () => {
|
||||
opened = false;
|
||||
}];
|
||||
const handle = (api) => new Proxy({}, { get: (_, key) => {
|
||||
const handle = (api2) => new Proxy({}, { get: (_, key) => {
|
||||
if (typeof key === "symbol" || key === "then") return void 0;
|
||||
const value2 = providers.get(api.name);
|
||||
if (!value2) throw new Error(`No provider for ${api.name}`);
|
||||
const value2 = providers.get(api2.name);
|
||||
if (!value2) throw new Error(`No provider for ${api2.name}`);
|
||||
const member = value2[key];
|
||||
return typeof member === "function" ? (...args) => {
|
||||
const current = providers.get(api.name);
|
||||
if (!current) throw new Error(`No provider for ${api.name}`);
|
||||
const current = providers.get(api2.name);
|
||||
if (!current) throw new Error(`No provider for ${api2.name}`);
|
||||
return Reflect.apply(current[key], current, args);
|
||||
} : member;
|
||||
} });
|
||||
@@ -54266,11 +54278,11 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
|
||||
exec: (command, options) => exec(command, { ...options, extension: name }),
|
||||
git,
|
||||
chain: chain2,
|
||||
implement: (api, value2) => {
|
||||
staged.set(api.name, value2);
|
||||
implement: (api2, value2) => {
|
||||
staged.set(api2.name, value2);
|
||||
},
|
||||
need: handle,
|
||||
want: (api) => providers.has(api.name) ? handle(api) : void 0,
|
||||
want: (api2) => providers.has(api2.name) ? handle(api2) : void 0,
|
||||
resource: (key, create) => {
|
||||
const id = `${name}/${key}`;
|
||||
if (!resources.has(id)) resources.set(id, create());
|
||||
@@ -54341,6 +54353,7 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
|
||||
for (const { name, module } of modules) {
|
||||
const facet = scope(name, staged);
|
||||
next.push(facet);
|
||||
facet.actions = Object.fromEntries((module.userActions ?? []).filter((key) => typeof module.actions?.[key] === "function").map((key) => [key, module.actions[key]]));
|
||||
facet.extension = typeof module.default === "function" ? await module.default(facet.host) : module.default;
|
||||
if (!facet.extension || facet.extension.name !== name) throw new Error(`Extension name must match ${name}`);
|
||||
}
|
||||
@@ -54406,7 +54419,12 @@ Last good: ${fallback}` : failed;
|
||||
if (active) await enqueue2();
|
||||
}
|
||||
})();
|
||||
return { reload: enqueue2, state: () => ({ ...state2 }), close: async () => {
|
||||
return { reload: enqueue2, state: () => ({ ...state2 }), async invoke(name, key, args, signal, conversationId) {
|
||||
const facet = facets.find((f) => f.extension.name === name);
|
||||
const action = facet?.actions?.[key];
|
||||
if (!action) throw Object.assign(new Error("Unknown user action"), { status: 404 });
|
||||
return action(args, { host: facet.host, signal, conversationId, source: "ui" });
|
||||
}, close: async () => {
|
||||
active = false;
|
||||
Deno.core.close(rid);
|
||||
await pending;
|
||||
@@ -54415,6 +54433,166 @@ Last good: ${fallback}` : failed;
|
||||
} };
|
||||
}
|
||||
|
||||
// harness-api.ts
|
||||
var Concluded = defineDoc({ kind: "conversations.concluded", version: 1, scope: "session", checkpointWhen: () => false, initial: () => ({ ids: [] }) });
|
||||
var Started2 = defineDoc({ kind: "board.started", version: 1, scope: "conversation", history: "latest", fork: "initial", checkpointWhen: () => false, initial: () => ({}) });
|
||||
var fail = (status, message) => {
|
||||
throw Object.assign(new Error(message), { status });
|
||||
};
|
||||
var integer = (value2, fallback) => {
|
||||
if (value2 === null && fallback !== void 0) return fallback;
|
||||
const n = Number(value2);
|
||||
if (!Number.isSafeInteger(n) || n < 0) fail(400, "Invalid cursor or conversation");
|
||||
return n;
|
||||
};
|
||||
function harnessApi(harness2, models2, extensions2, context) {
|
||||
const streams = /* @__PURE__ */ new Map();
|
||||
const log = eventLog(() => harness2).eventLog;
|
||||
const send = (id, value2) => Deno.core.ops.op_harness_reply(id, value2);
|
||||
async function entries2(conversation, after) {
|
||||
const found = [];
|
||||
let cursor;
|
||||
do {
|
||||
const page3 = await conversation.entries({ minEntryId: after + 1, order: "ascending" }, 256, cursor, context);
|
||||
found.push(...page3.items);
|
||||
cursor = page3.next;
|
||||
} while (cursor);
|
||||
return found;
|
||||
}
|
||||
async function follow(input, read2, cursor) {
|
||||
let active = true, queue2 = Promise.resolve();
|
||||
const scan2 = () => queue2 = queue2.then(async () => {
|
||||
if (!active) return;
|
||||
for (; ; ) {
|
||||
const page3 = await read2(cursor);
|
||||
if (!active || !page3.length) return;
|
||||
for (const entry2 of page3) {
|
||||
cursor = entry2.id;
|
||||
send(input.id, entry2);
|
||||
}
|
||||
}
|
||||
});
|
||||
const observation2 = harness2.observeCommits({ advance: (publication) => {
|
||||
if (publication.changes.some((change) => change.type === "entry")) void scan2();
|
||||
}, end: () => {
|
||||
void stop2();
|
||||
} });
|
||||
const stop2 = async () => {
|
||||
if (!active) return;
|
||||
active = false;
|
||||
observation2.detach();
|
||||
await queue2;
|
||||
send(input.id, null);
|
||||
streams.delete(input.id);
|
||||
};
|
||||
streams.set(input.id, stop2);
|
||||
send(input.id, { status: 200, stream: true });
|
||||
void Deno.core.ops.op_harness_closed(input.id).then(stop2);
|
||||
await scan2();
|
||||
}
|
||||
async function handle(input) {
|
||||
const url = new URL(input.path, "http://host.sausage");
|
||||
const path3 = url.pathname.slice("/harness".length);
|
||||
const data = input.body ?? {};
|
||||
const reply = (status, body) => {
|
||||
send(input.id, { status, body });
|
||||
send(input.id, null);
|
||||
};
|
||||
try {
|
||||
if (input.method === "GET" && path3 === "/conversations") {
|
||||
const records = await harness2.commit(async (tx) => {
|
||||
const all = [];
|
||||
let cursor;
|
||||
do {
|
||||
const page3 = await tx.scanConversations({}, 256, cursor);
|
||||
all.push(...page3.items);
|
||||
cursor = page3.next;
|
||||
} while (cursor);
|
||||
return all;
|
||||
}, context);
|
||||
const concluded = new Set((await harness2.snapshot(Concluded, context))?.ids ?? []);
|
||||
const conversations = await Promise.all(records.map(async (record2) => {
|
||||
const agent = await harness2.snapshot(AgentDoc, record2.id, context);
|
||||
const metadata = await harness2.snapshot(Metadata, record2.id, context);
|
||||
const live = await harness2.snapshot(LiveDoc, record2.id, context);
|
||||
const started = await harness2.snapshot(Started2, record2.id, context);
|
||||
return { ...record2, title: metadata?.title ?? "", agent, starter: started?.by ?? null, concluded: concluded.has(record2.id), running: !!live?.run };
|
||||
}));
|
||||
reply(200, { conversations });
|
||||
return;
|
||||
}
|
||||
if (input.method === "GET" && path3 === "/events") {
|
||||
const cursor = integer(url.searchParams.get("after"), 0);
|
||||
await follow(input, async (after2) => log.recent({ since: after2, order: "ascending", limit: 256 }), cursor);
|
||||
return;
|
||||
}
|
||||
if (input.method === "GET" && path3 === "/extensions") {
|
||||
reply(200, extensions2.state());
|
||||
return;
|
||||
}
|
||||
if (input.method === "GET" && path3 === "/families") {
|
||||
let families = {};
|
||||
try {
|
||||
families = JSON.parse(await fs.read("/src/sausage/agents/models.json"));
|
||||
} catch (error) {
|
||||
if (error.code !== "not_found") throw error;
|
||||
}
|
||||
reply(200, { families });
|
||||
return;
|
||||
}
|
||||
if (input.method === "GET" && path3 === "/models") {
|
||||
reply(200, { models: models2.getAllModels().map(({ id, name, provider: provider3, contextWindow, maxTokens, reasoning, cost }) => ({ id, name, provider: provider3, contextWindow, maxTokens, reasoning, cost })) });
|
||||
return;
|
||||
}
|
||||
const action = /^\/extensions\/([^/]+)\/actions\/([^/]+)$/.exec(path3);
|
||||
if (input.method === "POST" && action) {
|
||||
if (data.conversationId !== void 0 && (!Number.isSafeInteger(data.conversationId) || data.conversationId < 1)) fail(400, "Invalid conversation id");
|
||||
const controller = new AbortController();
|
||||
void Deno.core.ops.op_harness_closed(input.id).then(() => controller.abort());
|
||||
reply(200, { result: await extensions2.invoke(decodeURIComponent(action[1]), decodeURIComponent(action[2]), data.args ?? {}, controller.signal, data.conversationId) });
|
||||
return;
|
||||
}
|
||||
const match = /^\/conversations\/(\d+)\/(entries|stream|submit|stop)$/.exec(path3);
|
||||
if (!match) fail(404, "Unknown harness route");
|
||||
const conversation = await harness2.conversation(integer(match[1]), context);
|
||||
if (!conversation) fail(404, "Unknown conversation");
|
||||
const after = integer(url.searchParams.get("after"), 0);
|
||||
if (input.method === "GET" && match[2] === "entries") {
|
||||
reply(200, { entries: await entries2(conversation, after) });
|
||||
return;
|
||||
}
|
||||
if (input.method === "GET" && match[2] === "stream") {
|
||||
await follow(input, (cursor) => entries2(conversation, cursor), after);
|
||||
return;
|
||||
}
|
||||
if (input.method === "POST" && match[2] === "submit") {
|
||||
if (typeof data.requestId !== "string" || !data.requestId || typeof data.content !== "string" || !data.content.trim()) fail(400, "Expected content and requestId");
|
||||
const submission2 = await conversation.submit({ type: "input", content: data.content, requestId: data.requestId }, context);
|
||||
reply(202, { submissionId: submission2.id });
|
||||
return;
|
||||
}
|
||||
if (input.method === "POST" && match[2] === "stop") {
|
||||
for (; ; ) {
|
||||
const own = (await harness2.inspect(context)).tasks.filter(({ record: record2 }) => record2.conversationId === conversation.id && !record2.owner && !record2.background);
|
||||
if (!own.length) break;
|
||||
for (const { record: record2 } of own) {
|
||||
await harness2.abortTask(record2.id, context);
|
||||
await harness2.waitForTask(record2.id, context);
|
||||
}
|
||||
}
|
||||
reply(200, { ok: true });
|
||||
return;
|
||||
}
|
||||
fail(404, "Unknown harness route");
|
||||
} catch (error) {
|
||||
reply(error.status ?? 500, { error: error.message });
|
||||
}
|
||||
}
|
||||
return { handle, async close() {
|
||||
await Promise.all([...streams.values()].map((stop2) => stop2()));
|
||||
} };
|
||||
}
|
||||
|
||||
// main.ts
|
||||
var proxy = Deno.core.ops.op_model_proxy();
|
||||
if (!proxy?.base_url || !proxy?.api_key) throw new Error("The llm-proxy credential is required");
|
||||
@@ -54490,6 +54668,7 @@ if (globalThis.consoleEnabled) watch.start((event) => {
|
||||
});
|
||||
var stopRequests = await startRequests(harness, BACKGROUND_CONTEXT);
|
||||
var extensions = await loadExtensions(harness, registry, storage, models, BACKGROUND_CONTEXT, [CodingTools2, CredentialRequests]);
|
||||
var api = harnessApi(harness, models, extensions, BACKGROUND_CONTEXT);
|
||||
harness.resume();
|
||||
await serve(harness, BACKGROUND_CONTEXT, globalThis.consoleEnabled ? async () => {
|
||||
await harness.waitForIdle(BACKGROUND_CONTEXT);
|
||||
@@ -54497,7 +54676,8 @@ await serve(harness, BACKGROUND_CONTEXT, globalThis.consoleEnabled ? async () =>
|
||||
render(final.value);
|
||||
await final.stop();
|
||||
Deno.core.ops.op_output("\n");
|
||||
} : void 0);
|
||||
} : void 0, api);
|
||||
await api.close();
|
||||
await extensions.close();
|
||||
await stopRequests();
|
||||
await watch.stop();
|
||||
|
||||
Reference in New Issue
Block a user