@hachej/boring-agent 0.1.110 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +262 -51
- package/dist/agents.d.ts +75 -0
- package/dist/agents.js +91 -0
- package/dist/approval.d.ts +71 -0
- package/dist/approval.js +126 -0
- package/dist/artifacts.d.ts +53 -0
- package/dist/artifacts.js +129 -0
- package/dist/ask-user.d.ts +40 -0
- package/dist/ask-user.js +134 -0
- package/dist/attachment.d.ts +5 -0
- package/dist/attachment.js +4 -0
- package/dist/browser-task.d.ts +32 -0
- package/dist/browser-task.js +119 -0
- package/dist/chat-transport.d.ts +80 -0
- package/dist/chat-transport.js +306 -0
- package/dist/contracts.d.ts +40 -0
- package/dist/contracts.js +1 -0
- package/dist/conversations.d.ts +136 -0
- package/dist/conversations.js +366 -0
- package/dist/definitions.d.ts +37 -0
- package/dist/definitions.js +99 -0
- package/dist/delivery-input.d.ts +35 -0
- package/dist/delivery-input.js +56 -0
- package/dist/delivery.d.ts +33 -0
- package/dist/delivery.js +154 -0
- package/dist/durable-wait.d.ts +23 -0
- package/dist/durable-wait.js +79 -0
- package/dist/file-convert.d.ts +46 -0
- package/dist/file-convert.js +159 -0
- package/dist/file-guard.d.ts +50 -0
- package/dist/file-guard.js +171 -0
- package/dist/file-types.d.ts +17 -0
- package/dist/file-types.js +95 -0
- package/dist/gateway-provider.d.ts +24 -0
- package/dist/gateway-provider.js +42 -0
- package/dist/git.d.ts +34 -0
- package/dist/git.js +79 -0
- package/dist/harness-pool.d.ts +38 -0
- package/dist/harness-pool.js +134 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1 -0
- package/dist/json-schema-tool.d.ts +25 -0
- package/dist/json-schema-tool.js +19 -0
- package/dist/memory/optchat-core.d.ts +118 -0
- package/dist/memory/optchat-core.js +317 -0
- package/dist/memory/optchat.d.ts +156 -0
- package/dist/memory/optchat.js +432 -0
- package/dist/mentions.d.ts +56 -0
- package/dist/mentions.js +140 -0
- package/dist/metering.d.ts +183 -0
- package/dist/metering.js +443 -0
- package/dist/model-gateway.d.ts +86 -0
- package/dist/model-gateway.js +241 -0
- package/dist/native-version.d.ts +9 -0
- package/dist/native-version.js +7 -0
- package/dist/native.d.ts +4 -0
- package/dist/native.js +1 -0
- package/dist/presentation.d.ts +17 -0
- package/dist/presentation.js +39 -0
- package/dist/projection-format.d.ts +68 -0
- package/dist/projection-format.js +118 -0
- package/dist/projection-window.d.ts +9 -0
- package/dist/projection-window.js +93 -0
- package/dist/projection.d.ts +22 -0
- package/dist/projection.js +153 -0
- package/dist/question-response.d.ts +20 -0
- package/dist/question-response.js +75 -0
- package/dist/questions.d.ts +74 -0
- package/dist/questions.js +199 -0
- package/dist/self-evolving.d.ts +55 -0
- package/dist/self-evolving.js +239 -0
- package/dist/skills.d.ts +15 -0
- package/dist/skills.js +51 -0
- package/dist/tool-evidence.d.ts +14 -0
- package/dist/tool-evidence.js +54 -0
- package/dist/validation.d.ts +38 -0
- package/dist/validation.js +68 -0
- package/dist/workspaces.d.ts +107 -0
- package/dist/workspaces.js +212 -0
- package/package.json +145 -150
- package/dist/DebugDrawer-JNA43SVR.js +0 -8
- package/dist/admin-DORVQfLn.d.ts +0 -340
- package/dist/chunk-2BGLZZ7T.js +0 -10
- package/dist/chunk-42D6KAQV.js +0 -2882
- package/dist/chunk-4XOW57KJ.js +0 -343
- package/dist/chunk-72KT6VWV.js +0 -1338
- package/dist/chunk-7XAOVFSN.js +0 -28
- package/dist/chunk-KJIEMSQP.js +0 -198
- package/dist/chunk-KN72XWP7.js +0 -394
- package/dist/chunk-N6THTH7P.js +0 -110
- package/dist/chunk-V3H5A3BR.js +0 -22501
- package/dist/chunk-YGRQ4Q5P.js +0 -20
- package/dist/chunk-YTUZFKB3.js +0 -78
- package/dist/core/index.d.ts +0 -28
- package/dist/core/index.js +0 -617
- package/dist/createHarness-DOLOHA62.js +0 -18
- package/dist/eval/index.d.ts +0 -247
- package/dist/eval/index.js +0 -541
- package/dist/file-search-BUgMSKZE.d.ts +0 -289
- package/dist/front/artifacts.d.ts +0 -34
- package/dist/front/artifacts.js +0 -21
- package/dist/front/index.d.ts +0 -1082
- package/dist/front/index.js +0 -12912
- package/dist/front/styles.css +0 -5380
- package/dist/harness-qHlMJhZo.d.ts +0 -276
- package/dist/middleware-CaAEXpi5.d.ts +0 -115
- package/dist/piChatEvent-DpE0lIb9.d.ts +0 -450
- package/dist/piChatSessionService-CBct-hmz.d.ts +0 -86
- package/dist/sandbox-BMhut3kh.d.ts +0 -317
- package/dist/server/agent-host/testing/compositionRouteProof.d.ts +0 -8
- package/dist/server/agent-host/testing/compositionRouteProof.js +0 -78
- package/dist/server/agent-host/testing/gatewayConformance.d.ts +0 -43
- package/dist/server/agent-host/testing/gatewayConformance.js +0 -605
- package/dist/server/index.d.ts +0 -2278
- package/dist/server/index.js +0 -274
- package/dist/server/pi-session-readability.d.ts +0 -12
- package/dist/server/pi-session-readability.js +0 -23
- package/dist/server/worker/index.d.ts +0 -68
- package/dist/server/worker/index.js +0 -377
- package/dist/share-entry-CxS6UJ_L.d.ts +0 -330
- package/dist/shared/index.d.ts +0 -2111
- package/dist/shared/index.js +0 -262
- package/dist/types-DcWoQMn8.d.ts +0 -1377
- package/docs/ACCESSIBILITY.md +0 -52
- package/docs/AGENT_GATEWAY_V0.md +0 -212
- package/docs/AGENT_PACKAGES.md +0 -90
- package/docs/API.md +0 -123
- package/docs/AUTHORING.md +0 -68
- package/docs/CSP.md +0 -41
- package/docs/ERROR_CODES.md +0 -229
- package/docs/KNOWN_LIMITATIONS.md +0 -100
- package/docs/MIGRATION.md +0 -50
- package/docs/PERFORMANCE.md +0 -68
- package/docs/PLUGINS.md +0 -138
- package/docs/README.md +0 -98
- package/docs/RISKS-MULTI-TAB.md +0 -46
- package/docs/SESSION_IDENTITY.md +0 -91
- package/docs/STYLING.md +0 -73
- package/docs/UI-SHADCN.md +0 -56
- package/docs/VERCEL_COSTS.md +0 -103
- package/docs/plans/archive/AGENT_EVAL_FRAMEWORK.md +0 -466
- package/docs/plans/archive/agent-package-spec.md +0 -1777
- package/docs/plans/archive/harness-followup-capabilities.md +0 -440
- package/docs/plans/archive/harness-tool-ui-capabilities.md +0 -144
- package/docs/plans/archive/pi-followup-history-projection.md +0 -229
- package/docs/plans/archive/pi-tools-migration.md +0 -1061
- package/docs/plans/archive/reviews/pi-followup-history-codex-review.md +0 -11
- package/docs/plans/archive/reviews/pi-followup-history-gpt-review.md +0 -64
- package/docs/plans/archive/reviews/pi-followup-history-opus-review.md +0 -43
- package/docs/plans/archive/reviews/pi-followup-history-xai-review.md +0 -43
- package/docs/plans/archive/vercel-base-snapshot-template-plan.md +0 -527
- package/docs/plans/archive/vercel-persistent-sandbox-adapter.md +0 -553
- package/docs/runtime-provisioning.md +0 -157
- package/docs/runtime.md +0 -296
- package/docs/tools.md +0 -102
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Extension, PromptSection, Registry } from '@earendil-works/pi-durable';
|
|
2
|
+
import type { ExecutionEnv } from '@earendil-works/pi-durable/env';
|
|
3
|
+
import type { Context } from '@earendil-works/chord';
|
|
4
|
+
import type { Skill } from './skills.js';
|
|
5
|
+
/** The folder, relative to the environment's working directory. */
|
|
6
|
+
export declare const AGENT_FOLDER = ".agent";
|
|
7
|
+
/** Largest part of `.agent/AGENTS.md` put in the prompt, in characters. */
|
|
8
|
+
export declare const MAX_AGENT_INSTRUCTIONS = 16000;
|
|
9
|
+
export interface SelfEvolutionError {
|
|
10
|
+
readonly path: string;
|
|
11
|
+
readonly message: string;
|
|
12
|
+
}
|
|
13
|
+
/** What one scan found and applied. `text` is what the `reload` tool and the person's `/reload` show. */
|
|
14
|
+
export interface SelfEvolutionReport {
|
|
15
|
+
readonly extension: string;
|
|
16
|
+
readonly tools: {
|
|
17
|
+
readonly available: boolean;
|
|
18
|
+
readonly added: string[];
|
|
19
|
+
readonly changed: string[];
|
|
20
|
+
readonly removed: string[];
|
|
21
|
+
readonly current: string[];
|
|
22
|
+
};
|
|
23
|
+
readonly skills: string[];
|
|
24
|
+
readonly instructions: {
|
|
25
|
+
readonly present: boolean;
|
|
26
|
+
readonly characters: number;
|
|
27
|
+
readonly truncated: boolean;
|
|
28
|
+
};
|
|
29
|
+
readonly errors: SelfEvolutionError[];
|
|
30
|
+
readonly text: string;
|
|
31
|
+
}
|
|
32
|
+
export interface SelfEvolutionOptions {
|
|
33
|
+
/** The native extension name, one per workspace (`self-evolving:<workspace>`). */
|
|
34
|
+
readonly name: string;
|
|
35
|
+
/** The host's skills: listed first; an agent-written skill of the same name is refused. */
|
|
36
|
+
readonly skills: readonly Skill[];
|
|
37
|
+
/** Tool names the agent already has from the host; an agent-written tool may not take one. */
|
|
38
|
+
readonly reserved: readonly string[];
|
|
39
|
+
readonly parseSkill: (markdown: string) => Skill;
|
|
40
|
+
readonly skillsExtension: (name: string, skills: readonly Skill[]) => Extension;
|
|
41
|
+
}
|
|
42
|
+
export interface SelfEvolution {
|
|
43
|
+
/** The extension as it is now (the host's skills, `reload` and the agent-written section before the first scan). */
|
|
44
|
+
readonly current: () => Extension;
|
|
45
|
+
/** Skills now offered: the host's, then the agent's. */
|
|
46
|
+
readonly skills: () => readonly Skill[];
|
|
47
|
+
/** Install the current extension and keep the registry for later reloads. */
|
|
48
|
+
readonly install: (registry: Registry) => void;
|
|
49
|
+
/** Rescan `.agent/` through `env` and replace the extension in every registry it was installed in. */
|
|
50
|
+
readonly reload: (env: ExecutionEnv | undefined, context: Context) => Promise<SelfEvolutionReport>;
|
|
51
|
+
}
|
|
52
|
+
/** The labelled agent-written section: rendered on every request from `.agent/AGENTS.md`, after the host's instructions. */
|
|
53
|
+
export declare function agentInstructionsSection(): PromptSection;
|
|
54
|
+
export declare function createSelfEvolution(options: SelfEvolutionOptions): SelfEvolution;
|
|
55
|
+
//# sourceMappingURL=self-evolving.d.ts.map
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { defineExtension, defineTool } from '@earendil-works/pi-durable';
|
|
2
|
+
import { Type } from '@earendil-works/pi-ai';
|
|
3
|
+
import { jsonSchemaTool } from './json-schema-tool.js';
|
|
4
|
+
/*
|
|
5
|
+
* Self-evolution (docs/architecture/SELF-EVOLUTION.md, laws SELF-1..4 in this package's README): an agent keeps its own standing
|
|
6
|
+
* instructions, skills and tools in the `.agent/` folder of its workspace and applies them with `reload`. Everything is read through
|
|
7
|
+
* the conversation's own ExecutionEnv (its FileSystem and, for tools, its `exec`): the host never imports or evaluates what the agent
|
|
8
|
+
* wrote. The result is one native extension per workspace, replaced in place with the native `registry.install()`.
|
|
9
|
+
*/
|
|
10
|
+
/** The folder, relative to the environment's working directory. */
|
|
11
|
+
export const AGENT_FOLDER = '.agent';
|
|
12
|
+
/** Largest part of `.agent/AGENTS.md` put in the prompt, in characters. */
|
|
13
|
+
export const MAX_AGENT_INSTRUCTIONS = 16_000;
|
|
14
|
+
const INSTRUCTIONS = `${AGENT_FOLDER}/AGENTS.md`, SKILLS = `${AGENT_FOLDER}/skills`, TOOLS = `${AGENT_FOLDER}/tools`;
|
|
15
|
+
const TOOL_NAME = /^[a-z][a-z0-9_]{0,63}$/;
|
|
16
|
+
const ARGS_END = 'BORING_AGENT_TOOL_ARGS';
|
|
17
|
+
const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
18
|
+
const list = (names) => names.length ? names.join(', ') : 'none';
|
|
19
|
+
const reason = (error) => error instanceof Error ? error.message : String(error);
|
|
20
|
+
/** The labelled agent-written section: rendered on every request from `.agent/AGENTS.md`, after the host's instructions. */
|
|
21
|
+
export function agentInstructionsSection() {
|
|
22
|
+
return {
|
|
23
|
+
key: 'agent-written',
|
|
24
|
+
render: async (input, context) => {
|
|
25
|
+
if (input.env === undefined)
|
|
26
|
+
return undefined;
|
|
27
|
+
const read = await input.env.readTextFile(INSTRUCTIONS, context);
|
|
28
|
+
if (!read.ok)
|
|
29
|
+
return undefined;
|
|
30
|
+
const text = read.value.trim();
|
|
31
|
+
if (!text)
|
|
32
|
+
return undefined;
|
|
33
|
+
const shown = text.length > MAX_AGENT_INSTRUCTIONS
|
|
34
|
+
? `${text.slice(0, MAX_AGENT_INSTRUCTIONS)}\n[Truncated: ${INSTRUCTIONS} has ${text.length} characters; only the first ${MAX_AGENT_INSTRUCTIONS} are shown.]`
|
|
35
|
+
: text;
|
|
36
|
+
return `Agent-written: standing instructions you wrote for yourself in ${INSTRUCTIONS}. They come after the host's instructions and never override them.\n\n${shown}`;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** Runs `run` through the conversation's environment with the arguments as one line of JSON on stdin; the output is the result. */
|
|
41
|
+
async function runTool(description, env, args, context) {
|
|
42
|
+
if (env === undefined || typeof env.exec !== 'function')
|
|
43
|
+
return { content: [{ type: 'text', text: `${description.name} cannot run: this conversation has no execution environment.` }], isError: true };
|
|
44
|
+
let output = '';
|
|
45
|
+
const command = `{ ${description.run}\n} <<'${ARGS_END}'\n${JSON.stringify(args ?? {})}\n${ARGS_END}`;
|
|
46
|
+
const result = await env.exec(command, { onOutput: text => { output += text; } }, context);
|
|
47
|
+
if (!result.ok)
|
|
48
|
+
return { content: [{ type: 'text', text: `${description.name} failed: ${result.error.message}${output ? `\n${output}` : ''}` }], isError: true };
|
|
49
|
+
const failed = result.value.exitCode !== 0;
|
|
50
|
+
return { content: [{ type: 'text', text: output || '(no output)' }], ...(failed ? { isError: true, diagnostics: [{ severity: 'error', message: `${description.run} exited with code ${result.value.exitCode}` }] } : {}) };
|
|
51
|
+
}
|
|
52
|
+
/** The Harness validates each call's arguments against the agent-written schema before `run` starts (see `jsonSchemaTool`). */
|
|
53
|
+
function toolOf(description) {
|
|
54
|
+
return jsonSchemaTool({
|
|
55
|
+
name: description.name, description: description.description, parameters: description.parameters,
|
|
56
|
+
execute: (args, api, context) => runTool(description, api.env, args, context),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/** Validates one tool description; returns it or a reason. Paths that `run` names under `.agent/` must exist. */
|
|
60
|
+
async function describe(env, path, text, reserved, context) {
|
|
61
|
+
let value;
|
|
62
|
+
try {
|
|
63
|
+
value = JSON.parse(text);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
return `invalid JSON: ${reason(error)}`;
|
|
67
|
+
}
|
|
68
|
+
if (!isObject(value))
|
|
69
|
+
return 'a tool description is a JSON object { name, description, parameters, run }';
|
|
70
|
+
const extra = Object.keys(value).filter(key => !['name', 'description', 'parameters', 'run'].includes(key));
|
|
71
|
+
if (extra.length)
|
|
72
|
+
return `unknown field ${extra.join(', ')}`;
|
|
73
|
+
const { name, description, parameters, run } = value;
|
|
74
|
+
if (typeof name !== 'string' || !TOOL_NAME.test(name))
|
|
75
|
+
return 'name must be snake_case: a lowercase letter, then lowercase letters, digits or _ (at most 64)';
|
|
76
|
+
if (reserved.has(name))
|
|
77
|
+
return `the name ${name} is already a tool of this agent`;
|
|
78
|
+
if (typeof description !== 'string' || !description.trim() || description.length > 2000)
|
|
79
|
+
return 'description must be a non-empty string of at most 2000 characters';
|
|
80
|
+
if (!isObject(parameters) || parameters['type'] !== 'object')
|
|
81
|
+
return 'parameters must be a JSON Schema object with "type": "object"';
|
|
82
|
+
if (parameters['properties'] !== undefined && !isObject(parameters['properties']))
|
|
83
|
+
return 'parameters.properties must be an object';
|
|
84
|
+
if (parameters['required'] !== undefined && !(Array.isArray(parameters['required']) && parameters['required'].every(item => typeof item === 'string')))
|
|
85
|
+
return 'parameters.required must be a list of property names';
|
|
86
|
+
if (typeof run !== 'string' || !run.trim() || run.length > 2000 || /[\r\n\0]/.test(run))
|
|
87
|
+
return 'run must be one command line (at most 2000 characters)';
|
|
88
|
+
for (const token of run.split(/\s+/)) {
|
|
89
|
+
const script = token.replace(/^['"]|['"]$/g, '');
|
|
90
|
+
if (!script.startsWith(`${AGENT_FOLDER}/`) && !script.startsWith(`./${AGENT_FOLDER}/`))
|
|
91
|
+
continue;
|
|
92
|
+
const found = await env.exists(script, context);
|
|
93
|
+
if (!found.ok || !found.value)
|
|
94
|
+
return `run names ${script}, which does not exist`;
|
|
95
|
+
}
|
|
96
|
+
return { name, description: description.trim(), parameters, run: run.trim(), path };
|
|
97
|
+
}
|
|
98
|
+
/** Files directly in `directory` with `suffix`, sorted; `missing` when the directory does not exist. */
|
|
99
|
+
async function filesIn(env, directory, suffix, context) {
|
|
100
|
+
const listed = await env.listDir(directory, context);
|
|
101
|
+
if (!listed.ok)
|
|
102
|
+
return listed.error.code === 'not_found' ? 'missing' : { error: listed.error.message };
|
|
103
|
+
return listed.value.filter(entry => entry.kind === 'file' && entry.name.endsWith(suffix)).map(entry => `${directory}/${entry.name}`).sort();
|
|
104
|
+
}
|
|
105
|
+
export function createSelfEvolution(options) {
|
|
106
|
+
const { name, parseSkill, skillsExtension } = options;
|
|
107
|
+
const hostSkills = [...options.skills];
|
|
108
|
+
const registries = new Set();
|
|
109
|
+
let agentSkills = [];
|
|
110
|
+
let described = new Map();
|
|
111
|
+
let queue = Promise.resolve();
|
|
112
|
+
const reloadTool = defineTool({
|
|
113
|
+
name: 'reload',
|
|
114
|
+
description: `Apply the changes you made to your own ${AGENT_FOLDER}/ folder in the workspace, then return what was added, changed and removed and every load error. `
|
|
115
|
+
+ `${INSTRUCTIONS}: standing instructions for yourself, shown after the host's instructions on every request. `
|
|
116
|
+
+ `${SKILLS}/<name>.md: skills (front matter with name and description between --- lines, then the instructions), loaded with load_skill. `
|
|
117
|
+
+ `${TOOLS}/<name>.json: your own tools, {"name": "snake_case", "description": "...", "parameters": {a JSON Schema with "type": "object"}, "run": "a command run in the workspace, for example sh ${TOOLS}/<name>.sh"}; `
|
|
118
|
+
+ 'the arguments arrive as one line of JSON on standard input and the command output is the result. Restore or delete the files and reload to undo a change.',
|
|
119
|
+
parameters: Type.Object({}, { additionalProperties: false }),
|
|
120
|
+
replay: 'safe',
|
|
121
|
+
executionMode: 'sequential',
|
|
122
|
+
execute: async (_args, api, context) => {
|
|
123
|
+
const report = await reload(api.env, context);
|
|
124
|
+
return { content: [{ type: 'text', text: report.text }], details: JSON.parse(JSON.stringify(report)) };
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
const section = agentInstructionsSection();
|
|
128
|
+
function build(tools) {
|
|
129
|
+
const skills = [...hostSkills, ...agentSkills];
|
|
130
|
+
const offered = skills.length ? skillsExtension(`${name}.skills`, skills) : undefined;
|
|
131
|
+
return defineExtension({
|
|
132
|
+
name,
|
|
133
|
+
tools: [reloadTool, ...(offered?.tools ?? []), ...tools.map(toolOf)],
|
|
134
|
+
sections: [...(offered?.sections ?? []), section],
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
let current = build([]);
|
|
138
|
+
async function scan(env, context) {
|
|
139
|
+
const errors = [];
|
|
140
|
+
// Instructions are rendered by the section on every request; the scan reports what it will show.
|
|
141
|
+
let instructions = { present: false, characters: 0, truncated: false };
|
|
142
|
+
const read = await env.readTextFile(INSTRUCTIONS, context);
|
|
143
|
+
if (read.ok) {
|
|
144
|
+
const text = read.value.trim();
|
|
145
|
+
instructions = { present: text.length > 0, characters: text.length, truncated: text.length > MAX_AGENT_INSTRUCTIONS };
|
|
146
|
+
}
|
|
147
|
+
else if (read.error.code !== 'not_found')
|
|
148
|
+
errors.push({ path: INSTRUCTIONS, message: read.error.message });
|
|
149
|
+
const skills = [];
|
|
150
|
+
const skillFiles = await filesIn(env, SKILLS, '.md', context);
|
|
151
|
+
if (typeof skillFiles === 'object' && !Array.isArray(skillFiles))
|
|
152
|
+
errors.push({ path: SKILLS, message: skillFiles.error });
|
|
153
|
+
for (const path of Array.isArray(skillFiles) ? skillFiles : []) {
|
|
154
|
+
const text = await env.readTextFile(path, context);
|
|
155
|
+
if (!text.ok) {
|
|
156
|
+
errors.push({ path, message: text.error.message });
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
let skill;
|
|
160
|
+
try {
|
|
161
|
+
skill = parseSkill(text.value);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
errors.push({ path, message: reason(error) });
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if ([...hostSkills, ...skills].some(other => other.name === skill.name)) {
|
|
168
|
+
errors.push({ path, message: `a skill named ${skill.name} already exists` });
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
skills.push(skill);
|
|
172
|
+
}
|
|
173
|
+
const available = typeof env.exec === 'function';
|
|
174
|
+
const tools = [];
|
|
175
|
+
const toolFiles = await filesIn(env, TOOLS, '.json', context);
|
|
176
|
+
if (typeof toolFiles === 'object' && !Array.isArray(toolFiles))
|
|
177
|
+
errors.push({ path: TOOLS, message: toolFiles.error });
|
|
178
|
+
const files = Array.isArray(toolFiles) ? toolFiles : [];
|
|
179
|
+
if (!available && files.length)
|
|
180
|
+
errors.push({ path: TOOLS, message: `tools are unavailable here: this environment has no exec, so ${files.length} tool description(s) were not loaded` });
|
|
181
|
+
const reserved = new Set([...options.reserved, 'reload', 'load_skill']);
|
|
182
|
+
for (const path of available ? files : []) {
|
|
183
|
+
const text = await env.readTextFile(path, context);
|
|
184
|
+
if (!text.ok) {
|
|
185
|
+
errors.push({ path, message: text.error.message });
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const result = await describe(env, path, text.value, new Set([...reserved, ...tools.map(tool => tool.name)]), context);
|
|
189
|
+
if (typeof result === 'string')
|
|
190
|
+
errors.push({ path, message: result });
|
|
191
|
+
else
|
|
192
|
+
tools.push(result);
|
|
193
|
+
}
|
|
194
|
+
return { errors, instructions, skills, tools, available };
|
|
195
|
+
}
|
|
196
|
+
async function apply(env, context) {
|
|
197
|
+
if (env === undefined) {
|
|
198
|
+
const errors = [{ path: AGENT_FOLDER, message: 'this conversation has no execution environment; nothing was changed' }];
|
|
199
|
+
return report({ available: false, added: [], changed: [], removed: [], current: [...described.keys()] }, agentSkills, { present: false, characters: 0, truncated: false }, errors);
|
|
200
|
+
}
|
|
201
|
+
const found = await scan(env, context);
|
|
202
|
+
const next = new Map(found.tools.map(tool => [tool.name, tool]));
|
|
203
|
+
const same = (a, b) => JSON.stringify([a.description, a.parameters, a.run]) === JSON.stringify([b.description, b.parameters, b.run]);
|
|
204
|
+
const added = [...next.keys()].filter(key => !described.has(key));
|
|
205
|
+
const changed = [...next.keys()].filter(key => described.has(key) && !same(described.get(key), next.get(key)));
|
|
206
|
+
const removed = [...described.keys()].filter(key => !next.has(key));
|
|
207
|
+
agentSkills = found.skills;
|
|
208
|
+
described = next;
|
|
209
|
+
current = build(found.tools);
|
|
210
|
+
// Native replacement in place: the next request of every conversation selecting this extension sees the new tools; a call
|
|
211
|
+
// already running finishes with the registration it started with.
|
|
212
|
+
for (const registry of registries)
|
|
213
|
+
registry.install(current);
|
|
214
|
+
return report({ available: found.available, added, changed, removed, current: [...next.keys()] }, found.skills, found.instructions, found.errors);
|
|
215
|
+
}
|
|
216
|
+
function report(tools, skills, instructions, errors) {
|
|
217
|
+
const lines = [
|
|
218
|
+
`Reloaded ${AGENT_FOLDER}/ (${name}).`,
|
|
219
|
+
tools.available
|
|
220
|
+
? `Tools added: ${list(tools.added)}. Changed: ${list(tools.changed)}. Removed: ${list(tools.removed)}. Now: ${list(tools.current)}.`
|
|
221
|
+
: 'Tools: unavailable in this environment (no exec).',
|
|
222
|
+
`Skills: ${list(skills.map(skill => skill.name))}.`,
|
|
223
|
+
`Instructions: ${instructions.present ? `${INSTRUCTIONS}, ${instructions.characters} characters${instructions.truncated ? ` (only the first ${MAX_AGENT_INSTRUCTIONS} are shown)` : ''}, after the host's instructions` : 'none'}.`,
|
|
224
|
+
errors.length ? `Errors (${errors.length}):\n${errors.map(error => `- ${error.path}: ${error.message}`).join('\n')}` : 'Errors: none.',
|
|
225
|
+
];
|
|
226
|
+
return { extension: name, tools, skills: skills.map(skill => skill.name), instructions, errors, text: lines.join('\n') };
|
|
227
|
+
}
|
|
228
|
+
const reload = (env, context) => {
|
|
229
|
+
const run = queue.then(() => apply(env, context));
|
|
230
|
+
queue = run.catch(() => undefined);
|
|
231
|
+
return run;
|
|
232
|
+
};
|
|
233
|
+
return {
|
|
234
|
+
current: () => current,
|
|
235
|
+
skills: () => [...hostSkills, ...agentSkills],
|
|
236
|
+
install: registry => { registries.add(registry); registry.install(current); },
|
|
237
|
+
reload,
|
|
238
|
+
};
|
|
239
|
+
}
|
package/dist/skills.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Extension } from '@earendil-works/pi-durable';
|
|
2
|
+
/** Names of skills and agents: a lowercase letter or digit, then lowercase letters, digits, `.`, `_` or `-` (at most 64). */
|
|
3
|
+
export declare const NAME: RegExp;
|
|
4
|
+
/** Instructions the agent loads on demand: only name and description occupy the system prompt. */
|
|
5
|
+
export interface Skill {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly description: string;
|
|
8
|
+
readonly body: string;
|
|
9
|
+
}
|
|
10
|
+
/** Parse a `SKILL.md`-style document: `---` front matter with `name` and `description`, then the body. */
|
|
11
|
+
export declare function parseSkill(markdown: string): Skill;
|
|
12
|
+
export declare function validSkill(skill: Skill): Skill;
|
|
13
|
+
/** A prompt section listing the skills and a `load_skill` tool returning one body. Usable without `defineAgent`. */
|
|
14
|
+
export declare function createSkillsExtension(name: string, skills: readonly Skill[]): Extension;
|
|
15
|
+
//# sourceMappingURL=skills.d.ts.map
|
package/dist/skills.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { defineExtension, defineTool } from '@earendil-works/pi-durable';
|
|
2
|
+
import { Type } from '@earendil-works/pi-ai';
|
|
3
|
+
/** Names of skills and agents: a lowercase letter or digit, then lowercase letters, digits, `.`, `_` or `-` (at most 64). */
|
|
4
|
+
export const NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
5
|
+
/** Parse a `SKILL.md`-style document: `---` front matter with `name` and `description`, then the body. */
|
|
6
|
+
export function parseSkill(markdown) {
|
|
7
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(markdown);
|
|
8
|
+
if (!match)
|
|
9
|
+
throw new TypeError('Skill needs front matter with name and description');
|
|
10
|
+
const fields = new Map();
|
|
11
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
12
|
+
const separator = line.indexOf(':');
|
|
13
|
+
if (separator > 0)
|
|
14
|
+
fields.set(line.slice(0, separator).trim(), line.slice(separator + 1).trim().replace(/^(["'])(.*)\1$/, '$2'));
|
|
15
|
+
}
|
|
16
|
+
const name = fields.get('name'), description = fields.get('description');
|
|
17
|
+
if (!name || !description)
|
|
18
|
+
throw new TypeError('Skill front matter needs name and description');
|
|
19
|
+
return validSkill({ name, description, body: match[2].trim() });
|
|
20
|
+
}
|
|
21
|
+
export function validSkill(skill) {
|
|
22
|
+
if (!NAME.test(skill.name))
|
|
23
|
+
throw new TypeError(`Invalid skill name: ${skill.name}`);
|
|
24
|
+
if (!skill.description.trim() || !skill.body.trim())
|
|
25
|
+
throw new TypeError(`Skill ${skill.name} needs a description and a body`);
|
|
26
|
+
return Object.freeze({ name: skill.name, description: skill.description.trim(), body: skill.body });
|
|
27
|
+
}
|
|
28
|
+
/** A prompt section listing the skills and a `load_skill` tool returning one body. Usable without `defineAgent`. */
|
|
29
|
+
export function createSkillsExtension(name, skills) {
|
|
30
|
+
const known = new Map();
|
|
31
|
+
for (const skill of skills.map(validSkill)) {
|
|
32
|
+
if (known.has(skill.name))
|
|
33
|
+
throw new TypeError(`Duplicate skill: ${skill.name}`);
|
|
34
|
+
known.set(skill.name, skill);
|
|
35
|
+
}
|
|
36
|
+
const listing = [...known.values()].map(skill => `- ${skill.name}: ${skill.description}`).join('\n');
|
|
37
|
+
return defineExtension({
|
|
38
|
+
name,
|
|
39
|
+
sections: [{ key: 'skills', render: () => `Before starting a task that matches a skill, call load_skill with its name and follow the returned instructions. A user message that starts with /<skill-name> naming one of these skills is an explicit request: call load_skill for that skill first and follow it for the rest of the message.\n${listing}` }],
|
|
40
|
+
tools: [defineTool({
|
|
41
|
+
name: 'load_skill',
|
|
42
|
+
description: 'Load the full instructions of one listed skill.',
|
|
43
|
+
parameters: Type.Object({ name: Type.String() }, { additionalProperties: false }),
|
|
44
|
+
replay: 'safe',
|
|
45
|
+
execute: async (args) => {
|
|
46
|
+
const skill = known.get(args.name);
|
|
47
|
+
return { content: [{ type: 'text', text: skill ? skill.body : `Unknown skill "${args.name}". Available: ${[...known.keys()].join(', ')}` }] };
|
|
48
|
+
},
|
|
49
|
+
})],
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ConversationId, EntryId, Harness, TaskId, ToolDiagnostic } from '@earendil-works/pi-durable';
|
|
2
|
+
import type { ToolCall, ToolResultMessage } from '@earendil-works/pi-ai';
|
|
3
|
+
import type { Context } from '@earendil-works/chord';
|
|
4
|
+
export interface NativeToolEvidence {
|
|
5
|
+
readonly entryId: EntryId;
|
|
6
|
+
readonly taskId: TaskId;
|
|
7
|
+
readonly assistantId: EntryId;
|
|
8
|
+
readonly conversationId: ConversationId;
|
|
9
|
+
readonly call: ToolCall;
|
|
10
|
+
readonly result: ToolResultMessage;
|
|
11
|
+
readonly diagnostics: readonly ToolDiagnostic[];
|
|
12
|
+
}
|
|
13
|
+
export declare function readToolEvidence(harness: Harness, owner: TaskId, ids: readonly EntryId[], context: Context): Promise<NativeToolEvidence[]>;
|
|
14
|
+
//# sourceMappingURL=tool-evidence.d.ts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { AssistantEntry, ToolResultEntry, ToolTask } from '@earendil-works/pi-durable';
|
|
2
|
+
function object(value) {
|
|
3
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
function entryId(value) {
|
|
6
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value > 0;
|
|
7
|
+
}
|
|
8
|
+
async function ownedBy(tx, id, owner) {
|
|
9
|
+
const visited = new Set();
|
|
10
|
+
let current = id;
|
|
11
|
+
while (current !== undefined && !visited.has(current) && visited.size < 128) {
|
|
12
|
+
if (current === owner)
|
|
13
|
+
return (await tx.task(owner)) !== undefined;
|
|
14
|
+
visited.add(current);
|
|
15
|
+
const task = await tx.task(current);
|
|
16
|
+
if (!task)
|
|
17
|
+
return false;
|
|
18
|
+
current = task.owner ?? (await tx.conversation(task.conversationId))?.owner?.taskId;
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
export async function readToolEvidence(harness, owner, ids, context) {
|
|
23
|
+
return harness.commit(async (tx) => {
|
|
24
|
+
const evidence = [];
|
|
25
|
+
for (const id of ids) {
|
|
26
|
+
const entry = await tx.entry(ToolResultEntry, id);
|
|
27
|
+
if (!entry?.byTaskId || entry.model?.length !== 1)
|
|
28
|
+
throw new Error('Missing native tool result');
|
|
29
|
+
const task = await tx.task(entry.byTaskId);
|
|
30
|
+
if (!task || task.kind !== ToolTask.definition.name || task.version !== ToolTask.definition.version
|
|
31
|
+
|| task.conversationId !== entry.conversationId || task.state.status !== 'terminal'
|
|
32
|
+
|| task.state.outcome.status !== 'completed' || !object(task.state.outcome.result)
|
|
33
|
+
|| task.state.outcome.result.entryId !== id || !object(task.input)
|
|
34
|
+
|| !entryId(task.input.assistant) || typeof task.input.callId !== 'string'
|
|
35
|
+
|| !await ownedBy(tx, task.id, owner))
|
|
36
|
+
throw new Error('Unbound native tool result');
|
|
37
|
+
const result = entry.model[0];
|
|
38
|
+
if (result?.role !== 'toolResult' || result.isError || result.toolCallId !== task.input.callId
|
|
39
|
+
|| entry.data.diagnostics.length !== 0)
|
|
40
|
+
throw new Error('Unusable native tool result');
|
|
41
|
+
const assistant = await tx.entry(AssistantEntry, task.input.assistant);
|
|
42
|
+
if (!assistant || assistant.conversationId !== entry.conversationId || assistant.model?.length !== 1
|
|
43
|
+
|| assistant.model[0]?.role !== 'assistant')
|
|
44
|
+
throw new Error('Missing native tool call');
|
|
45
|
+
const calls = assistant.model[0].content.filter(item => item.type === 'toolCall' && item.id === result.toolCallId);
|
|
46
|
+
const call = calls[0];
|
|
47
|
+
if (calls.length !== 1 || call?.type !== 'toolCall' || call.name !== result.toolName)
|
|
48
|
+
throw new Error('Mismatched native tool call');
|
|
49
|
+
evidence.push(structuredClone({ entryId: id, taskId: task.id, assistantId: assistant.id,
|
|
50
|
+
conversationId: entry.conversationId, call, result, diagnostics: entry.data.diagnostics }));
|
|
51
|
+
}
|
|
52
|
+
return evidence;
|
|
53
|
+
}, context);
|
|
54
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { EntryId, Harness, TaskId } from '@earendil-works/pi-durable';
|
|
2
|
+
import type { Context, JsonValue } from '@earendil-works/chord';
|
|
3
|
+
import type { NativeToolEvidence } from './tool-evidence.js';
|
|
4
|
+
export type { NativeToolEvidence } from './tool-evidence.js';
|
|
5
|
+
export type OutputProposal = {
|
|
6
|
+
readonly text: string;
|
|
7
|
+
readonly evidence: readonly EntryId[];
|
|
8
|
+
};
|
|
9
|
+
export type ValidationResult<T extends JsonValue> = {
|
|
10
|
+
kind: 'valid';
|
|
11
|
+
value: T;
|
|
12
|
+
} | {
|
|
13
|
+
kind: 'invalid';
|
|
14
|
+
errors: readonly string[];
|
|
15
|
+
};
|
|
16
|
+
export type OutputValidationResult<T extends JsonValue> = ValidationResult<T> | {
|
|
17
|
+
kind: 'producer-failed';
|
|
18
|
+
status: string;
|
|
19
|
+
};
|
|
20
|
+
export interface OutputValidationOptions<T extends JsonValue> {
|
|
21
|
+
readonly name: string;
|
|
22
|
+
readonly version: number;
|
|
23
|
+
readonly harness: () => Harness;
|
|
24
|
+
readonly authorize: (owner: TaskId, evidence: readonly NativeToolEvidence[], context: Context) => boolean | Promise<boolean>;
|
|
25
|
+
readonly validate: (text: string, evidence: readonly NativeToolEvidence[], context: Context) => ValidationResult<T> | Promise<ValidationResult<T>>;
|
|
26
|
+
}
|
|
27
|
+
export declare function createOutputValidation<T extends JsonValue>(options: OutputValidationOptions<T>): {
|
|
28
|
+
task: import("@earendil-works/pi-durable").Task<{
|
|
29
|
+
producer: TaskId<OutputProposal>;
|
|
30
|
+
}, {
|
|
31
|
+
phase: "wait";
|
|
32
|
+
} | {
|
|
33
|
+
phase: "validate";
|
|
34
|
+
}, OutputValidationResult<T>, object>;
|
|
35
|
+
extension: import("@earendil-works/pi-durable").Extension<import("@earendil-works/pi-durable").ToolRegistration>;
|
|
36
|
+
check: (proposal: OutputProposal, owner: TaskId, context: Context) => Promise<ValidationResult<T>>;
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { defineExtension, defineTask } from '@earendil-works/pi-durable';
|
|
2
|
+
import { isJsonValue } from '@earendil-works/chord';
|
|
3
|
+
import { readToolEvidence } from './tool-evidence.js';
|
|
4
|
+
export function createOutputValidation(options) {
|
|
5
|
+
const { name, version, harness, authorize, validate } = options;
|
|
6
|
+
if (!name || name.startsWith('pi.') || !Number.isSafeInteger(version) || version < 1)
|
|
7
|
+
throw new TypeError('A host validation name and positive version are required');
|
|
8
|
+
const invalid = (message) => ({ kind: 'invalid', errors: [message] });
|
|
9
|
+
async function check(proposal, owner, context) {
|
|
10
|
+
if (!proposal || typeof proposal.text !== 'string' || !Array.isArray(proposal.evidence) || !proposal.evidence.length || proposal.evidence.length > 64
|
|
11
|
+
|| proposal.evidence.some(id => !Number.isSafeInteger(id) || id <= 0)
|
|
12
|
+
|| new Set(proposal.evidence).size !== proposal.evidence.length)
|
|
13
|
+
return invalid('Actual distinct tool result references are required');
|
|
14
|
+
const text = proposal.text;
|
|
15
|
+
const ids = Array.from(proposal.evidence);
|
|
16
|
+
try {
|
|
17
|
+
const evidence = await readToolEvidence(harness(), owner, ids, context);
|
|
18
|
+
if (await authorize(owner, structuredClone(evidence), context) !== true)
|
|
19
|
+
return invalid('Tool evidence is not authorized');
|
|
20
|
+
const returned = await validate(text, structuredClone(evidence), context);
|
|
21
|
+
let result;
|
|
22
|
+
if (returned?.kind === 'valid') {
|
|
23
|
+
const value = returned.value;
|
|
24
|
+
if (!isJsonValue(value))
|
|
25
|
+
return invalid('Validator did not return a JSON value');
|
|
26
|
+
result = { kind: 'valid', value: structuredClone(value) };
|
|
27
|
+
}
|
|
28
|
+
else if (returned?.kind === 'invalid') {
|
|
29
|
+
const supplied = returned.errors;
|
|
30
|
+
if (!Array.isArray(supplied))
|
|
31
|
+
return invalid('Validator did not return errors');
|
|
32
|
+
const errors = Array.from(supplied);
|
|
33
|
+
if (!errors.length || errors.some(error => typeof error !== 'string'))
|
|
34
|
+
return invalid('Validator did not return errors');
|
|
35
|
+
result = { kind: 'invalid', errors };
|
|
36
|
+
}
|
|
37
|
+
else
|
|
38
|
+
return invalid('Validator did not return a valid result');
|
|
39
|
+
if (await authorize(owner, structuredClone(evidence), context) !== true)
|
|
40
|
+
return invalid('Tool evidence is no longer authorized');
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return invalid('Native tool evidence or validation is unavailable');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const task = defineTask({
|
|
48
|
+
name, version, initial: () => ({ phase: 'wait' }),
|
|
49
|
+
phases: {
|
|
50
|
+
wait: async (running, runtime, context) => {
|
|
51
|
+
await runtime.commit(() => ({ status: 'waiting', on: [running.input.producer], policy: 'allSettled', checkpoint: { phase: 'validate' } }), context);
|
|
52
|
+
},
|
|
53
|
+
validate: async (running, runtime, context) => {
|
|
54
|
+
const [outcome] = await runtime.outcomes([running.input.producer], context);
|
|
55
|
+
if (!outcome)
|
|
56
|
+
throw new Error('Native producer outcome is missing');
|
|
57
|
+
const result = outcome.status === 'completed'
|
|
58
|
+
? await check(outcome.result, running.input.producer, context)
|
|
59
|
+
: { kind: 'producer-failed', status: outcome.status };
|
|
60
|
+
await runtime.commit(() => ({ status: 'terminal', outcome: { status: 'completed', result } }), context);
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
abort: async (_running, runtime, context) => {
|
|
64
|
+
await runtime.commit(() => ({ status: 'terminal', outcome: { status: 'aborted' } }), context);
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
return { task, extension: defineExtension({ name, tasks: [task] }), check };
|
|
68
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { ConversationId, EnvTarget, Session, ToolExecutionApi } from '@earendil-works/pi-durable';
|
|
2
|
+
import type { ExecutionEnv } from '@earendil-works/pi-durable/env';
|
|
3
|
+
import type { Context } from '@earendil-works/chord';
|
|
4
|
+
import type { ResourceAccess } from '@hachej/boring-files';
|
|
5
|
+
import type { WorkspaceResourceProvider } from '@hachej/boring-files/workspace';
|
|
6
|
+
import type { GitRepository } from '@hachej/boring-files/git';
|
|
7
|
+
/** One workspace as the tools of a call see it. */
|
|
8
|
+
export interface WorkspaceBinding {
|
|
9
|
+
/** Stable workspace identity; scopes the guard's per-conversation baselines. Default: the provider id. */
|
|
10
|
+
readonly id?: string;
|
|
11
|
+
/** The workspace's single provider (`@hachej/boring-files/workspace`): its queue, reads, history and conditional writes. */
|
|
12
|
+
readonly files: WorkspaceResourceProvider;
|
|
13
|
+
/** The workspace root as the call's ExecutionEnv names it (the provider's `fs.cwd`). */
|
|
14
|
+
readonly root: string;
|
|
15
|
+
/** The ExecutionEnv Pi uses for this workspace. When present, a call whose `api.env` is another object is refused. */
|
|
16
|
+
readonly env?: ExecutionEnv;
|
|
17
|
+
/** The agent's principal in this workspace, chosen by the host when it opened it; tools use it unless they are given `resolveAccess`. */
|
|
18
|
+
readonly access?: ResourceAccess;
|
|
19
|
+
/** The workspace's working Git repository (`@hachej/boring-files/git`) over the same files, when it has one; `working_git` uses it. */
|
|
20
|
+
readonly repository?: GitRepository;
|
|
21
|
+
}
|
|
22
|
+
/** Same shape as `HarnessOptions.env`: the workspace of one call, or `undefined` when the conversation has none. */
|
|
23
|
+
export type WorkspaceResolver = (target: EnvTarget, context: Context) => WorkspaceBinding | undefined | Promise<WorkspaceBinding | undefined>;
|
|
24
|
+
/** What a tool needs a credential for: the tool, and the external server when there is one (an MCP server id). */
|
|
25
|
+
export interface CredentialRequest {
|
|
26
|
+
readonly tool: string;
|
|
27
|
+
readonly server?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The host's per-call credentials: a secret for the person behind the call, or `undefined` when they have none. Tools call it at
|
|
31
|
+
* execute time and keep the value only in memory: never in conversation state, a tool result or a log.
|
|
32
|
+
*/
|
|
33
|
+
export type CredentialResolver = (target: EnvTarget, context: Context, request: CredentialRequest) => string | undefined | Promise<string | undefined>;
|
|
34
|
+
/** The single-workspace case: every call uses `binding`. */
|
|
35
|
+
export declare function constantWorkspace(binding: WorkspaceBinding): WorkspaceResolver;
|
|
36
|
+
/** A resolver as given, or a binding wrapped as a constant resolver; nothing: the workspace of the call's env (`withWorkspace`). */
|
|
37
|
+
export declare function asWorkspaceResolver(workspace?: WorkspaceResolver | WorkspaceBinding): WorkspaceResolver | undefined;
|
|
38
|
+
/** Mark `view` as a decoration of `env` (the same workspace, for example `confinedEnv`'s): the workspace of `view` is that of `env`. Returns `view`. */
|
|
39
|
+
export declare function decorate<E extends ExecutionEnv>(env: ExecutionEnv, view: E): E;
|
|
40
|
+
/**
|
|
41
|
+
* Attach `binding` to `env`: the workspace that comes with that env. The host's `HarnessOptions.env(target, context)` returns such an
|
|
42
|
+
* env, and every tool without its own `workspace` option uses its binding, so one host function resolves both. Returns `env`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function withWorkspace<E extends ExecutionEnv>(env: E, binding: WorkspaceBinding): E;
|
|
45
|
+
/** The workspace attached to `env` (`withWorkspace`, or the env of a `createWorkspaceCache` entry), if any. */
|
|
46
|
+
export declare const workspaceOfEnv: (env: ExecutionEnv | undefined) => WorkspaceBinding | undefined;
|
|
47
|
+
/** The target Pi would pass to `HarnessOptions.env` for this call: its conversation, the agent's `cwd`, committed reads. */
|
|
48
|
+
export declare function callTarget(api: ToolExecutionApi, context: Context): Promise<EnvTarget>;
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the workspace of a tool call, or the reason it has none: through `resolver` (or one binding), or without either from the env
|
|
51
|
+
* Pi handed the call (`withWorkspace`).
|
|
52
|
+
*/
|
|
53
|
+
export declare function workspaceFor(resolver: WorkspaceResolver | WorkspaceBinding | undefined, api: ToolExecutionApi, context: Context): Promise<{
|
|
54
|
+
readonly binding: WorkspaceBinding;
|
|
55
|
+
} | {
|
|
56
|
+
readonly refused: string;
|
|
57
|
+
}>;
|
|
58
|
+
/**
|
|
59
|
+
* The conversation whose workspace a conversation uses: itself, or for a conversation a task owns (a subagent's child), the
|
|
60
|
+
* conversation of that task, transitively. Read-only; the mapping never changes, so a host may memoise it.
|
|
61
|
+
*/
|
|
62
|
+
export declare function rootConversation(session: Pick<Session, 'commit'>, id: ConversationId, context: Context): Promise<ConversationId>;
|
|
63
|
+
/** A workspace the cache opened: a binding with its env, and how to close it. */
|
|
64
|
+
export interface OpenedWorkspace extends WorkspaceBinding {
|
|
65
|
+
readonly env: ExecutionEnv;
|
|
66
|
+
readonly close?: () => void | Promise<void>;
|
|
67
|
+
}
|
|
68
|
+
export interface WorkspaceCacheOptions<W extends OpenedWorkspace = OpenedWorkspace> {
|
|
69
|
+
/** The workspace key of a call (for example the owner of the conversation), or `undefined`: the conversation has no workspace. */
|
|
70
|
+
readonly key: (target: EnvTarget, context: Context) => string | undefined | Promise<string | undefined>;
|
|
71
|
+
/** Open the workspace of `key`: one provider instance over one env. Called once per key while it stays open. */
|
|
72
|
+
readonly open: (key: string, context: Context) => W | Promise<W>;
|
|
73
|
+
/** A native cwd view over this same workspace. The workspace owner closes any resources it acquires. Other cwd values refuse without this factory. */
|
|
74
|
+
readonly atCwd?: (workspace: W, cwd: string, context: Context) => W['env'] | Promise<W['env']>;
|
|
75
|
+
/** Close a workspace this long after its last use, when nothing holds it (default 10 minutes; `Infinity`: never). */
|
|
76
|
+
readonly idleMs?: number;
|
|
77
|
+
/**
|
|
78
|
+
* Whether `key` is still in use beyond what the cache sees, checked before an idle close (for example: the harness has live
|
|
79
|
+
* tasks). Pi does not report when a call stops using its env, so a host whose tool calls can outlast `idleMs` answers here.
|
|
80
|
+
*/
|
|
81
|
+
readonly busy?: (key: string) => boolean | Promise<boolean>;
|
|
82
|
+
/** A close that failed. Must not throw. */
|
|
83
|
+
readonly onError?: (error: unknown, key: string) => void;
|
|
84
|
+
}
|
|
85
|
+
/** A borrowed workspace: `release` returns the hold and never closes the workspace itself. */
|
|
86
|
+
export interface WorkspaceLease<W extends OpenedWorkspace = OpenedWorkspace> {
|
|
87
|
+
readonly workspace: W;
|
|
88
|
+
readonly release: () => void;
|
|
89
|
+
}
|
|
90
|
+
export interface WorkspaceCache<W extends OpenedWorkspace = OpenedWorkspace> {
|
|
91
|
+
/** `HarnessOptions.env`: the env of the call's workspace. */
|
|
92
|
+
readonly env: (target: EnvTarget, context: Context) => Promise<ExecutionEnv | undefined>;
|
|
93
|
+
/** The tools' resolver: the binding of the same workspace instance as `env`. */
|
|
94
|
+
readonly workspace: (target: EnvTarget, context: Context) => Promise<W | undefined>;
|
|
95
|
+
/** Borrow the workspace of `key` (a viewer or HTTP route, after authentication): kept open until every lease is released. */
|
|
96
|
+
readonly acquire: (key: string, context: Context) => Promise<WorkspaceLease<W>>;
|
|
97
|
+
/** The keys open now. */
|
|
98
|
+
readonly keys: () => string[];
|
|
99
|
+
/** Close every workspace (host shutdown). */
|
|
100
|
+
readonly close: () => Promise<void>;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* One open workspace per key, opened on first use and closed when idle. The cache owns what `open` returns: a lease or a call
|
|
104
|
+
* borrows it, and only the cache closes it (idle, or `close()`), never a viewer that stops looking.
|
|
105
|
+
*/
|
|
106
|
+
export declare function createWorkspaceCache<W extends OpenedWorkspace = OpenedWorkspace>(options: WorkspaceCacheOptions<W>): WorkspaceCache<W>;
|
|
107
|
+
//# sourceMappingURL=workspaces.d.ts.map
|