Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 4 additions & 22 deletions apps/hub/src/builds/sidecar-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,12 @@ import { defineTool, type BaseEnv } from "@intx/agent/authoring";
import { HUB_CREDENTIAL_HANDLE } from "@intx/types/credential-delivery";
import { readFile, realpath, stat } from "node:fs/promises";
import { isAbsolute, join, relative, resolve, sep } from "node:path";
import { createPosixTools } from "@intx/tools-posix";
import { posix } from "@intx/tools-posix/sidecar-bundle";
import {
createBlobReader,
type ToolCall,
type ToolResult,
type ToolRunner,
} from "@intx/types/runtime";
import type { ToolCall, ToolResult, ToolRunner } from "@intx/types/runtime";
import type { RuntimeCapabilities } from "@intx/types/runtime-capabilities";
import { resolveWorkflowProjectScope } from "../workflows/project-scope.ts";
import { WorkflowHttpError } from "../workflows/http-json.ts";
import { savedResultTools } from "../workflows/saved-result-tools.ts";
import { APP_STATUS_TOOL, createCodingTools } from "./coding-tools.ts";
import {
clearCommandProgress,
Expand Down Expand Up @@ -218,7 +213,7 @@ export function createSidecarCodingTools(policy: {
],
factory: (env) => {
let sandbox: ReturnType<typeof createPosixBuildExecutor> | undefined;
let spilled: ReturnType<typeof createPosixTools> | undefined;
const savedResults = savedResultTools(env);
// The coder learns whether its passing app reached the person; a failure leaves the build
// going but is never silent.
const fileCheckedApp = async (workspace: string, signal: AbortSignal) => {
Expand Down Expand Up @@ -395,19 +390,7 @@ export function createSidecarCodingTools(policy: {
typeof path === "string" &&
path.startsWith("tool-output://")
) {
spilled ??= createPosixTools({
cwd: workspace,
blobReader: createBlobReader(env.storage),
});
// Models drop a slash from tool-output:///<callId> and retry the same URI verbatim.
const uri = path.replace(
/^tool-output:\/\/(?!\/)/,
"tool-output:///",
);
return spilled.run(
{ ...call, arguments: { ...call.arguments, path: uri } },
signal,
);
return savedResults.run(call, signal);
}
// The sandbox refuses an outside write with a bare EPERM, which a model retries
// verbatim until the doom guard fails the run; name the mistake instead.
Expand Down Expand Up @@ -641,7 +624,6 @@ export function createSidecarCodingTools(policy: {
await opened?.close(AbortSignal.timeout(VERSION_CLOSE_MS));
designCheck?.dispose();
await native.dispose?.();
await spilled?.dispose();
await browser?.tools.then(
(tools) => tools.dispose?.(),
() => undefined,
Expand Down
2 changes: 2 additions & 0 deletions apps/hub/src/workflows/entries/coordinator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { createSkillTools } from "../../skills/tools.ts";
import { roleInference } from "../role-inference.ts";
import { compactingDirector } from "../director.ts";
import { dropDegenerateTurn, trimHistory } from "../compaction.ts";
import { savedResultTools } from "../saved-result-tools.ts";
import { nativePackageName } from "../package-name.ts";

const compacting = compactingDirector(nativePackageName("coordinator"));
Expand All @@ -25,6 +26,7 @@ const agent = createCoordinatorAgent({
createArtifactTools("coordinator"),
createDeployTools(),
createSkillTools(["problem-brief"]),
savedResultTools,
],
});
const workflow = createCoordinatorWorkflow({
Expand Down
8 changes: 7 additions & 1 deletion apps/hub/src/workflows/entries/presenter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { createSkillTools } from "../../skills/tools.ts";
import { roleInference } from "../role-inference.ts";
import { compactingDirector } from "../director.ts";
import { dropDegenerateTurn, trimHistory } from "../compaction.ts";
import { savedResultTools } from "../saved-result-tools.ts";
import { nativePackageName } from "../package-name.ts";

const compacting = compactingDirector(nativePackageName("presenter"));
Expand All @@ -23,7 +24,12 @@ const agent = createPresenterAgent<PackageToolEnv & MailToolEnv>({
id: "presenter",
capabilities: [],
inference: { ...source, parameters },
tools: [createPackageTools(), mail, createSkillTools(["presenting"])],
tools: [
createPackageTools(),
mail,
createSkillTools(["presenting"]),
savedResultTools,
],
});
const workflow = createPresenterWorkflow({
id: "corbits-builder-presenter",
Expand Down
3 changes: 2 additions & 1 deletion apps/hub/src/workflows/entries/reviewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { mail } from "@intx/tools-mail/sidecar-bundle";
import { roleInference } from "../role-inference.ts";
import { compactingDirector } from "../director.ts";
import { dropDegenerateTurn, trimHistory } from "../compaction.ts";
import { savedResultTools } from "../saved-result-tools.ts";
import { nativePackageName } from "../package-name.ts";

const compacting = compactingDirector(nativePackageName("reviewer"));
Expand All @@ -17,7 +18,7 @@ const agent = createReviewerAgent({
id: "reviewer",
capabilities: [],
inference: { ...source, parameters },
tools: [mail],
tools: [mail, savedResultTools],
});
const workflow = createReviewerWorkflow({
id: "corbits-builder-reviewer",
Expand Down
12 changes: 11 additions & 1 deletion apps/hub/src/workflows/packaged.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,18 @@ test("production entries expose concrete native workflows without callback crede
coderAnyPackage,
designer,
presenter,
])
]) {
expect(workflow.credentialBindings ?? []).toEqual([]);
// A trimmed result's note names read_file, so every agent needs one.
const tools = Object.values(workflow.steps).flatMap((step) =>
"agent" in step
? step.agent.toolFactories.flatMap(
(factory) => factory.definitions ?? [],
)
: [],
);
expect(tools.map((tool) => tool.name)).toContain("read_file");
}
expect(
JSON.stringify([
coordinator,
Expand Down
29 changes: 29 additions & 0 deletions apps/hub/src/workflows/saved-result-tools.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { expect, test } from "bun:test";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { savedResultTools } from "./saved-result-tools.ts";

test("the saved-result read_file returns a saved result and refuses every file", async () => {
const workdir = await mkdtemp(join(tmpdir(), "saved-result-"));
await writeFile(join(workdir, "secret.txt"), "SECRET");
const tools = savedResultTools({
workdir,
storage: {
readBlob: async (key: string) => new TextEncoder().encode(`blob ${key}`),
},
} as never);
const read = (path: string) =>
tools.run(
{ id: "c", name: "read_file", arguments: { path } },
new AbortController().signal,
);
expect((await read("tool-output:///abc")).content).toContain("blob abc");
expect((await read("tool-output://abc")).content).toContain("blob abc");
for (const path of [join(workdir, "secret.txt"), "secret.txt"]) {
const result = await read(path);
expect(result.isError).toBe(true);
expect(result.content).not.toContain("SECRET");
}
await rm(workdir, { recursive: true });
});
65 changes: 65 additions & 0 deletions apps/hub/src/workflows/saved-result-tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { defineTool } from "@intx/agent/authoring";
import { createPosixTools, TOOL_NAMES } from "@intx/tools-posix";
import { createBlobReader } from "@intx/types/runtime";

const SAVED_RESULT = "tool-output://";

// Named read_file because the trimming note and Interchange's size-cap note both tell the agent to
// read_file a tool-output:/// URI. It reads only those URIs, so an agent without a workspace gets no
// file access from it; the workdir is a required cwd that is never read.
export const savedResultTools = defineTool({
id: "@corbits-builder/saved-result/tools",
definitions: [{ name: TOOL_NAMES.READ_FILE }],
factory: (env) => {
const posix = createPosixTools({
cwd: env.workdir,
blobReader: createBlobReader(env.storage),
});
return {
definitions: [
{
name: TOOL_NAMES.READ_FILE,
description:
"Read back the whole of an earlier tool result that was trimmed or cut short, with line numbers. Pass the tool-output:///... URI its note gives.",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "The tool-output:///... URI the note gives",
},
offset: {
type: "number",
description: "Zero-based line offset to start reading from",
},
limit: {
type: "number",
description: "Maximum number of lines to return",
},
},
required: ["path"],
},
},
],
run: async (call, signal) => {
const path = call.arguments["path"];
if (typeof path !== "string" || !path.startsWith(SAVED_RESULT))
return {
callId: call.id,
content:
"read_file here reads only a saved result: pass the tool-output:///... URI its note gives.",
isError: true,
};
// Models drop a slash from tool-output:///<callId> and retry it verbatim, and the URI parser
// reads the callId after two slashes as a host.
const uri = path.startsWith(`${SAVED_RESULT}/`)
? path
: path.replace(SAVED_RESULT, `${SAVED_RESULT}/`);
return posix.run(
{ ...call, arguments: { ...call.arguments, path: uri } },
signal,
);
},
};
},
});
2 changes: 2 additions & 0 deletions apps/hub/src/workflows/writer-package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { createSkillTools } from "../skills/tools.ts";
import { roleInference } from "./role-inference.ts";
import { compactingDirector } from "./director.ts";
import { dropDegenerateTurn, trimHistory } from "./compaction.ts";
import { savedResultTools } from "./saved-result-tools.ts";
import { WRITER_PACKAGES } from "@corbits-builder/schemas/installed-workflows";
import { nativePackageName } from "./package-name.ts";

Expand All @@ -31,6 +32,7 @@ export function writerPackage(kind: WriterDocumentKind) {
"product-requirements",
"build-plan",
]),
savedResultTools,
],
});
return {
Expand Down