agency-lang 0.18.0 → 0.18.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/dist/lib/cli/hostedModels.js +2 -1
- package/dist/lib/cli/mcp.js +2 -6
- package/dist/lib/preprocessors/typescriptPreprocessor.js +1 -34
- package/dist/lib/runtime/agentHome.d.ts +6 -0
- package/dist/lib/runtime/agentHome.js +14 -0
- package/dist/lib/runtime/builtinPolicies.js +30 -15
- package/dist/lib/runtime/builtins.d.ts +0 -4
- package/dist/lib/runtime/builtins.js +0 -3
- package/dist/lib/runtime/index.d.ts +1 -2
- package/dist/lib/runtime/index.js +1 -2
- package/dist/lib/runtime/policy.d.ts +8 -0
- package/dist/lib/runtime/policy.js +30 -8
- package/dist/lib/runtime/toolLoopGuards.d.ts +5 -0
- package/dist/lib/runtime/toolLoopGuards.js +6 -1
- package/dist/lib/stdlib/agentSessions.js +23 -25
- package/dist/lib/stdlib/cli.js +12 -12
- package/dist/lib/stdlib/contained.d.ts +24 -1
- package/dist/lib/stdlib/contained.js +70 -17
- package/dist/lib/stdlib/fs.d.ts +9 -1
- package/dist/lib/stdlib/fs.js +20 -8
- package/dist/lib/stdlib/llm.d.ts +11 -5
- package/dist/lib/stdlib/llm.js +15 -8
- package/dist/lib/stdlib/localModelManifest.d.ts +5 -5
- package/dist/lib/stdlib/localModelManifest.js +10 -22
- package/dist/lib/stdlib/localModels.d.ts +3 -0
- package/dist/lib/stdlib/localModels.js +50 -28
- package/dist/lib/stdlib/mcp.js +6 -5
- package/dist/lib/stdlib/oauth.js +15 -20
- package/dist/lib/stdlib/shell.d.ts +2 -2
- package/dist/lib/stdlib/skills.d.ts +0 -18
- package/dist/lib/stdlib/skills.js +0 -22
- package/dist/lib/stdlib/speech.d.ts +5 -5
- package/dist/lib/stdlib/speech.js +44 -68
- package/package.json +1 -1
- package/stdlib/docs/guide/custom-providers.md +4 -1
- package/stdlib/docs/stdlib/fs.md +6 -5
- package/stdlib/docs/stdlib/llm.md +43 -12
- package/stdlib/docs/stdlib/shell.md +1 -1
- package/stdlib/docs/stdlib/skills.md +8 -8
- package/stdlib/docs/stdlib/toolbox.md +25 -12
- package/stdlib/fs.agency +9 -4
- package/stdlib/fs.js +54 -10
- package/stdlib/llm.agency +35 -3
- package/stdlib/llm.js +187 -10
- package/stdlib/shell.agency +3 -3
- package/stdlib/shell.js +4 -4
- package/stdlib/skills.agency +8 -5
- package/stdlib/skills.js +38 -10
- package/stdlib/toolbox.agency +21 -13
- package/stdlib/toolbox.js +59 -62
- package/dist/lib/runtime/builtinTools.d.ts +0 -9
- package/dist/lib/runtime/builtinTools.js +0 -16
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { _listHostedModels, _fetchModelData, _loadModelData, } from "../stdlib/llm.js";
|
|
2
|
+
import { wholePath } from "../stdlib/contained.js";
|
|
2
3
|
// Filter predicate mirrors the Agency `filterHostedModels` in
|
|
3
4
|
// lib/agents/agency-agent/lib/modelFilters.agency (the agent can't call this TS
|
|
4
5
|
// and vice-versa) — keep the two in sync; both are unit-tested.
|
|
@@ -40,7 +41,7 @@ export function formatHostedCatalog(models) {
|
|
|
40
41
|
// doesn't mistake a baked-only list for the merged one.
|
|
41
42
|
export async function modelsList(opts, files = []) {
|
|
42
43
|
for (const file of files) {
|
|
43
|
-
const res = _loadModelData(file);
|
|
44
|
+
const res = _loadModelData(file, wholePath);
|
|
44
45
|
if (!res.ok) {
|
|
45
46
|
console.error(`Cannot load ${file}: ${res.error}`);
|
|
46
47
|
process.exitCode = 1;
|
package/dist/lib/cli/mcp.js
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
import * as os from "os";
|
|
2
1
|
import * as path from "path";
|
|
3
2
|
import { isFailure } from "../runtime/index.js";
|
|
3
|
+
import { agentHomeDir } from "../runtime/agentHome.js";
|
|
4
4
|
import { _addMcpServer, _removeMcpServer, _readMcpServersFromFile } from "../stdlib/mcp.js";
|
|
5
|
-
function agentHome() {
|
|
6
|
-
const override = process.env.AGENCY_AGENT_HOME;
|
|
7
|
-
return override ? path.resolve(override) : path.join(os.homedir(), ".agency-agent");
|
|
8
|
-
}
|
|
9
5
|
const projectFile = () => path.resolve(process.cwd(), "agency.json");
|
|
10
|
-
const globalFile = () => path.join(
|
|
6
|
+
const globalFile = () => path.join(agentHomeDir(), "settings.json");
|
|
11
7
|
const scopeFile = (o) => (o.global ? globalFile() : projectFile());
|
|
12
8
|
const scopeName = (o) => (o.global ? "global" : "project");
|
|
13
9
|
function transportSummary(config) {
|
|
@@ -374,40 +374,7 @@ export class TypescriptPreprocessor {
|
|
|
374
374
|
// TODO: Update collectSkillsInFunction to work with llm() as FunctionCall.
|
|
375
375
|
// Skills are now passed in the config object (2nd arg to llm()), but skill
|
|
376
376
|
// statements still need to be collected and merged with config skills.
|
|
377
|
-
collectSkillsInFunction(body) {
|
|
378
|
-
/* Original implementation (used PromptLiteral nodes, needs rewrite for FunctionCall):
|
|
379
|
-
let skillsUsed: Skill[] = [];
|
|
380
|
-
|
|
381
|
-
const setSkillsForPrompt = (promptNode: PromptLiteral) => {
|
|
382
|
-
promptNode.skills = skillsUsed;
|
|
383
|
-
|
|
384
|
-
if (skillsUsed.length > 0) {
|
|
385
|
-
const hasReadSkillTool = promptNode.tools?.toolNames.some(
|
|
386
|
-
(t) => t === "readSkill",
|
|
387
|
-
);
|
|
388
|
-
if (!hasReadSkillTool) {
|
|
389
|
-
promptNode.tools = promptNode.tools || {
|
|
390
|
-
type: "usesTool",
|
|
391
|
-
toolNames: [],
|
|
392
|
-
};
|
|
393
|
-
promptNode.tools.toolNames.push("readSkill");
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
skillsUsed = [];
|
|
398
|
-
};
|
|
399
|
-
|
|
400
|
-
for (const { node } of walkNodesArray(body)) {
|
|
401
|
-
if (node.type === "skill") {
|
|
402
|
-
skillsUsed.push(node);
|
|
403
|
-
} else if (node.type === "prompt") {
|
|
404
|
-
setSkillsForPrompt(node);
|
|
405
|
-
} else if (node.type === "assignment" && node.value.type === "prompt") {
|
|
406
|
-
setSkillsForPrompt(node.value);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
*/
|
|
410
|
-
}
|
|
377
|
+
collectSkillsInFunction(body) { }
|
|
411
378
|
findChildren(body, type) {
|
|
412
379
|
const children = [];
|
|
413
380
|
for (const { node } of walkNodesArray(body)) {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** The agent home directory: `AGENCY_AGENT_HOME`, or `~/.agency-agent`.
|
|
2
|
+
* An empty variable counts as unset, so a set-but-blank value never
|
|
3
|
+
* turns the home into the current directory. A relative override is
|
|
4
|
+
* resolved against the process cwd, the way the `--agent-home` launcher
|
|
5
|
+
* resolves it. */
|
|
6
|
+
export declare function agentHomeDir(): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import path from "path";
|
|
3
|
+
/** The agent home directory: `AGENCY_AGENT_HOME`, or `~/.agency-agent`.
|
|
4
|
+
* An empty variable counts as unset, so a set-but-blank value never
|
|
5
|
+
* turns the home into the current directory. A relative override is
|
|
6
|
+
* resolved against the process cwd, the way the `--agent-home` launcher
|
|
7
|
+
* resolves it. */
|
|
8
|
+
export function agentHomeDir() {
|
|
9
|
+
const override = process.env.AGENCY_AGENT_HOME;
|
|
10
|
+
if (override !== undefined && override !== "") {
|
|
11
|
+
return path.resolve(override);
|
|
12
|
+
}
|
|
13
|
+
return path.join(os.homedir(), ".agency-agent");
|
|
14
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AGENCY_INSTALL_DIR_PLACEHOLDER as INSTALL, escapeGlob, } from "./policy.js";
|
|
1
|
+
import { AGENCY_INSTALL_DIR_PLACEHOLDER as INSTALL, AGENT_HOME_PLACEHOLDER as AGENT_HOME, escapeGlob, } from "./policy.js";
|
|
2
2
|
// Read-only `agency` subcommands the code agent runs via its exec-based
|
|
3
3
|
// `agencyCli` tool. Matched on command + subcommand (no shell chaining);
|
|
4
4
|
// other subcommands (run, compile, ...) still prompt.
|
|
@@ -22,21 +22,35 @@ function agencyExecApproveRules() {
|
|
|
22
22
|
}));
|
|
23
23
|
}
|
|
24
24
|
const approve = [{ action: "approve" }];
|
|
25
|
-
// Where the read-only file tools may look without asking
|
|
26
|
-
// directory
|
|
27
|
-
// and bundled skills are plain reads of
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
25
|
+
// Where the read-only file tools may look without asking. The launch
|
|
26
|
+
// directory. The agency install itself, because the agent's docs tools
|
|
27
|
+
// (`agencyGuide` and friends) and bundled skills are plain reads of
|
|
28
|
+
// shipped files. The agent home's learned skills and tools, which only
|
|
29
|
+
// enter those directories through a review interrupt. All three are
|
|
30
|
+
// placeholders the matcher expands at match time (`.` to the process cwd,
|
|
31
|
+
// `<agency>` to the package root, `<agent-home>` to the agent home, see
|
|
32
|
+
// docs/dev/agents/approval-policies.md), so a saved copy of this policy
|
|
33
|
+
// keeps meaning "wherever the agent runs, wherever agency is installed
|
|
34
|
+
// now". Reads anywhere else fall through: a prompt in an interactive
|
|
35
|
+
// session, an automatic rejection in a headless one.
|
|
34
36
|
export function readScopeRules() {
|
|
35
37
|
return [
|
|
36
38
|
{ match: { dir: "{.,./**}" }, action: "approve" },
|
|
37
39
|
{ match: { dir: `{${INSTALL}/stdlib/**,${INSTALL}/dist/**}` }, action: "approve" },
|
|
40
|
+
{
|
|
41
|
+
match: {
|
|
42
|
+
dir: `{${AGENT_HOME}/skills,${AGENT_HOME}/skills/**,${AGENT_HOME}/tools,${AGENT_HOME}/tools/**}`,
|
|
43
|
+
},
|
|
44
|
+
action: "approve",
|
|
45
|
+
},
|
|
38
46
|
];
|
|
39
47
|
}
|
|
48
|
+
// runTool's use count in a tool's meta.json. An effect of its own, never
|
|
49
|
+
// a std::write rule on the file; docs/dev/agents/approval-policies.md
|
|
50
|
+
// says why.
|
|
51
|
+
function toolboxRecordUseRules() {
|
|
52
|
+
return [{ match: { dir: `${AGENT_HOME}/tools/**` }, action: "approve" }];
|
|
53
|
+
}
|
|
40
54
|
export const minimalAutoApprovePolicy = {
|
|
41
55
|
"std::memory::remember": approve,
|
|
42
56
|
"std::memory::forget": approve,
|
|
@@ -66,11 +80,12 @@ export const recommendedAutoApprovePolicy = {
|
|
|
66
80
|
"std::weather": approve,
|
|
67
81
|
"std::search": approve,
|
|
68
82
|
"std::tavilySearch": approve,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
83
|
+
// A scan reads every skill, command, or tool file under a directory,
|
|
84
|
+
// so it takes the read scope.
|
|
85
|
+
"std::skills::skillsDir": readScopeRules(),
|
|
86
|
+
"std::skills::commandsDir": readScopeRules(),
|
|
73
87
|
"std::toolbox::scan": readScopeRules(),
|
|
88
|
+
"std::toolbox::recordUse": toolboxRecordUseRules(),
|
|
74
89
|
"std::notify": approve,
|
|
75
90
|
"std::clipboardCopy": approve,
|
|
76
91
|
"std::git::status": approve,
|
|
@@ -126,7 +141,7 @@ export const approveAllPolicy = {
|
|
|
126
141
|
export const BUILTIN_POLICIES = [
|
|
127
142
|
{
|
|
128
143
|
name: "recommended",
|
|
129
|
-
description: "Auto-approve reads under the current directory
|
|
144
|
+
description: "Auto-approve reads under the current directory, the agency install's own docs and skills, and the agent home's learned skills and tools (plus the toolbox use count there), and web/search; prompt for reads elsewhere, writes, shell, and git changes.",
|
|
130
145
|
},
|
|
131
146
|
{
|
|
132
147
|
name: "minimal",
|
|
@@ -36,8 +36,7 @@ export type { FuncParam, CallType, ToolDefinition, AgencyFunctionOpts } from "./
|
|
|
36
36
|
export { __call, __callMethod } from "./call.js";
|
|
37
37
|
export { callHook, registerGlobalHook } from "./hooks.js";
|
|
38
38
|
export type { AgencyCallbacks, CallbackMap, CallbackReturn } from "./hooks.js";
|
|
39
|
-
export { head, tail, empty, builtinRead, builtinSleep
|
|
40
|
-
export { readSkillTool, readSkillToolParams } from "./builtinTools.js";
|
|
39
|
+
export { head, tail, empty, builtinRead, builtinSleep } from "./builtins.js";
|
|
41
40
|
export { interrupt, isInterrupt, hasInterrupts, reportUnhandledInterrupts, isDebugger, isRejected, isApproved, approve, reject, pass, interruptWithHandlers, respondToInterrupts, respondToInterruptsForServe, } from "./interrupts.js";
|
|
42
41
|
export { checkPolicy, checkPolicyExplicit, validatePolicy } from "./policy.js";
|
|
43
42
|
export { resolveCliInterrupts } from "./cliInterruptResolution.js";
|
|
@@ -25,8 +25,7 @@ export { functionRefReviver } from "./revivers/index.js";
|
|
|
25
25
|
export { AgencyFunction, UNSET } from "./agencyFunction.js";
|
|
26
26
|
export { __call, __callMethod } from "./call.js";
|
|
27
27
|
export { callHook, registerGlobalHook } from "./hooks.js";
|
|
28
|
-
export { head, tail, empty, builtinRead, builtinSleep
|
|
29
|
-
export { readSkillTool, readSkillToolParams } from "./builtinTools.js";
|
|
28
|
+
export { head, tail, empty, builtinRead, builtinSleep } from "./builtins.js";
|
|
30
29
|
export { interrupt, isInterrupt, hasInterrupts, reportUnhandledInterrupts, isDebugger, isRejected, isApproved, approve, reject, pass, interruptWithHandlers, respondToInterrupts, respondToInterruptsForServe, } from "./interrupts.js";
|
|
31
30
|
export { checkPolicy, checkPolicyExplicit, validatePolicy } from "./policy.js";
|
|
32
31
|
export { resolveCliInterrupts } from "./cliInterruptResolution.js";
|
|
@@ -56,6 +56,14 @@ export declare function resolveDotDirPattern(pattern: string, cwd?: string): str
|
|
|
56
56
|
* path of one machine or one version. */
|
|
57
57
|
export declare const AGENCY_INSTALL_DIR_PLACEHOLDER = "<agency>";
|
|
58
58
|
export declare function expandAgencyInstallDir(pattern: string, root?: () => string): string;
|
|
59
|
+
/** In a `dir` pattern, `<agent-home>` stands for the agent home directory
|
|
60
|
+
* (`AGENCY_AGENT_HOME`, or `~/.agency-agent`). The built-in read scope
|
|
61
|
+
* uses it for the learned skills and tools directories, so a saved policy
|
|
62
|
+
* keeps meaning "wherever the agent home is now". */
|
|
63
|
+
export declare const AGENT_HOME_PLACEHOLDER = "<agent-home>";
|
|
64
|
+
/** Expand `<agent-home>` at match time, like `<agency>`. The home is
|
|
65
|
+
* escaped so a path containing glob or brace characters stays literal. */
|
|
66
|
+
export declare function expandAgentHomeDir(pattern: string, home?: () => string): string;
|
|
59
67
|
export declare function validatePolicy(policy: any): {
|
|
60
68
|
success: boolean;
|
|
61
69
|
error?: string;
|
|
@@ -2,6 +2,8 @@ import picomatch from "picomatch";
|
|
|
2
2
|
import { realpathSync } from "fs";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { getPackageRoot } from "../importPaths.js";
|
|
5
|
+
import { agentHomeDir } from "./agentHome.js";
|
|
6
|
+
import { root } from "../stdlib/contained.js";
|
|
5
7
|
export const PolicyRuleSchema = z
|
|
6
8
|
.object({
|
|
7
9
|
match: z.record(z.string(), z.string()).optional(),
|
|
@@ -75,9 +77,6 @@ function stripDotSlash(s) {
|
|
|
75
77
|
// whose name contains glob characters (say `v*1`) would widen the rule to
|
|
76
78
|
// its siblings — a safety boundary, so the substituted prefix must match
|
|
77
79
|
// itself only. Glob syntax stays live only in the user-written suffix.
|
|
78
|
-
function escapeForGlob(s) {
|
|
79
|
-
return s.replace(/[\\*?[\]{}()!+@|]/g, "\\$&");
|
|
80
|
-
}
|
|
81
80
|
// In a `dir` pattern, `.` also means "wherever the agent was launched".
|
|
82
81
|
// Tools absolutize the dir they put in interrupt data, so a literal `.` in
|
|
83
82
|
// a policy file could never match those; resolving it lets a static policy
|
|
@@ -103,7 +102,7 @@ export function resolveDotDirPattern(pattern, cwd = process.cwd()) {
|
|
|
103
102
|
}
|
|
104
103
|
// Callback, not a replacement string: a legal cwd containing `$&`/`$'`
|
|
105
104
|
// would otherwise be interpreted as replacement-string syntax.
|
|
106
|
-
return pattern.replace(/(^|\{|,)\.(?=$|\/|,|\})/g, (_match, prefix) => prefix +
|
|
105
|
+
return pattern.replace(/(^|\{|,)\.(?=$|\/|,|\})/g, (_match, prefix) => prefix + escapeGlob(realCwd));
|
|
107
106
|
}
|
|
108
107
|
/** In a `dir` pattern, `<agency>` stands for the directory the agency
|
|
109
108
|
* package is installed in. A rule approving reads of the shipped docs and
|
|
@@ -124,7 +123,30 @@ export function expandAgencyInstallDir(pattern, root = getPackageRoot) {
|
|
|
124
123
|
catch {
|
|
125
124
|
return pattern;
|
|
126
125
|
}
|
|
127
|
-
return pattern.split(AGENCY_INSTALL_DIR_PLACEHOLDER).join(
|
|
126
|
+
return pattern.split(AGENCY_INSTALL_DIR_PLACEHOLDER).join(escapeGlob(resolved));
|
|
127
|
+
}
|
|
128
|
+
/** In a `dir` pattern, `<agent-home>` stands for the agent home directory
|
|
129
|
+
* (`AGENCY_AGENT_HOME`, or `~/.agency-agent`). The built-in read scope
|
|
130
|
+
* uses it for the learned skills and tools directories, so a saved policy
|
|
131
|
+
* keeps meaning "wherever the agent home is now". */
|
|
132
|
+
export const AGENT_HOME_PLACEHOLDER = "<agent-home>";
|
|
133
|
+
/** Expand `<agent-home>` at match time, like `<agency>`. The home is
|
|
134
|
+
* escaped so a path containing glob or brace characters stays literal. */
|
|
135
|
+
export function expandAgentHomeDir(pattern, home = canonicalAgentHome) {
|
|
136
|
+
if (!pattern.includes(AGENT_HOME_PLACEHOLDER))
|
|
137
|
+
return pattern;
|
|
138
|
+
return pattern.split(AGENT_HOME_PLACEHOLDER).join(escapeGlob(home()));
|
|
139
|
+
}
|
|
140
|
+
/** The real spelling of the agent home, the spelling file effects put in
|
|
141
|
+
* their payloads. A home that does not exist yet keeps a lexical tail. */
|
|
142
|
+
function canonicalAgentHome() {
|
|
143
|
+
const home = agentHomeDir();
|
|
144
|
+
try {
|
|
145
|
+
return root(home).real;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return home;
|
|
149
|
+
}
|
|
128
150
|
}
|
|
129
151
|
function matchesRule(rule, interrupt) {
|
|
130
152
|
if (!rule.match)
|
|
@@ -151,11 +173,11 @@ function matchesRule(rule, interrupt) {
|
|
|
151
173
|
const viaDot = !raw &&
|
|
152
174
|
key === "dir" &&
|
|
153
175
|
picomatch.isMatch(stripDotSlash(value), stripDotSlash(resolveDotDirPattern(pattern)));
|
|
154
|
-
const
|
|
176
|
+
const viaPlaceholders = !raw &&
|
|
155
177
|
!viaDot &&
|
|
156
178
|
key === "dir" &&
|
|
157
|
-
picomatch.isMatch(stripDotSlash(value), expandAgencyInstallDir(pattern));
|
|
158
|
-
if (!raw && !viaDot && !
|
|
179
|
+
picomatch.isMatch(stripDotSlash(value), expandAgentHomeDir(expandAgencyInstallDir(pattern)));
|
|
180
|
+
if (!raw && !viaDot && !viaPlaceholders) {
|
|
159
181
|
return false;
|
|
160
182
|
}
|
|
161
183
|
}
|
|
@@ -20,6 +20,11 @@ export declare const DEFAULT_MAX_REPEATED_TOOL_CALLS = 3;
|
|
|
20
20
|
* a transcript or XML tool may legitimately be given. */
|
|
21
21
|
export declare function markupArgument(args: Record<string, unknown>, params: readonly FuncParam[]): string | null;
|
|
22
22
|
export declare function markupArgumentMessage(toolName: string, argument: string): string;
|
|
23
|
+
/** The key is stored on the checkpoint, and checkpoints are kept as JSON
|
|
24
|
+
* in places that reject U+0000 (Postgres jsonb, for one), so the two parts
|
|
25
|
+
* are joined with a colon. A namespaced tool name has colons of its own,
|
|
26
|
+
* but the digest is always 64 hex characters, so two different calls can
|
|
27
|
+
* never produce the same key. */
|
|
23
28
|
export declare function repeatKey(toolName: string, args: Record<string, unknown>): string;
|
|
24
29
|
/** The current run of identical calls: one record, because only calls in a
|
|
25
30
|
* row count. Any other call, a different result, or a refusal resets it. */
|
|
@@ -66,8 +66,13 @@ function canonicalJson(value) {
|
|
|
66
66
|
function digest(text) {
|
|
67
67
|
return createHash("sha256").update(text).digest("hex");
|
|
68
68
|
}
|
|
69
|
+
/** The key is stored on the checkpoint, and checkpoints are kept as JSON
|
|
70
|
+
* in places that reject U+0000 (Postgres jsonb, for one), so the two parts
|
|
71
|
+
* are joined with a colon. A namespaced tool name has colons of its own,
|
|
72
|
+
* but the digest is always 64 hex characters, so two different calls can
|
|
73
|
+
* never produce the same key. */
|
|
69
74
|
export function repeatKey(toolName, args) {
|
|
70
|
-
return `${toolName}
|
|
75
|
+
return `${toolName}:${digest(canonicalJson(args))}`;
|
|
71
76
|
}
|
|
72
77
|
export function freshRepeatStreak() {
|
|
73
78
|
return { key: "", result: "", count: 0 };
|
|
@@ -1,28 +1,22 @@
|
|
|
1
|
-
import
|
|
2
|
-
import * as path from "path";
|
|
1
|
+
import { root, list, stat, mkdir, readText, writeText } from "./contained.js";
|
|
3
2
|
import { __call } from "../runtime/call.js";
|
|
4
3
|
import { checkpoint, getCheckpoint } from "../runtime/checkpoint.js";
|
|
5
4
|
import { Checkpoint } from "../runtime/state/checkpointStore.js";
|
|
6
5
|
import { _contentToString } from "./threads.js";
|
|
7
6
|
const META_SUFFIX = ".meta.json";
|
|
8
|
-
function checkpointFile(
|
|
9
|
-
return
|
|
7
|
+
function checkpointFile(id) {
|
|
8
|
+
return `${id}.json`;
|
|
10
9
|
}
|
|
11
|
-
function metaFile(
|
|
12
|
-
return
|
|
10
|
+
function metaFile(id) {
|
|
11
|
+
return `${id}${META_SUFFIX}`;
|
|
13
12
|
}
|
|
14
|
-
/**
|
|
15
|
-
*
|
|
16
|
-
function
|
|
17
|
-
const tmp = `${file}.tmp-${process.pid}`;
|
|
18
|
-
fs.writeFileSync(tmp, data, "utf8");
|
|
19
|
-
fs.renameSync(tmp, file);
|
|
20
|
-
}
|
|
21
|
-
function readJson(file) {
|
|
22
|
-
if (!fs.existsSync(file))
|
|
23
|
-
return null;
|
|
13
|
+
/** The parsed JSON of `name` under `dir`, or null when it is missing or
|
|
14
|
+
* malformed. */
|
|
15
|
+
function readJson(dir, name) {
|
|
24
16
|
try {
|
|
25
|
-
|
|
17
|
+
if (stat(dir, name) === null)
|
|
18
|
+
return null;
|
|
19
|
+
return JSON.parse(readText(dir, name));
|
|
26
20
|
}
|
|
27
21
|
catch {
|
|
28
22
|
return null;
|
|
@@ -45,13 +39,14 @@ function isRecord(value) {
|
|
|
45
39
|
/** Every session in `dir`, most recently active first. A malformed
|
|
46
40
|
* record file is skipped. */
|
|
47
41
|
export function _listSessions(dir) {
|
|
48
|
-
|
|
42
|
+
const sessions = root(dir);
|
|
43
|
+
if (stat(sessions, ".") === null)
|
|
49
44
|
return [];
|
|
50
45
|
const records = [];
|
|
51
|
-
for (const
|
|
52
|
-
if (!name.endsWith(META_SUFFIX))
|
|
46
|
+
for (const entry of list(sessions, ".")) {
|
|
47
|
+
if (entry.type !== "file" || !entry.name.endsWith(META_SUFFIX))
|
|
53
48
|
continue;
|
|
54
|
-
const parsed = readJson(
|
|
49
|
+
const parsed = readJson(sessions, entry.name);
|
|
55
50
|
if (isRecord(parsed))
|
|
56
51
|
records.push(parsed);
|
|
57
52
|
}
|
|
@@ -62,9 +57,12 @@ export function _listSessions(dir) {
|
|
|
62
57
|
* error message. */
|
|
63
58
|
export function _saveSession(dir, record, checkpoint) {
|
|
64
59
|
try {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
60
|
+
const sessions = root(dir);
|
|
61
|
+
mkdir(sessions, ".");
|
|
62
|
+
// writeText renames a finished sibling over the target, so a crash
|
|
63
|
+
// mid-write never leaves a half-written file.
|
|
64
|
+
writeText(sessions, checkpointFile(record.id), JSON.stringify(checkpoint));
|
|
65
|
+
writeText(sessions, metaFile(record.id), JSON.stringify(record));
|
|
68
66
|
return "";
|
|
69
67
|
}
|
|
70
68
|
catch (err) {
|
|
@@ -73,7 +71,7 @@ export function _saveSession(dir, record, checkpoint) {
|
|
|
73
71
|
}
|
|
74
72
|
/** The parsed checkpoint, or null when the file is missing or malformed. */
|
|
75
73
|
export function _readCheckpointFile(dir, id) {
|
|
76
|
-
return readJson(
|
|
74
|
+
return readJson(root(dir), checkpointFile(id));
|
|
77
75
|
}
|
|
78
76
|
/**
|
|
79
77
|
* The user and assistant messages of a saved session's conversation
|
package/dist/lib/stdlib/cli.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as readline from "readline";
|
|
2
2
|
import process from "process";
|
|
3
|
-
import {
|
|
4
|
-
import { dirname } from "path";
|
|
3
|
+
import { wholePath, stat, readText, writeText, mkdir } from "./contained.js";
|
|
5
4
|
import { __call } from "../runtime/call.js";
|
|
6
5
|
import { getRuntimeContext } from "../runtime/asyncContext.js";
|
|
7
6
|
import { RESET, styles } from "../utils/termcolors.js";
|
|
@@ -53,10 +52,13 @@ function summarizeMultiline(text) {
|
|
|
53
52
|
* startup. */
|
|
54
53
|
function loadHistory(file, max) {
|
|
55
54
|
const empty = { entries: [], expansions: {} };
|
|
56
|
-
if (!file
|
|
55
|
+
if (!file)
|
|
57
56
|
return empty;
|
|
58
57
|
try {
|
|
59
|
-
const
|
|
58
|
+
const located = wholePath(file);
|
|
59
|
+
if (stat(located.root, located.target) === null)
|
|
60
|
+
return empty;
|
|
61
|
+
const parsed = JSON.parse(readText(located.root, located.target));
|
|
60
62
|
if (!Array.isArray(parsed))
|
|
61
63
|
return empty;
|
|
62
64
|
const entries = [];
|
|
@@ -87,19 +89,17 @@ function saveHistory(file, history, max, expansions = {}) {
|
|
|
87
89
|
if (!file)
|
|
88
90
|
return;
|
|
89
91
|
try {
|
|
90
|
-
|
|
92
|
+
const located = wholePath(file);
|
|
93
|
+
mkdir(located.root, ".");
|
|
91
94
|
const oldestFirst = history.slice(0, max).slice().reverse();
|
|
92
95
|
const records = oldestFirst.map((entry) => Object.prototype.hasOwnProperty.call(expansions, entry)
|
|
93
96
|
? { preview: entry, text: expansions[entry] }
|
|
94
97
|
: entry);
|
|
95
98
|
const data = JSON.stringify(records, null, 2) + "\n";
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
const tmp = `${file}.tmp-${process.pid}`;
|
|
101
|
-
writeFileSync(tmp, data, "utf8");
|
|
102
|
-
renameSync(tmp, file);
|
|
99
|
+
// writeText replaces the file by renaming a finished sibling over it, so
|
|
100
|
+
// a crash mid-write cannot leave half-written JSON that would parse to
|
|
101
|
+
// empty and wipe the user's history on the next load.
|
|
102
|
+
writeText(located.root, located.target, data);
|
|
103
103
|
}
|
|
104
104
|
catch {
|
|
105
105
|
// Ignore — best-effort persistence.
|
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every file operation the standard library performs on a path an Agency
|
|
3
|
+
* program chose goes through this module. The rule it enforces: under an
|
|
4
|
+
* approval that names directory D, no byte is read from or written to any
|
|
5
|
+
* path outside D. A symlink in the caller's own spelling of D (a linked
|
|
6
|
+
* /tmp, a linked home) resolves normally, once, in `root`. A symlink at
|
|
7
|
+
* any component below D is refused, because the approver never named
|
|
8
|
+
* where it points.
|
|
9
|
+
*
|
|
10
|
+
* How strong that rule is depends on the operation. Reads and writes,
|
|
11
|
+
* `readStream`, and `copy`, which is built on them, move bytes only through a
|
|
12
|
+
* descriptor that is validated after it is open, so a directory swapped
|
|
13
|
+
* to a link while the operation runs cannot redirect them. `list`,
|
|
14
|
+
* `stat`, `mkdir`, `remove`, and `move` act on a pathname that was
|
|
15
|
+
* checked a moment earlier. Node has no openat, so a swap between that
|
|
16
|
+
* check and the action is not closed here. Process containment is the
|
|
17
|
+
* answer for that window. See docs/dev/stdlib/contained-files.md.
|
|
18
|
+
*/
|
|
19
|
+
import fs from "fs";
|
|
1
20
|
import type { Stats } from "fs";
|
|
2
21
|
/** A directory an approval named, realpathed once. Every operation in this
|
|
3
22
|
* module takes one. Nothing takes a bare string root. */
|
|
@@ -57,6 +76,10 @@ export declare function readText(root: Root, target: string, seams?: Seams): str
|
|
|
57
76
|
* inside the root, and require the same (dev, ino) as the descriptor so a
|
|
58
77
|
* swap undone after the open is caught too. */
|
|
59
78
|
export declare function readBytes(root: Root, target: string, seams?: Seams): Buffer;
|
|
79
|
+
/** A read stream over a validated descriptor, for a file too large to
|
|
80
|
+
* buffer, such as a downloaded model being hashed. The stream owns the
|
|
81
|
+
* descriptor and closes it when it ends. */
|
|
82
|
+
export declare function readStream(root: Root, target: string, seams?: Seams): fs.ReadStream;
|
|
60
83
|
export declare function writeText(root: Root, target: string, content: string, options?: WriteOptions): void;
|
|
61
84
|
export declare function writeBytes(root: Root, target: string, data: Buffer, options?: WriteOptions): void;
|
|
62
85
|
export type Entry = {
|
|
@@ -83,6 +106,6 @@ export declare function copy(from: Located, to: Located): void;
|
|
|
83
106
|
export declare function move(from: Located, to: Located): void;
|
|
84
107
|
/** Every operation this module performs. The symlink battery runs each
|
|
85
108
|
* one; adding an operation without a row here fails the registry test. */
|
|
86
|
-
export declare const PRIMITIVES: readonly ["readText", "readBytes", "writeText", "writeBytes", "list", "stat", "mkdir", "remove", "copy", "move"];
|
|
109
|
+
export declare const PRIMITIVES: readonly ["readText", "readBytes", "readStream", "writeText", "writeBytes", "list", "stat", "mkdir", "remove", "copy", "move"];
|
|
87
110
|
/** Exports that are not operations: they resolve, they do not touch bytes. */
|
|
88
111
|
export declare const HELPERS: readonly ["root", "fixedRoot", "resolveUnder", "wholePath", "fixedPath", "isContained", "_realDir", "_realTarget"];
|