@mku1tra/que-pasa 0.1.0 → 0.2.1
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/README.md +20 -1
- package/dist/define/index.d.ts +1 -0
- package/dist/define/index.js +1 -0
- package/dist/define/resource.d.ts +15 -0
- package/dist/define/resource.js +8 -0
- package/dist/define/runtime.d.ts +6 -3
- package/dist/define/tool.d.ts +3 -2
- package/dist/define/tool.js +8 -3
- package/dist/loop/extension.d.ts +2 -1
- package/dist/loop/extension.js +17 -7
- package/dist/loop/index.d.ts +1 -0
- package/dist/loop/index.js +1 -0
- package/dist/loop/resources.d.ts +34 -0
- package/dist/loop/resources.js +57 -0
- package/dist/loop/runtime.d.ts +7 -1
- package/dist/loop/runtime.js +10 -2
- package/dist/sandbox/backend.d.ts +82 -0
- package/dist/sandbox/backend.js +10 -0
- package/dist/sandbox/index.d.ts +7 -0
- package/dist/sandbox/index.js +6 -0
- package/dist/sandbox/local.d.ts +8 -0
- package/dist/sandbox/local.js +20 -0
- package/dist/sandbox/paths.d.ts +10 -0
- package/dist/sandbox/paths.js +31 -0
- package/dist/sandbox/podman.d.ts +38 -0
- package/dist/sandbox/podman.js +137 -0
- package/dist/sandbox/process.d.ts +27 -0
- package/dist/sandbox/process.js +52 -0
- package/dist/sandbox/shell.d.ts +17 -0
- package/dist/sandbox/shell.js +206 -0
- package/dist/sandbox/tools.d.ts +80 -0
- package/dist/sandbox/tools.js +242 -0
- package/dist/sandbox/view.d.ts +17 -0
- package/dist/sandbox/view.js +105 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ que-v2's harness, on pi-durable: a workspace defines its agents with `@mku1tra/q
|
|
|
4
4
|
|
|
5
5
|
## Run and check
|
|
6
6
|
|
|
7
|
-
Node runs in the dev container (`.devcontainer/devcontainer.json`), never on the host. You need `just`, `podman` and `npx
|
|
7
|
+
Node runs in the dev container (`.devcontainer/devcontainer.json`), never on the host. The container starts from the dev image, `ghcr.io/matta-kelly/que-pasa-dev:main`, which `.devcontainer/Containerfile` builds with the npm cache of `package-lock.json`, and the `dev-image` workflow on main builds and pushes it. You need `just`, `podman` and `npx`; once per machine, log in with `podman login ghcr.io`, and the first `just up` pulls the dev image, then installs from its cache in seconds.
|
|
8
8
|
|
|
9
9
|
- `just up`: make or start this checkout's dev container
|
|
10
10
|
- `just check`: the check set, `scripts/check.sh`: notes, then types, lint, format, tests and the build. The pre-push hook and the pipeline run the same file
|
|
@@ -24,6 +24,25 @@ await runtime.close();
|
|
|
24
24
|
|
|
25
25
|
One runtime per process owns its SQLite file. After a crash, open it again with the same agents: unfinished runs go on, and `run` with the same `runId` gives back the result.
|
|
26
26
|
|
|
27
|
+
A tool gets what it acts on from its run, as a resource the host provides. The file and command tools of `@mku1tra/que-pasa/sandbox` act in the `Sandbox` the host gives each run:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { provide } from "@mku1tra/que-pasa/loop";
|
|
31
|
+
import { Sandbox, execute, fileTools, podman, view } from "@mku1tra/que-pasa/sandbox";
|
|
32
|
+
|
|
33
|
+
const coder = defineAgent({ name: "coder", model, instructions, tools: [...fileTools, execute] });
|
|
34
|
+
const runtime = await openRuntime({
|
|
35
|
+
storage, models, agents: [coder],
|
|
36
|
+
resources: [
|
|
37
|
+
provide(Sandbox, ({ runId }) =>
|
|
38
|
+
podman({ name: `run-${runId}`, image: "node:22", mounts: [{ source: worktree, target: "/work" }] }),
|
|
39
|
+
),
|
|
40
|
+
],
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
A provider resolves from the run as it was started, so a run that resumes after a crash gets the same sandbox. `view(backend, { read, write })` limits a run to some files. What each backend's wall stops: `sandbox/que-pasa-sandbox.md`.
|
|
45
|
+
|
|
27
46
|
Each run is traced as OpenTelemetry GenAI spans through `@opentelemetry/api`. To keep them, register an SDK (a tracer provider, an exporter, and an async context manager). To add what only the host knows, call `runtime.run()` inside a span of its own. `captureContent: true` on `openRuntime` adds message content.
|
|
28
47
|
|
|
29
48
|
## Use it from another project
|
package/dist/define/index.d.ts
CHANGED
package/dist/define/index.js
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
declare const valueType: unique symbol;
|
|
2
|
+
/**
|
|
3
|
+
* Something a tool needs that the host gives each run: a sandbox, and later a connection. It holds no value, only its
|
|
4
|
+
* name and the type of its value: the host provides the value per run (`openRuntime({ resources })`), and a tool asks
|
|
5
|
+
* for it with `runtime.use(resource)`.
|
|
6
|
+
*/
|
|
7
|
+
export type Resource<T = unknown> = {
|
|
8
|
+
/** kebab-case: the host provides it by this name. */
|
|
9
|
+
readonly name: string;
|
|
10
|
+
/** The type of its value; never set. */
|
|
11
|
+
readonly [valueType]?: T;
|
|
12
|
+
};
|
|
13
|
+
/** A resource, checked: its name is kebab-case. */
|
|
14
|
+
export declare function defineResource<T>(name: string): Resource<T>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const resourceName = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
2
|
+
/** A resource, checked: its name is kebab-case. */
|
|
3
|
+
export function defineResource(name) {
|
|
4
|
+
if (!resourceName.test(name)) {
|
|
5
|
+
throw new Error(`resource "${name}": the name must be kebab-case`);
|
|
6
|
+
}
|
|
7
|
+
return Object.freeze({ name });
|
|
8
|
+
}
|
package/dist/define/runtime.d.ts
CHANGED
|
@@ -2,16 +2,19 @@
|
|
|
2
2
|
export type Caller = {
|
|
3
3
|
id: string;
|
|
4
4
|
};
|
|
5
|
+
import type { Resource } from "./resource.ts";
|
|
5
6
|
/** A JSON value: what a run keeps in storage, so it survives a restart. */
|
|
6
7
|
export type Json = string | number | boolean | null | Json[] | {
|
|
7
8
|
[key: string]: Json;
|
|
8
9
|
};
|
|
9
10
|
/**
|
|
10
|
-
* What a tool gets from its run, besides its input: the caller,
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* What a tool gets from its run, besides its input: the caller, the context the run was given (LangChain's runtime
|
|
12
|
+
* context), and the resources the host provides for the run. The context is JSON, because the run keeps it, and a run
|
|
13
|
+
* that resumes after a restart gives its tools the same one, so they get the same resources.
|
|
13
14
|
*/
|
|
14
15
|
export type Runtime<C extends Json | undefined = undefined> = {
|
|
15
16
|
caller: Caller;
|
|
16
17
|
context: C;
|
|
18
|
+
/** The run's value of a resource the tool declares in `uses`. Any other resource throws. */
|
|
19
|
+
use<T>(resource: Resource<T>): Promise<T>;
|
|
17
20
|
};
|
package/dist/define/tool.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { z } from "zod";
|
|
2
|
+
import type { Resource } from "./resource.ts";
|
|
2
3
|
import type { Json, Runtime } from "./runtime.ts";
|
|
3
4
|
/** A function an agent can call: written once in a workspace, so any agent in it can be given it. */
|
|
4
5
|
export type Tool<S extends z.ZodObject = z.ZodObject, C extends Json | undefined = undefined> = {
|
|
@@ -8,8 +9,8 @@ export type Tool<S extends z.ZodObject = z.ZodObject, C extends Json | undefined
|
|
|
8
9
|
description: string;
|
|
9
10
|
/** The input the model must give. */
|
|
10
11
|
schema: S;
|
|
11
|
-
/** The
|
|
12
|
-
|
|
12
|
+
/** The resources it asks its run for, with `runtime.use`: a sandbox, a connection. It never holds one. */
|
|
13
|
+
uses?: readonly Resource[];
|
|
13
14
|
/**
|
|
14
15
|
* "safe": a call a crash cut off runs again when the run resumes. Left out, it never runs twice: the model gets an
|
|
15
16
|
* `interrupted` result and decides what to do. pi's own term.
|
package/dist/define/tool.js
CHANGED
|
@@ -7,8 +7,13 @@ export function defineTool(tool) {
|
|
|
7
7
|
if (tool.description.trim() === "") {
|
|
8
8
|
throw new Error(`tool "${tool.name}": the description is empty`);
|
|
9
9
|
}
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const uses = tool.uses ?? [];
|
|
11
|
+
const named = new Set(uses.map((resource) => resource.name));
|
|
12
|
+
if (named.size !== uses.length) {
|
|
13
|
+
throw new Error(`tool "${tool.name}": a resource is in uses twice`);
|
|
12
14
|
}
|
|
13
|
-
return Object.freeze({
|
|
15
|
+
return Object.freeze({
|
|
16
|
+
...tool,
|
|
17
|
+
...(tool.uses ? { uses: Object.freeze([...tool.uses]) } : {}),
|
|
18
|
+
});
|
|
14
19
|
}
|
package/dist/loop/extension.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Extension } from "@earendil-works/pi-durable";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import type { Agent, Json } from "../define/index.ts";
|
|
4
|
+
import type { Resources } from "./resources.ts";
|
|
4
5
|
/** Any agent, as the runtime holds it. */
|
|
5
6
|
export type AnyAgent = Agent<z.ZodObject | undefined, Json | undefined>;
|
|
6
7
|
/** The tool an agent with an output calls to give its result. No agent tool may take its name. */
|
|
@@ -14,7 +15,7 @@ export declare const extensionName: (agent: AnyAgent) => string;
|
|
|
14
15
|
* no run's state: each call reads its run's document and transcript, so it serves every run of the agent, also one that
|
|
15
16
|
* resumes in a new process.
|
|
16
17
|
*/
|
|
17
|
-
export declare function agentExtension(agent: AnyAgent): Extension;
|
|
18
|
+
export declare function agentExtension(agent: AnyAgent, resources: Resources): Extension;
|
|
18
19
|
/**
|
|
19
20
|
* The JSON Schema of what the model must give: the input shape, so a field with a default is not required. Without the
|
|
20
21
|
* `$schema` key a tool's parameters do not take.
|
package/dist/loop/extension.js
CHANGED
|
@@ -15,12 +15,12 @@ export const extensionName = (agent) => `agent-${agent.name}`;
|
|
|
15
15
|
* no run's state: each call reads its run's document and transcript, so it serves every run of the agent, also one that
|
|
16
16
|
* resumes in a new process.
|
|
17
17
|
*/
|
|
18
|
-
export function agentExtension(agent) {
|
|
18
|
+
export function agentExtension(agent, resources) {
|
|
19
19
|
const { output } = agent;
|
|
20
20
|
if (output && agent.tools.some((tool) => tool.name === submitTool)) {
|
|
21
21
|
throw new Error(`agent "${agent.name}": a tool is named "${submitTool}", which its output needs`);
|
|
22
22
|
}
|
|
23
|
-
const tools = agent.tools.map(toPiTool);
|
|
23
|
+
const tools = agent.tools.map((tool) => toPiTool(tool, resources));
|
|
24
24
|
if (output)
|
|
25
25
|
tools.push(submit(output));
|
|
26
26
|
return defineExtension({
|
|
@@ -30,14 +30,14 @@ export function agentExtension(agent) {
|
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
32
|
/** A que-pasa tool as a pi tool: pi checks the arguments against the JSON Schema, zod parses them, and the run's runtime goes in. */
|
|
33
|
-
function toPiTool(tool) {
|
|
33
|
+
function toPiTool(tool, resources) {
|
|
34
34
|
return definePiTool({
|
|
35
35
|
name: tool.name,
|
|
36
36
|
description: tool.description,
|
|
37
37
|
parameters: jsonSchema(tool.schema),
|
|
38
38
|
...(tool.replay ? { replay: tool.replay } : {}),
|
|
39
39
|
execute: async (args, api, context) => {
|
|
40
|
-
const result = await tool.run(tool.schema.parse(args), await runtimeOf(api, context));
|
|
40
|
+
const result = await tool.run(tool.schema.parse(args), await runtimeOf(tool, resources, api, context));
|
|
41
41
|
return {
|
|
42
42
|
output: [
|
|
43
43
|
{
|
|
@@ -73,12 +73,22 @@ const remind = hook(GenerationTask, {
|
|
|
73
73
|
return sent < reminders ? { continue: reminder } : undefined;
|
|
74
74
|
},
|
|
75
75
|
});
|
|
76
|
-
/** The runtime of the call's run, from its run document. */
|
|
77
|
-
async function runtimeOf(api, context) {
|
|
76
|
+
/** The runtime of the call's run, from its run document: its caller, its context, and the resources the tool uses. */
|
|
77
|
+
async function runtimeOf(tool, resources, api, context) {
|
|
78
78
|
const run = await api.snapshot(RunDoc, api.conversationId, context);
|
|
79
79
|
if (!run)
|
|
80
80
|
throw new Error("the run has no run document");
|
|
81
|
-
|
|
81
|
+
const uses = new Set((tool.uses ?? []).map((resource) => resource.name));
|
|
82
|
+
return {
|
|
83
|
+
caller: run.caller,
|
|
84
|
+
context: run.context,
|
|
85
|
+
use: async (resource) => {
|
|
86
|
+
if (!uses.has(resource.name)) {
|
|
87
|
+
throw new Error(`tool "${tool.name}" uses resource "${resource.name}", which it does not declare in uses`);
|
|
88
|
+
}
|
|
89
|
+
return resources.use(run, resource);
|
|
90
|
+
},
|
|
91
|
+
};
|
|
82
92
|
}
|
|
83
93
|
/**
|
|
84
94
|
* The JSON Schema of what the model must give: the input shape, so a field with a default is not required. Without the
|
package/dist/loop/index.d.ts
CHANGED
package/dist/loop/index.js
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Caller, Json, Resource } from "../define/index.ts";
|
|
2
|
+
import type { RunState } from "./state.ts";
|
|
3
|
+
/** What a provider resolves a resource from: the run as it was started, read from its run document. */
|
|
4
|
+
export type RunOf = {
|
|
5
|
+
runId: string;
|
|
6
|
+
caller: Caller;
|
|
7
|
+
/** Absent when the run was given none. */
|
|
8
|
+
context?: Json;
|
|
9
|
+
};
|
|
10
|
+
/** The host's value of one resource, per run. */
|
|
11
|
+
export type Provider<T = unknown> = {
|
|
12
|
+
readonly resource: Resource<T>;
|
|
13
|
+
resolve(run: RunOf): T | Promise<T>;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* The host's value of a resource for each run, resolved from the run as it was started: a run that resumes in a new
|
|
17
|
+
* process gets the same one.
|
|
18
|
+
*/
|
|
19
|
+
export declare function provide<T>(resource: Resource<T>, resolve: (run: RunOf) => T | Promise<T>): Provider<T>;
|
|
20
|
+
/** The host's providers, and each run's resolved values: resolved once per run while it goes on in this process. */
|
|
21
|
+
export declare function createResources(providers: readonly Provider[]): {
|
|
22
|
+
/** Throws when a resource a tool uses has no provider: at open, never in a run. */
|
|
23
|
+
check(uses: readonly {
|
|
24
|
+
tool: string;
|
|
25
|
+
resource: Resource;
|
|
26
|
+
}[]): void;
|
|
27
|
+
/** The run's value of a resource. */
|
|
28
|
+
use<T>(run: RunState, resource: Resource<T>): Promise<T>;
|
|
29
|
+
/** Drop a run's values, when it ends. */
|
|
30
|
+
forget(runId: string): void;
|
|
31
|
+
/** Drop every run's values, when the runtime closes. */
|
|
32
|
+
clear(): void;
|
|
33
|
+
};
|
|
34
|
+
export type Resources = ReturnType<typeof createResources>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The host's value of a resource for each run, resolved from the run as it was started: a run that resumes in a new
|
|
3
|
+
* process gets the same one.
|
|
4
|
+
*/
|
|
5
|
+
export function provide(resource, resolve) {
|
|
6
|
+
return Object.freeze({ resource, resolve });
|
|
7
|
+
}
|
|
8
|
+
/** The host's providers, and each run's resolved values: resolved once per run while it goes on in this process. */
|
|
9
|
+
export function createResources(providers) {
|
|
10
|
+
const byName = new Map();
|
|
11
|
+
for (const provider of providers) {
|
|
12
|
+
const { name } = provider.resource;
|
|
13
|
+
if (byName.has(name))
|
|
14
|
+
throw new Error(`resource "${name}" is provided twice`);
|
|
15
|
+
byName.set(name, provider);
|
|
16
|
+
}
|
|
17
|
+
const resolved = new Map();
|
|
18
|
+
return {
|
|
19
|
+
/** Throws when a resource a tool uses has no provider: at open, never in a run. */
|
|
20
|
+
check(uses) {
|
|
21
|
+
for (const { tool, resource } of uses) {
|
|
22
|
+
if (!byName.has(resource.name)) {
|
|
23
|
+
throw new Error(`tool "${tool}" uses resource "${resource.name}", which is not provided`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
/** The run's value of a resource. */
|
|
28
|
+
use(run, resource) {
|
|
29
|
+
const provider = byName.get(resource.name);
|
|
30
|
+
if (!provider)
|
|
31
|
+
throw new Error(`resource "${resource.name}" is not provided`);
|
|
32
|
+
const values = resolved.get(run.runId) ??
|
|
33
|
+
resolved.set(run.runId, new Map()).get(run.runId);
|
|
34
|
+
let value = values.get(resource.name);
|
|
35
|
+
if (!value) {
|
|
36
|
+
const { runId, caller, context } = run;
|
|
37
|
+
value = Promise.resolve(provider.resolve({
|
|
38
|
+
runId,
|
|
39
|
+
caller,
|
|
40
|
+
...(context === undefined ? {} : { context }),
|
|
41
|
+
}));
|
|
42
|
+
// A failed resolve is not kept: the next call tries again.
|
|
43
|
+
value.catch(() => values.delete(resource.name));
|
|
44
|
+
values.set(resource.name, value);
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
},
|
|
48
|
+
/** Drop a run's values, when it ends. */
|
|
49
|
+
forget(runId) {
|
|
50
|
+
resolved.delete(runId);
|
|
51
|
+
},
|
|
52
|
+
/** Drop every run's values, when the runtime closes. */
|
|
53
|
+
clear() {
|
|
54
|
+
resolved.clear();
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
package/dist/loop/runtime.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type Storage, type UsageState } from "@earendil-works/pi-durable";
|
|
|
3
3
|
import type { z } from "zod";
|
|
4
4
|
import { type Agent, type Caller, type Json } from "../define/index.ts";
|
|
5
5
|
import { type AnyAgent } from "./extension.ts";
|
|
6
|
+
import { type Provider } from "./resources.ts";
|
|
6
7
|
/** The tokens and cost a run spent: per `provider/model-id` for model calls, and per tool for tools that report it. */
|
|
7
8
|
export type RunUsage = UsageState;
|
|
8
9
|
/** What a run gives back: the agent's output, checked against its schema, or its answer as text. */
|
|
@@ -15,6 +16,11 @@ export type RuntimeOptions = {
|
|
|
15
16
|
models: Models;
|
|
16
17
|
/** Every agent the host can run. A run that resumes after a restart finds its agent here, by name. */
|
|
17
18
|
agents: readonly AnyAgent[];
|
|
19
|
+
/**
|
|
20
|
+
* The host's value of each resource the agents' tools use, per run: `provide(resource, (run) => value)`. A provider
|
|
21
|
+
* resolves from the run as it was started, so a run that resumes after a restart gets the same one.
|
|
22
|
+
*/
|
|
23
|
+
resources?: readonly Provider[];
|
|
18
24
|
/**
|
|
19
25
|
* Record message content in the trace: instructions, tool definitions, messages, tool arguments and results. Off by
|
|
20
26
|
* default, as the GenAI conventions say: content can hold sensitive data.
|
|
@@ -58,4 +64,4 @@ export type Runtime = {
|
|
|
58
64
|
* Open a runtime: one pi harness on `storage`, with each agent installed as its own extension. Runs a crash left
|
|
59
65
|
* unfinished go on at once.
|
|
60
66
|
*/
|
|
61
|
-
export declare function openRuntime({ storage, models, agents, captureContent, }: RuntimeOptions): Promise<Runtime>;
|
|
67
|
+
export declare function openRuntime({ storage, models, agents, resources: providers, captureContent, }: RuntimeOptions): Promise<Runtime>;
|
package/dist/loop/runtime.js
CHANGED
|
@@ -2,6 +2,7 @@ import { BACKGROUND_CONTEXT, withAbortSignal, } from "@earendil-works/chord/cont
|
|
|
2
2
|
import { configure, createRegistry, Harness, UsageDoc, } from "@earendil-works/pi-durable";
|
|
3
3
|
import { parseModel, } from "../define/index.js";
|
|
4
4
|
import { agentExtension, submitTool } from "./extension.js";
|
|
5
|
+
import { createResources } from "./resources.js";
|
|
5
6
|
import { RunDoc, RunIndex } from "./state.js";
|
|
6
7
|
import { activeParent, createTracing } from "./trace.js";
|
|
7
8
|
import { answerOf, messagesOf, resultOf } from "./transcript.js";
|
|
@@ -10,12 +11,17 @@ const context = BACKGROUND_CONTEXT;
|
|
|
10
11
|
* Open a runtime: one pi harness on `storage`, with each agent installed as its own extension. Runs a crash left
|
|
11
12
|
* unfinished go on at once.
|
|
12
13
|
*/
|
|
13
|
-
export async function openRuntime({ storage, models, agents, captureContent = false, }) {
|
|
14
|
+
export async function openRuntime({ storage, models, agents, resources: providers = [], captureContent = false, }) {
|
|
15
|
+
const resources = createResources(providers);
|
|
16
|
+
resources.check(agents.flatMap((agent) => agent.tools.flatMap((tool) => (tool.uses ?? []).map((resource) => ({ tool: tool.name, resource })))));
|
|
14
17
|
const installed = new Map();
|
|
15
18
|
for (const agent of agents) {
|
|
16
19
|
if (installed.has(agent.name))
|
|
17
20
|
throw new Error(`two agents are named "${agent.name}"`);
|
|
18
|
-
installed.set(agent.name, {
|
|
21
|
+
installed.set(agent.name, {
|
|
22
|
+
agent,
|
|
23
|
+
extension: agentExtension(agent, resources),
|
|
24
|
+
});
|
|
19
25
|
}
|
|
20
26
|
const tracing = createTracing({
|
|
21
27
|
agents: new Map([...installed].map(([name, { agent }]) => [name, agent])),
|
|
@@ -103,6 +109,7 @@ export async function openRuntime({ storage, models, agents, captureContent = fa
|
|
|
103
109
|
tools: {},
|
|
104
110
|
});
|
|
105
111
|
onMessages?.(await transcript());
|
|
112
|
+
resources.forget(runId);
|
|
106
113
|
}
|
|
107
114
|
}
|
|
108
115
|
return {
|
|
@@ -110,6 +117,7 @@ export async function openRuntime({ storage, models, agents, captureContent = fa
|
|
|
110
117
|
async close() {
|
|
111
118
|
await tracing.closeAll();
|
|
112
119
|
await harness.close(context);
|
|
120
|
+
resources.clear();
|
|
113
121
|
},
|
|
114
122
|
};
|
|
115
123
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A backend: the place where a run's files are and its commands run, behind one interface. Two abilities, as in
|
|
3
|
+
* LangChain Deep Agents' backend protocol: files (`FileBackend`), and commands on top of files (`SandboxBackend`).
|
|
4
|
+
* Every path is the backend's own: relative to its root, or absolute under it. Every path a result gives is relative to
|
|
5
|
+
* the root. A backend gives its errors back in the result, and throws only when it cannot run at all.
|
|
6
|
+
*/
|
|
7
|
+
/** Why an operation did nothing. Deep Agents' file errors, and the two of an edit. */
|
|
8
|
+
export type FileError = "file_not_found" | "is_directory" | "invalid_path" | "permission_denied" | "string_not_found" | "string_not_unique";
|
|
9
|
+
/** What every result can say: that the operation did nothing, and why. `message` says more, for the model. */
|
|
10
|
+
export type Failed = {
|
|
11
|
+
error?: FileError;
|
|
12
|
+
message?: string;
|
|
13
|
+
};
|
|
14
|
+
export type FileInfo = {
|
|
15
|
+
path: string;
|
|
16
|
+
isDir: boolean;
|
|
17
|
+
};
|
|
18
|
+
export type LsResult = Failed & {
|
|
19
|
+
files?: FileInfo[];
|
|
20
|
+
};
|
|
21
|
+
export type ReadResult = Failed & {
|
|
22
|
+
/** The lines asked for, each with its newline. */
|
|
23
|
+
content?: string;
|
|
24
|
+
/** How many lines the file has. */
|
|
25
|
+
totalLines?: number;
|
|
26
|
+
};
|
|
27
|
+
export type WriteResult = Failed & {
|
|
28
|
+
path?: string;
|
|
29
|
+
};
|
|
30
|
+
export type EditResult = Failed & {
|
|
31
|
+
path?: string;
|
|
32
|
+
occurrences?: number;
|
|
33
|
+
};
|
|
34
|
+
export type GlobResult = Failed & {
|
|
35
|
+
files?: string[];
|
|
36
|
+
truncated?: boolean;
|
|
37
|
+
};
|
|
38
|
+
export type GrepMatch = {
|
|
39
|
+
path: string;
|
|
40
|
+
line: number;
|
|
41
|
+
text: string;
|
|
42
|
+
};
|
|
43
|
+
export type GrepResult = Failed & {
|
|
44
|
+
matches?: GrepMatch[];
|
|
45
|
+
truncated?: boolean;
|
|
46
|
+
};
|
|
47
|
+
export type ExecuteResult = {
|
|
48
|
+
/** stdout and stderr, as they came. */
|
|
49
|
+
output: string;
|
|
50
|
+
/** null when the command was killed by a signal. */
|
|
51
|
+
exitCode: number | null;
|
|
52
|
+
/** The output kept only its tail. */
|
|
53
|
+
truncated: boolean;
|
|
54
|
+
};
|
|
55
|
+
/** Files: read, write and search them. */
|
|
56
|
+
export interface FileBackend {
|
|
57
|
+
/** The folder every path is under. */
|
|
58
|
+
readonly root: string;
|
|
59
|
+
/** What a folder holds, or the file itself. */
|
|
60
|
+
ls(path: string): Promise<LsResult>;
|
|
61
|
+
/** `limit` lines of a file, after the first `offset`. */
|
|
62
|
+
read(path: string, offset: number, limit: number): Promise<ReadResult>;
|
|
63
|
+
/** A file, whole, made with its folder if it has none. It replaces what was there. */
|
|
64
|
+
write(path: string, content: string): Promise<WriteResult>;
|
|
65
|
+
/** Replace `old` with `next` in a file: it must occur once, unless `all`. */
|
|
66
|
+
edit(path: string, old: string, next: string, all: boolean): Promise<EditResult>;
|
|
67
|
+
/** The files under a folder whose path from that folder matches a glob. */
|
|
68
|
+
glob(pattern: string, path: string): Promise<GlobResult>;
|
|
69
|
+
/** The lines of the files under a path that hold a text, as is (not a pattern), in files matching `glob` if given. */
|
|
70
|
+
grep(pattern: string, path: string, glob?: string): Promise<GrepResult>;
|
|
71
|
+
}
|
|
72
|
+
/** Files, and commands: a backend that can run a shell command. */
|
|
73
|
+
export interface SandboxBackend extends FileBackend {
|
|
74
|
+
/** Names this sandbox, such as its container. */
|
|
75
|
+
readonly id: string;
|
|
76
|
+
/** Run a shell command in the root. The signal kills it. */
|
|
77
|
+
execute(command: string, options?: {
|
|
78
|
+
signal?: AbortSignal;
|
|
79
|
+
}): Promise<ExecuteResult>;
|
|
80
|
+
}
|
|
81
|
+
/** Whether a backend can run commands. */
|
|
82
|
+
export declare function canExecute(backend: FileBackend): backend is SandboxBackend;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A backend: the place where a run's files are and its commands run, behind one interface. Two abilities, as in
|
|
3
|
+
* LangChain Deep Agents' backend protocol: files (`FileBackend`), and commands on top of files (`SandboxBackend`).
|
|
4
|
+
* Every path is the backend's own: relative to its root, or absolute under it. Every path a result gives is relative to
|
|
5
|
+
* the root. A backend gives its errors back in the result, and throws only when it cannot run at all.
|
|
6
|
+
*/
|
|
7
|
+
/** Whether a backend can run commands. */
|
|
8
|
+
export function canExecute(backend) {
|
|
9
|
+
return typeof backend.execute === "function";
|
|
10
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { canExecute, type EditResult, type ExecuteResult, type FileBackend, type FileError, type FileInfo, type GlobResult, type GrepMatch, type GrepResult, type LsResult, type ReadResult, type SandboxBackend, type WriteResult, } from "./backend.ts";
|
|
2
|
+
export { local } from "./local.ts";
|
|
3
|
+
export { podman, type Mount, type Podman, type PodmanOptions, } from "./podman.ts";
|
|
4
|
+
export { shellBackend, type Exec } from "./shell.ts";
|
|
5
|
+
export type { Ran, Script } from "./process.ts";
|
|
6
|
+
export { Sandbox, edit_file, execute, fileTools, glob, grep, ls, read_file, readTools, write_file, } from "./tools.ts";
|
|
7
|
+
export { view, type ViewPaths } from "./view.ts";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { canExecute, } from "./backend.js";
|
|
2
|
+
export { local } from "./local.js";
|
|
3
|
+
export { podman, } from "./podman.js";
|
|
4
|
+
export { shellBackend } from "./shell.js";
|
|
5
|
+
export { Sandbox, edit_file, execute, fileTools, glob, grep, ls, read_file, readTools, write_file, } from "./tools.js";
|
|
6
|
+
export { view } from "./view.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SandboxBackend } from "./backend.ts";
|
|
2
|
+
/**
|
|
3
|
+
* A folder on the host as a backend: files and commands run as the host's own user. No wall: a path is checked
|
|
4
|
+
* against the root, but a symlink, or any command, reaches the whole host. For the host's own trusted work, and tests.
|
|
5
|
+
*/
|
|
6
|
+
export declare function local({ root }: {
|
|
7
|
+
root: string;
|
|
8
|
+
}): SandboxBackend;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { isAbsolute } from "node:path";
|
|
2
|
+
import { run } from "./process.js";
|
|
3
|
+
import { shellBackend } from "./shell.js";
|
|
4
|
+
/**
|
|
5
|
+
* A folder on the host as a backend: files and commands run as the host's own user. No wall: a path is checked
|
|
6
|
+
* against the root, but a symlink, or any command, reaches the whole host. For the host's own trusted work, and tests.
|
|
7
|
+
*/
|
|
8
|
+
export function local({ root }) {
|
|
9
|
+
if (!isAbsolute(root))
|
|
10
|
+
throw new Error(`local: the root must be absolute, not "${root}"`);
|
|
11
|
+
return shellBackend({
|
|
12
|
+
id: `local:${root}`,
|
|
13
|
+
root,
|
|
14
|
+
exec: ({ script, args = [], stdin, signal }) => run("sh", ["-c", script, "sh", ...args], {
|
|
15
|
+
cwd: root,
|
|
16
|
+
...(stdin === undefined ? {} : { stdin }),
|
|
17
|
+
...(signal === undefined ? {} : { signal }),
|
|
18
|
+
}),
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A path as a tool gives it, as a path from the root: "" for the root itself. undefined for a path the backend refuses:
|
|
3
|
+
* one that leaves the root (by `..`, or absolute and not under it), starts with `~`, or holds a control character,
|
|
4
|
+
* which a line-based listing cannot carry.
|
|
5
|
+
*/
|
|
6
|
+
export declare function fromRoot(root: string, path: string): string | undefined;
|
|
7
|
+
/** A path from the root as a shell argument: "." for the root. */
|
|
8
|
+
export declare const asArg: (p: string) => string;
|
|
9
|
+
/** Whether `path` is `folder` or under it; every path is under the root (""). */
|
|
10
|
+
export declare const isUnder: (path: string, folder: string) => boolean;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
/**
|
|
3
|
+
* A path as a tool gives it, as a path from the root: "" for the root itself. undefined for a path the backend refuses:
|
|
4
|
+
* one that leaves the root (by `..`, or absolute and not under it), starts with `~`, or holds a control character,
|
|
5
|
+
* which a line-based listing cannot carry.
|
|
6
|
+
*/
|
|
7
|
+
export function fromRoot(root, path) {
|
|
8
|
+
const given = path.trim();
|
|
9
|
+
// A control character, such as a newline: no file of a backend is named with one.
|
|
10
|
+
// eslint-disable-next-line no-control-regex
|
|
11
|
+
if (/[\u0000-\u001f\u007f]/.test(given) || given.startsWith("~"))
|
|
12
|
+
return undefined;
|
|
13
|
+
let p = given === "" ? "." : given;
|
|
14
|
+
if (posix.isAbsolute(p)) {
|
|
15
|
+
const top = posix.normalize(root).replace(/\/+$/, "");
|
|
16
|
+
const at = posix.normalize(p).replace(/\/+$/, "");
|
|
17
|
+
if (at === top)
|
|
18
|
+
return "";
|
|
19
|
+
if (!at.startsWith(`${top}/`))
|
|
20
|
+
return undefined;
|
|
21
|
+
p = at.slice(top.length + 1);
|
|
22
|
+
}
|
|
23
|
+
const n = posix.normalize(p).replace(/\/+$/, "");
|
|
24
|
+
if (n === ".." || n.startsWith("../"))
|
|
25
|
+
return undefined;
|
|
26
|
+
return n === "." ? "" : n;
|
|
27
|
+
}
|
|
28
|
+
/** A path from the root as a shell argument: "." for the root. */
|
|
29
|
+
export const asArg = (p) => p || ".";
|
|
30
|
+
/** Whether `path` is `folder` or under it; every path is under the root (""). */
|
|
31
|
+
export const isUnder = (path, folder) => folder === "" || path === folder || path.startsWith(`${folder}/`);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { SandboxBackend } from "./backend.ts";
|
|
2
|
+
/** A folder of the host in the container. */
|
|
3
|
+
export type Mount = {
|
|
4
|
+
source: string;
|
|
5
|
+
target: string;
|
|
6
|
+
readOnly?: boolean;
|
|
7
|
+
};
|
|
8
|
+
/** One container, as the host sets it: what it runs, what it sees, and what bounds it. */
|
|
9
|
+
export type PodmanOptions = {
|
|
10
|
+
/** The container's name: one per sandbox, so a process that starts again finds it. */
|
|
11
|
+
name: string;
|
|
12
|
+
image: string;
|
|
13
|
+
/** The host folders it sees. Nothing else of the host is in it. */
|
|
14
|
+
mounts: readonly Mount[];
|
|
15
|
+
/** Its root: every path is under it. */
|
|
16
|
+
workdir?: string;
|
|
17
|
+
/** "none" by default: no network. */
|
|
18
|
+
network?: string;
|
|
19
|
+
/** The user commands run as, such as "1000:1000". */
|
|
20
|
+
user?: string;
|
|
21
|
+
/** Such as "4g". */
|
|
22
|
+
memory?: string;
|
|
23
|
+
/** Such as 3. */
|
|
24
|
+
cpus?: number;
|
|
25
|
+
/** Any other `podman run` options, such as `--userns=keep-id`. */
|
|
26
|
+
runArgs?: readonly string[];
|
|
27
|
+
};
|
|
28
|
+
/** A Podman container that outlives the process, as a backend. */
|
|
29
|
+
export type Podman = SandboxBackend & {
|
|
30
|
+
/** Remove the container. The next call makes it again. */
|
|
31
|
+
close(): Promise<void>;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* A Podman container as a backend: every operation execs into it, so its mounts, its network and its limits are the
|
|
35
|
+
* wall. It is made on its first call and kept: started again if it stopped, made again if it is gone, so a run that
|
|
36
|
+
* resumes after a restart works with no host step. One made with other options is refused, never used.
|
|
37
|
+
*/
|
|
38
|
+
export declare function podman(options: PodmanOptions): Podman;
|