@corenel/harness 0.1.0 → 0.3.2
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/acp/agentMap.d.ts +34 -0
- package/dist/acp/agentMap.d.ts.map +1 -0
- package/dist/acp/agentMap.js +110 -0
- package/dist/acp/agentMap.js.map +1 -0
- package/dist/acp/index.d.ts +4 -0
- package/dist/acp/index.d.ts.map +1 -0
- package/dist/acp/index.js +6 -0
- package/dist/acp/index.js.map +1 -0
- package/dist/acp/policyMap.d.ts +55 -0
- package/dist/acp/policyMap.d.ts.map +1 -0
- package/dist/acp/policyMap.js +121 -0
- package/dist/acp/policyMap.js.map +1 -0
- package/dist/acp/types.d.ts +41 -0
- package/dist/acp/types.d.ts.map +1 -0
- package/dist/acp/types.js +14 -0
- package/dist/acp/types.js.map +1 -0
- package/dist/api/backend.d.ts +39 -0
- package/dist/api/backend.d.ts.map +1 -0
- package/dist/api/backend.js +126 -0
- package/dist/api/backend.js.map +1 -0
- package/dist/api/client.d.ts +95 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +212 -0
- package/dist/api/client.js.map +1 -0
- package/dist/api/fileMirrorSource.d.ts +24 -0
- package/dist/api/fileMirrorSource.d.ts.map +1 -0
- package/dist/api/fileMirrorSource.js +59 -0
- package/dist/api/fileMirrorSource.js.map +1 -0
- package/dist/api/mirror.d.ts +102 -0
- package/dist/api/mirror.d.ts.map +1 -0
- package/dist/api/mirror.js +204 -0
- package/dist/api/mirror.js.map +1 -0
- package/dist/api/mirrorScheduler.d.ts +56 -0
- package/dist/api/mirrorScheduler.d.ts.map +1 -0
- package/dist/api/mirrorScheduler.js +167 -0
- package/dist/api/mirrorScheduler.js.map +1 -0
- package/dist/conversation.d.ts +0 -11
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +1 -24
- package/dist/conversation.js.map +1 -1
- package/dist/core/budget.d.ts +40 -0
- package/dist/core/budget.d.ts.map +1 -0
- package/dist/core/budget.js +62 -0
- package/dist/core/budget.js.map +1 -0
- package/dist/core/loop.d.ts +11 -0
- package/dist/core/loop.d.ts.map +1 -1
- package/dist/core/loop.js +89 -20
- package/dist/core/loop.js.map +1 -1
- package/dist/core/pricing.d.ts +8 -0
- package/dist/core/pricing.d.ts.map +1 -0
- package/dist/core/pricing.js +12 -0
- package/dist/core/pricing.js.map +1 -0
- package/dist/core/recovery.d.ts +60 -0
- package/dist/core/recovery.d.ts.map +1 -0
- package/dist/core/recovery.js +128 -0
- package/dist/core/recovery.js.map +1 -0
- package/dist/guardrail/builtins.js +6 -6
- package/dist/guardrail/builtins.js.map +1 -1
- package/dist/guardrail/permission-service.js +1 -1
- package/dist/guardrail/permission-service.js.map +1 -1
- package/dist/guardrail/serialize.d.ts.map +1 -1
- package/dist/guardrail/serialize.js +6 -0
- package/dist/guardrail/serialize.js.map +1 -1
- package/dist/guardrail/validate.d.ts.map +1 -1
- package/dist/guardrail/validate.js +13 -0
- package/dist/guardrail/validate.js.map +1 -1
- package/dist/install/lockfile.d.ts +79 -0
- package/dist/install/lockfile.d.ts.map +1 -0
- package/dist/install/lockfile.js +155 -0
- package/dist/install/lockfile.js.map +1 -0
- package/dist/install/store.d.ts +13 -0
- package/dist/install/store.d.ts.map +1 -0
- package/dist/install/store.js +38 -0
- package/dist/install/store.js.map +1 -0
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +6 -1
- package/dist/log.js.map +1 -1
- package/dist/prompts/host.d.ts +4 -0
- package/dist/prompts/host.d.ts.map +1 -1
- package/dist/prompts/host.js.map +1 -1
- package/dist/prompts/index.d.ts +6 -0
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +16 -6
- package/dist/prompts/index.js.map +1 -1
- package/dist/prompts/library/defaults.generated.d.ts.map +1 -1
- package/dist/prompts/library/defaults.generated.js +22 -4
- package/dist/prompts/library/defaults.generated.js.map +1 -1
- package/dist/prompts/toolsMarker.d.ts +13 -0
- package/dist/prompts/toolsMarker.d.ts.map +1 -0
- package/dist/prompts/toolsMarker.js +21 -0
- package/dist/prompts/toolsMarker.js.map +1 -0
- package/dist/providers/gateway.d.ts +19 -1
- package/dist/providers/gateway.d.ts.map +1 -1
- package/dist/providers/gateway.js +33 -4
- package/dist/providers/gateway.js.map +1 -1
- package/dist/services.d.ts +13 -0
- package/dist/services.d.ts.map +1 -1
- package/dist/services.js +2 -0
- package/dist/services.js.map +1 -1
- package/dist/sessions/apiLeaseStore.d.ts +10 -0
- package/dist/sessions/apiLeaseStore.d.ts.map +1 -0
- package/dist/sessions/apiLeaseStore.js +41 -0
- package/dist/sessions/apiLeaseStore.js.map +1 -0
- package/dist/sessions/fileRecallStore.d.ts +33 -0
- package/dist/sessions/fileRecallStore.d.ts.map +1 -0
- package/dist/sessions/fileRecallStore.js +112 -0
- package/dist/sessions/fileRecallStore.js.map +1 -0
- package/dist/sessions/fileSessionStore.d.ts +20 -0
- package/dist/sessions/fileSessionStore.d.ts.map +1 -0
- package/dist/sessions/fileSessionStore.js +151 -0
- package/dist/sessions/fileSessionStore.js.map +1 -0
- package/dist/sessions/head.d.ts +90 -0
- package/dist/sessions/head.d.ts.map +1 -0
- package/dist/sessions/head.js +82 -0
- package/dist/sessions/head.js.map +1 -0
- package/dist/sessions/inflight.d.ts +58 -0
- package/dist/sessions/inflight.d.ts.map +1 -0
- package/dist/sessions/inflight.js +0 -0
- package/dist/sessions/inflight.js.map +1 -0
- package/dist/sessions/jsonl.d.ts +23 -0
- package/dist/sessions/jsonl.d.ts.map +1 -0
- package/dist/sessions/jsonl.js +52 -0
- package/dist/sessions/jsonl.js.map +1 -0
- package/dist/sessions/lease.d.ts +59 -0
- package/dist/sessions/lease.d.ts.map +1 -0
- package/dist/sessions/lease.js +115 -0
- package/dist/sessions/lease.js.map +1 -0
- package/dist/sessions/leaseStore.d.ts +46 -0
- package/dist/sessions/leaseStore.d.ts.map +1 -0
- package/dist/sessions/leaseStore.js +58 -0
- package/dist/sessions/leaseStore.js.map +1 -0
- package/dist/sessions/testFs.d.ts +5 -0
- package/dist/sessions/testFs.d.ts.map +1 -0
- package/dist/sessions/testFs.js +85 -0
- package/dist/sessions/testFs.js.map +1 -0
- package/dist/sessions/timeline.d.ts +134 -0
- package/dist/sessions/timeline.d.ts.map +1 -0
- package/dist/sessions/timeline.js +127 -0
- package/dist/sessions/timeline.js.map +1 -0
- package/dist/sidecar/host.d.ts +284 -2
- package/dist/sidecar/host.d.ts.map +1 -1
- package/dist/sidecar/host.js +725 -1
- package/dist/sidecar/host.js.map +1 -1
- package/dist/sidecar/server.d.ts +217 -2
- package/dist/sidecar/server.d.ts.map +1 -1
- package/dist/sidecar/server.js +828 -3
- package/dist/sidecar/server.js.map +1 -1
- package/dist/skills.d.ts +8 -3
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +71 -35
- package/dist/skills.js.map +1 -1
- package/dist/snapshot/effects.d.ts +69 -0
- package/dist/snapshot/effects.d.ts.map +1 -0
- package/dist/snapshot/effects.js +67 -0
- package/dist/snapshot/effects.js.map +1 -0
- package/dist/snapshot/manifest.d.ts +73 -0
- package/dist/snapshot/manifest.d.ts.map +1 -0
- package/dist/snapshot/manifest.js +141 -0
- package/dist/snapshot/manifest.js.map +1 -0
- package/dist/snapshot/tree.d.ts +62 -0
- package/dist/snapshot/tree.d.ts.map +1 -0
- package/dist/snapshot/tree.js +147 -0
- package/dist/snapshot/tree.js.map +1 -0
- package/dist/tools/builtins.d.ts +1 -0
- package/dist/tools/builtins.d.ts.map +1 -1
- package/dist/tools/builtins.js +150 -5
- package/dist/tools/builtins.js.map +1 -1
- package/dist/tools/files.d.ts.map +1 -1
- package/dist/tools/files.js +13 -0
- package/dist/tools/files.js.map +1 -1
- package/dist/tools/http.d.ts.map +1 -1
- package/dist/tools/http.js.map +1 -1
- package/dist/tools/namespaces.d.ts +1 -1
- package/dist/tools/namespaces.d.ts.map +1 -1
- package/dist/tools/namespaces.js +24 -0
- package/dist/tools/namespaces.js.map +1 -1
- package/dist/tools/todo.d.ts.map +1 -1
- package/dist/tools/todo.js +12 -0
- package/dist/tools/todo.js.map +1 -1
- package/package.json +26 -34
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
export const SNAPSHOT_VERSION = 1;
|
|
2
|
+
/**
|
|
3
|
+
* Strip anything that looks like a secret VALUE from provider config.
|
|
4
|
+
*
|
|
5
|
+
* Defense in depth, not a substitute for callers behaving: provider settings are
|
|
6
|
+
* a loose bag, and a snapshot is a thing people mail around. A key that survives
|
|
7
|
+
* into a manifest is a key in someone's git history. Keys are matched by name
|
|
8
|
+
* because that is the only signal available in an untyped record.
|
|
9
|
+
*/
|
|
10
|
+
const SECRET_WORD = /(^|_)(key|secret|token|password|passwd|credential|credentials|authorization|auth|bearer|apikey)($|_)/;
|
|
11
|
+
/**
|
|
12
|
+
* Normalize a key so camelCase reads like snake_case before matching.
|
|
13
|
+
*
|
|
14
|
+
* Without this, `apiKey` — the single most common spelling of the thing this
|
|
15
|
+
* function exists to catch — sailed straight through, because `key` was not
|
|
16
|
+
* preceded by a separator. Both boundaries are needed: lower-to-upper for
|
|
17
|
+
* `apiKey`, and the caps-run split for `APIKey`.
|
|
18
|
+
*/
|
|
19
|
+
function normalizeKey(key) {
|
|
20
|
+
return key
|
|
21
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
|
|
22
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
23
|
+
.replace(/-/g, '_')
|
|
24
|
+
.toLowerCase();
|
|
25
|
+
}
|
|
26
|
+
function isSecretKey(key) {
|
|
27
|
+
return SECRET_WORD.test(normalizeKey(key));
|
|
28
|
+
}
|
|
29
|
+
export function redactProvider(provider) {
|
|
30
|
+
if (!provider)
|
|
31
|
+
return null;
|
|
32
|
+
const out = {};
|
|
33
|
+
for (const key of Object.keys(provider).sort()) {
|
|
34
|
+
if (isSecretKey(key))
|
|
35
|
+
continue;
|
|
36
|
+
const v = provider[key];
|
|
37
|
+
// Nested objects get the same treatment; anything else is copied as-is.
|
|
38
|
+
out[key] = (typeof v === 'object' && v !== null && !Array.isArray(v))
|
|
39
|
+
? redactProvider(v)
|
|
40
|
+
: v;
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
export function buildManifest(input) {
|
|
45
|
+
return {
|
|
46
|
+
snapshotVersion: SNAPSHOT_VERSION,
|
|
47
|
+
sessionId: input.sessionId,
|
|
48
|
+
// Unbranched sessions are their own lineage root, which is the common case
|
|
49
|
+
// and must not require every caller to think about it.
|
|
50
|
+
lineageRoot: input.lineageRoot ?? input.sessionId,
|
|
51
|
+
parent: input.parent ?? null,
|
|
52
|
+
createdAt: input.createdAt,
|
|
53
|
+
baseImage: input.baseImage ?? null,
|
|
54
|
+
policy: input.policy ?? null,
|
|
55
|
+
lock: input.lock ?? null,
|
|
56
|
+
provider: redactProvider(input.provider),
|
|
57
|
+
// Names only, deduped and sorted so the manifest is stable.
|
|
58
|
+
env: [...new Set(input.env ?? [])].sort(),
|
|
59
|
+
tree: input.tree,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Canonical text form: fixed key order, sorted env, trailing newline. */
|
|
63
|
+
export function serializeManifest(m) {
|
|
64
|
+
const canonical = {
|
|
65
|
+
snapshotVersion: SNAPSHOT_VERSION,
|
|
66
|
+
sessionId: m.sessionId,
|
|
67
|
+
lineageRoot: m.lineageRoot || m.sessionId,
|
|
68
|
+
parent: m.parent,
|
|
69
|
+
createdAt: m.createdAt,
|
|
70
|
+
baseImage: m.baseImage,
|
|
71
|
+
policy: m.policy ?? null,
|
|
72
|
+
lock: m.lock ?? null,
|
|
73
|
+
provider: m.provider ?? null,
|
|
74
|
+
env: [...new Set(m.env)].sort(),
|
|
75
|
+
tree: m.tree,
|
|
76
|
+
};
|
|
77
|
+
return JSON.stringify(canonical, null, 2) + '\n';
|
|
78
|
+
}
|
|
79
|
+
function isRecord(v) {
|
|
80
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Parse a manifest, or null when the text is not one.
|
|
84
|
+
*
|
|
85
|
+
* Strict about identity (`sessionId`, `tree`) because a snapshot that cannot say
|
|
86
|
+
* what it is or what it contains is unusable. A future `snapshotVersion` is
|
|
87
|
+
* rejected rather than read under rules that no longer apply — the same call the
|
|
88
|
+
* lockfile makes, for the same reason.
|
|
89
|
+
*/
|
|
90
|
+
export function parseManifest(text) {
|
|
91
|
+
let parsed;
|
|
92
|
+
try {
|
|
93
|
+
parsed = JSON.parse(text);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
if (!isRecord(parsed))
|
|
99
|
+
return null;
|
|
100
|
+
if (parsed.snapshotVersion !== SNAPSHOT_VERSION)
|
|
101
|
+
return null;
|
|
102
|
+
if (typeof parsed.sessionId !== 'string' || !parsed.sessionId)
|
|
103
|
+
return null;
|
|
104
|
+
if (typeof parsed.tree !== 'string' || !parsed.tree)
|
|
105
|
+
return null;
|
|
106
|
+
return {
|
|
107
|
+
snapshotVersion: SNAPSHOT_VERSION,
|
|
108
|
+
sessionId: parsed.sessionId,
|
|
109
|
+
// An older manifest has no lineage root; it was written before branching
|
|
110
|
+
// existed, so it IS its own root.
|
|
111
|
+
lineageRoot: typeof parsed.lineageRoot === 'string' && parsed.lineageRoot ? parsed.lineageRoot : parsed.sessionId,
|
|
112
|
+
parent: typeof parsed.parent === 'string' ? parsed.parent : null,
|
|
113
|
+
createdAt: typeof parsed.createdAt === 'number' ? parsed.createdAt : 0,
|
|
114
|
+
baseImage: typeof parsed.baseImage === 'string' ? parsed.baseImage : null,
|
|
115
|
+
policy: parsed.policy ?? null,
|
|
116
|
+
lock: (parsed.lock ?? null),
|
|
117
|
+
provider: isRecord(parsed.provider) ? parsed.provider : null,
|
|
118
|
+
env: Array.isArray(parsed.env) ? [...new Set(parsed.env.filter((e) => typeof e === 'string'))].sort() : [],
|
|
119
|
+
tree: parsed.tree,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Is this manifest safe to rehydrate from?
|
|
124
|
+
*
|
|
125
|
+
* A snapshot with no policy would restore UNGOVERNED. Rather than defaulting to
|
|
126
|
+
* something permissive — or to something strict the user did not choose — this
|
|
127
|
+
* refuses, and the caller decides whether to attach a policy explicitly. Silence
|
|
128
|
+
* here is how an audit trail ends up describing a run nobody authorized.
|
|
129
|
+
*/
|
|
130
|
+
export function manifestProblems(m) {
|
|
131
|
+
const problems = [];
|
|
132
|
+
if (m.policy === null || m.policy === undefined) {
|
|
133
|
+
problems.push('no policy — restoring would run ungoverned (see spec s5.1)');
|
|
134
|
+
}
|
|
135
|
+
if (!m.tree)
|
|
136
|
+
problems.push('no tree hash — the snapshot does not say what it contains');
|
|
137
|
+
if (!m.sessionId)
|
|
138
|
+
problems.push('no session id');
|
|
139
|
+
return problems;
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../snapshot/manifest.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** path -> content hash. Sorted on serialize, so the same tree is the same text. */
|
|
2
|
+
export type Tree = Record<string, string>;
|
|
3
|
+
export interface TreeDelta {
|
|
4
|
+
/** Paths present now and absent from the parent. */
|
|
5
|
+
added: string[];
|
|
6
|
+
/** Paths in both, with different content. */
|
|
7
|
+
modified: string[];
|
|
8
|
+
/** Paths in the parent and gone now. A snapshot that cannot express deletion
|
|
9
|
+
* silently resurrects files on restore. */
|
|
10
|
+
removed: string[];
|
|
11
|
+
}
|
|
12
|
+
/** Canonical text form of a tree — sorted paths, trailing newline. */
|
|
13
|
+
export declare function serializeTree(tree: Tree): string;
|
|
14
|
+
export declare function parseTree(text: string): Tree | null;
|
|
15
|
+
/** What changed between two trees. Deterministic: every list is sorted. */
|
|
16
|
+
export declare function diffTrees(parent: Tree, current: Tree): TreeDelta;
|
|
17
|
+
/** True when nothing changed — worth checking before writing a snapshot nobody
|
|
18
|
+
* needs, and before shipping bytes across a wire. */
|
|
19
|
+
export declare function isEmptyDelta(d: TreeDelta): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* The blob hashes a delta actually needs shipped.
|
|
22
|
+
*
|
|
23
|
+
* DEDUPED, which is the point of content addressing: two paths with identical
|
|
24
|
+
* content share one blob, so a copied file or a repeated boilerplate header costs
|
|
25
|
+
* nothing extra. Removals need no blob at all.
|
|
26
|
+
*/
|
|
27
|
+
export declare function blobsForDelta(delta: TreeDelta, current: Tree): string[];
|
|
28
|
+
/**
|
|
29
|
+
* Apply a delta to a parent tree, producing the child tree.
|
|
30
|
+
*
|
|
31
|
+
* The inverse of diffTrees, and the restore path's core step. `hashes` supplies
|
|
32
|
+
* the content hash for every added/modified path — a delta alone does not carry
|
|
33
|
+
* them, because the tree it came from already did.
|
|
34
|
+
*/
|
|
35
|
+
export declare function applyDelta(parent: Tree, delta: TreeDelta, hashes: Tree): Tree;
|
|
36
|
+
/**
|
|
37
|
+
* Collapse a lineage into one effective tree.
|
|
38
|
+
*
|
|
39
|
+
* `chain` runs oldest-first: a full snapshot's tree, then each descendant's
|
|
40
|
+
* delta. Restoring walks it forward rather than resolving parents recursively at
|
|
41
|
+
* read time, so a long chain costs one pass.
|
|
42
|
+
*/
|
|
43
|
+
export declare function collapseChain(base: Tree, steps: readonly {
|
|
44
|
+
delta: TreeDelta;
|
|
45
|
+
hashes: Tree;
|
|
46
|
+
}[]): Tree;
|
|
47
|
+
/** Paths in a tree whose blobs are absent from `have` — what a restore must
|
|
48
|
+
* fetch before it can write anything. */
|
|
49
|
+
export declare function missingBlobs(tree: Tree, have: ReadonlySet<string>): string[];
|
|
50
|
+
/**
|
|
51
|
+
* Paths a snapshot must NOT capture.
|
|
52
|
+
*
|
|
53
|
+
* Session and run logs are Layer 3 and travel by reference (spec s4), so
|
|
54
|
+
* capturing them here would duplicate them and, worse, let a stale copy overwrite
|
|
55
|
+
* the live append-only log on restore — the exact clobber the single-writer rule
|
|
56
|
+
* exists to prevent. Blobs are the session's own offloaded outputs, referenced
|
|
57
|
+
* the same way.
|
|
58
|
+
*/
|
|
59
|
+
export declare function isLayer3Path(path: string, stateDir: string): boolean;
|
|
60
|
+
/** Drop Layer 3 paths from a tree before diffing it. */
|
|
61
|
+
export declare function withoutLayer3(tree: Tree, stateDir: string): Tree;
|
|
62
|
+
//# sourceMappingURL=tree.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tree.d.ts","sourceRoot":"","sources":["../../snapshot/tree.ts"],"names":[],"mappings":"AAiBA,oFAAoF;AACpF,MAAM,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAE1C,MAAM,WAAW,SAAS;IACxB,oDAAoD;IACpD,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,6CAA6C;IAC7C,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB;+CAC2C;IAC3C,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,sEAAsE;AACtE,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAKhD;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CASnD;AAED,2EAA2E;AAC3E,wBAAgB,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,GAAG,SAAS,CAahE;AAED;qDACqD;AACrD,wBAAgB,YAAY,CAAC,CAAC,EAAE,SAAS,GAAG,OAAO,CAElD;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,GAAG,MAAM,EAAE,CAOvE;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,GAAG,IAAI,CAW7E;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS;IAAE,KAAK,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,IAAI,CAAA;CAAE,EAAE,GAAG,IAAI,CAEpG;AAED;yCACyC;AACzC,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE,CAM5E;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAMpE;AAED,wDAAwD;AACxD,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAMhE"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/* Snapshot Layer 2 — the content-addressed file tree and the delta between two.
|
|
2
|
+
*
|
|
3
|
+
* A snapshot ships a DIFF, not a disk image. Spec s4: a raw microVM memory dump
|
|
4
|
+
* does not move between an Apple Silicon laptop and a Linux host, but a file diff
|
|
5
|
+
* does, and because the base is pinned by digest both ends provably start from
|
|
6
|
+
* identical ground. Typically megabytes, not gigabytes.
|
|
7
|
+
*
|
|
8
|
+
* This module is pure — no I/O, no crypto, no node. Hashing is the caller's
|
|
9
|
+
* (node:crypto server-side, SubtleCrypto in a browser), which is also what keeps
|
|
10
|
+
* one implementation serving both.
|
|
11
|
+
*
|
|
12
|
+
* It also gives `parent` a job. An earlier review flagged the manifest's lineage
|
|
13
|
+
* field as unused and asked whether to cut it; incremental snapshots are the use.
|
|
14
|
+
* The first snapshot of a session is full; each later one records only what
|
|
15
|
+
* changed since its parent, and a restore walks the chain back to a full one.
|
|
16
|
+
*/
|
|
17
|
+
/** Canonical text form of a tree — sorted paths, trailing newline. */
|
|
18
|
+
export function serializeTree(tree) {
|
|
19
|
+
const keys = Object.keys(tree).sort();
|
|
20
|
+
const out = {};
|
|
21
|
+
for (const k of keys)
|
|
22
|
+
out[k] = tree[k];
|
|
23
|
+
return JSON.stringify(out, null, 2) + '\n';
|
|
24
|
+
}
|
|
25
|
+
export function parseTree(text) {
|
|
26
|
+
let parsed;
|
|
27
|
+
try {
|
|
28
|
+
parsed = JSON.parse(text);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
34
|
+
return null;
|
|
35
|
+
const out = {};
|
|
36
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
37
|
+
if (typeof v === 'string' && v)
|
|
38
|
+
out[k] = v;
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
/** What changed between two trees. Deterministic: every list is sorted. */
|
|
43
|
+
export function diffTrees(parent, current) {
|
|
44
|
+
const added = [];
|
|
45
|
+
const modified = [];
|
|
46
|
+
const removed = [];
|
|
47
|
+
for (const [path, hash] of Object.entries(current)) {
|
|
48
|
+
const before = parent[path];
|
|
49
|
+
if (before === undefined)
|
|
50
|
+
added.push(path);
|
|
51
|
+
else if (before !== hash)
|
|
52
|
+
modified.push(path);
|
|
53
|
+
}
|
|
54
|
+
for (const path of Object.keys(parent)) {
|
|
55
|
+
if (current[path] === undefined)
|
|
56
|
+
removed.push(path);
|
|
57
|
+
}
|
|
58
|
+
return { added: added.sort(), modified: modified.sort(), removed: removed.sort() };
|
|
59
|
+
}
|
|
60
|
+
/** True when nothing changed — worth checking before writing a snapshot nobody
|
|
61
|
+
* needs, and before shipping bytes across a wire. */
|
|
62
|
+
export function isEmptyDelta(d) {
|
|
63
|
+
return d.added.length === 0 && d.modified.length === 0 && d.removed.length === 0;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The blob hashes a delta actually needs shipped.
|
|
67
|
+
*
|
|
68
|
+
* DEDUPED, which is the point of content addressing: two paths with identical
|
|
69
|
+
* content share one blob, so a copied file or a repeated boilerplate header costs
|
|
70
|
+
* nothing extra. Removals need no blob at all.
|
|
71
|
+
*/
|
|
72
|
+
export function blobsForDelta(delta, current) {
|
|
73
|
+
const needed = new Set();
|
|
74
|
+
for (const path of [...delta.added, ...delta.modified]) {
|
|
75
|
+
const hash = current[path];
|
|
76
|
+
if (hash)
|
|
77
|
+
needed.add(hash);
|
|
78
|
+
}
|
|
79
|
+
return [...needed].sort();
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Apply a delta to a parent tree, producing the child tree.
|
|
83
|
+
*
|
|
84
|
+
* The inverse of diffTrees, and the restore path's core step. `hashes` supplies
|
|
85
|
+
* the content hash for every added/modified path — a delta alone does not carry
|
|
86
|
+
* them, because the tree it came from already did.
|
|
87
|
+
*/
|
|
88
|
+
export function applyDelta(parent, delta, hashes) {
|
|
89
|
+
const out = { ...parent };
|
|
90
|
+
for (const path of delta.removed)
|
|
91
|
+
delete out[path];
|
|
92
|
+
for (const path of [...delta.added, ...delta.modified]) {
|
|
93
|
+
const hash = hashes[path];
|
|
94
|
+
// A delta naming a path with no hash is corrupt; skipping it would silently
|
|
95
|
+
// produce a tree that does not match the snapshot's own tree hash, which the
|
|
96
|
+
// caller can then detect. Better a detectable mismatch than invented content.
|
|
97
|
+
if (hash)
|
|
98
|
+
out[path] = hash;
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Collapse a lineage into one effective tree.
|
|
104
|
+
*
|
|
105
|
+
* `chain` runs oldest-first: a full snapshot's tree, then each descendant's
|
|
106
|
+
* delta. Restoring walks it forward rather than resolving parents recursively at
|
|
107
|
+
* read time, so a long chain costs one pass.
|
|
108
|
+
*/
|
|
109
|
+
export function collapseChain(base, steps) {
|
|
110
|
+
return steps.reduce((acc, step) => applyDelta(acc, step.delta, step.hashes), { ...base });
|
|
111
|
+
}
|
|
112
|
+
/** Paths in a tree whose blobs are absent from `have` — what a restore must
|
|
113
|
+
* fetch before it can write anything. */
|
|
114
|
+
export function missingBlobs(tree, have) {
|
|
115
|
+
const missing = new Set();
|
|
116
|
+
for (const hash of Object.values(tree)) {
|
|
117
|
+
if (!have.has(hash))
|
|
118
|
+
missing.add(hash);
|
|
119
|
+
}
|
|
120
|
+
return [...missing].sort();
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Paths a snapshot must NOT capture.
|
|
124
|
+
*
|
|
125
|
+
* Session and run logs are Layer 3 and travel by reference (spec s4), so
|
|
126
|
+
* capturing them here would duplicate them and, worse, let a stale copy overwrite
|
|
127
|
+
* the live append-only log on restore — the exact clobber the single-writer rule
|
|
128
|
+
* exists to prevent. Blobs are the session's own offloaded outputs, referenced
|
|
129
|
+
* the same way.
|
|
130
|
+
*/
|
|
131
|
+
export function isLayer3Path(path, stateDir) {
|
|
132
|
+
const p = path.replace(/^\.\//, '');
|
|
133
|
+
return p.startsWith(`${stateDir}/sessions/`)
|
|
134
|
+
|| p.startsWith(`${stateDir}/runs/`)
|
|
135
|
+
|| p === `${stateDir}/blobs`
|
|
136
|
+
|| p.startsWith(`${stateDir}/blobs/`);
|
|
137
|
+
}
|
|
138
|
+
/** Drop Layer 3 paths from a tree before diffing it. */
|
|
139
|
+
export function withoutLayer3(tree, stateDir) {
|
|
140
|
+
const out = {};
|
|
141
|
+
for (const [path, hash] of Object.entries(tree)) {
|
|
142
|
+
if (!isLayer3Path(path, stateDir))
|
|
143
|
+
out[path] = hash;
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=tree.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tree.js","sourceRoot":"","sources":["../../snapshot/tree.ts"],"names":[],"mappings":"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"}
|
package/dist/tools/builtins.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export declare const recallResultTool: AgentTool;
|
|
|
8
8
|
export declare const askUserTool: AgentTool;
|
|
9
9
|
export declare const spawnAgentTool: AgentTool;
|
|
10
10
|
export declare const spawnAgentsTool: AgentTool;
|
|
11
|
+
export declare const listToolsTool: AgentTool;
|
|
11
12
|
export declare const compilePrompdTool: AgentTool;
|
|
12
13
|
/** The kernel's runtime-agnostic built-in tools. Product/host tools (registry
|
|
13
14
|
* search) are added by the registry — use `allTools()` for the complete set. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../../tools/builtins.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../../tools/builtins.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,mBAAmB,CAAC;AAwDhE,eAAO,MAAM,eAAe,EAAE,SA0F7B,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,SAyB5B,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,SAa9B,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,SAWvB,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,SAkBvB,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,SA6B9B,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,SAgEzB,CAAC;AAiBF,eAAO,MAAM,cAAc,EAAE,SA6B5B,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,SA6C7B,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,SAoB3B,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,SAgC/B,CAAC;AAEF;gFACgF;AAChF,eAAO,MAAM,YAAY,EAAE,SAAS,EAgBnC,CAAC;AAEF;;;+DAG+D;AAC/D,wBAAgB,QAAQ,IAAI,SAAS,EAAE,CAEtC"}
|
package/dist/tools/builtins.js
CHANGED
|
@@ -8,9 +8,55 @@ function str(args, key) {
|
|
|
8
8
|
const v = args[key];
|
|
9
9
|
return typeof v === 'string' ? v : v == null ? '' : String(v);
|
|
10
10
|
}
|
|
11
|
+
/* ---- propose_edit auto-correct ---- */
|
|
12
|
+
/** Max consecutive proposals that failed to compile and were bounced back to the
|
|
13
|
+
* model for a fix, before a stubbornly-broken proposal falls through to human
|
|
14
|
+
* review instead of looping. The streak lives in ctx.runState (per-run), keyed
|
|
15
|
+
* here, so it can't leak or race across runs. */
|
|
16
|
+
const MAX_AUTOFIX = 3;
|
|
17
|
+
const AUTOFIX_KEY = 'proposeEditAutofixStreak';
|
|
18
|
+
/** Read/advance the per-run auto-fix streak held in ctx.runState. */
|
|
19
|
+
function autofixStreak(ctx) {
|
|
20
|
+
const rs = ctx.runState;
|
|
21
|
+
const cur = rs && typeof rs[AUTOFIX_KEY] === 'number' ? rs[AUTOFIX_KEY] : 0;
|
|
22
|
+
return {
|
|
23
|
+
value: cur,
|
|
24
|
+
bump() { if (rs)
|
|
25
|
+
rs[AUTOFIX_KEY] = cur + 1; },
|
|
26
|
+
reset() { if (rs)
|
|
27
|
+
rs[AUTOFIX_KEY] = 0; },
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Only .prmd (always) and .md WITH frontmatter are worth compiling to validate —
|
|
31
|
+
* plain markdown has nothing to fail. Needs a known editor path for sibling
|
|
32
|
+
* resolution. */
|
|
33
|
+
function shouldValidatePrmd(path, content) {
|
|
34
|
+
if (!path)
|
|
35
|
+
return false;
|
|
36
|
+
if (path.endsWith('.prmd'))
|
|
37
|
+
return true;
|
|
38
|
+
if (path.endsWith('.md'))
|
|
39
|
+
return content.trimStart().startsWith('---');
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
/** Compile the proposed content (resolving inherits:/{% include %} against the
|
|
43
|
+
* editor file's siblings) to validate it. Returns a formatted error list, or null
|
|
44
|
+
* when it compiles cleanly. Unfilled {{ params }} render empty (not an error), so a
|
|
45
|
+
* parameterized prompt still validates. */
|
|
46
|
+
async function prmdCompileError(content, ctx) {
|
|
47
|
+
try {
|
|
48
|
+
const r = await compilePrompt(content, {}, { files: ctx.files, path: ctx.editorFile });
|
|
49
|
+
return r.errors?.length ? r.errors.map((e) => `- ${e.message}`).join('\n') : null;
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
return `- ${e instanceof Error ? e.message : String(e)}`;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
11
55
|
export const proposeEditTool = {
|
|
12
56
|
name: 'propose_edit',
|
|
13
|
-
description: 'Propose a replacement for the
|
|
57
|
+
description: 'Propose a replacement for the CURRENTLY OPEN editor file. It takes no path and can only ever replace that '
|
|
58
|
+
+ 'one file, so never use it to create a new file or to edit a different one — use create_file or write_file '
|
|
59
|
+
+ 'for those. The user reviews a diff and applies or rejects it — ' +
|
|
14
60
|
'this does NOT write the file directly. Provide the COMPLETE new file content (not a patch) plus a short ' +
|
|
15
61
|
'explanation. Use the editor file shown in the system context as the basis. This call WAITS for the review ' +
|
|
16
62
|
'and returns the outcome: applied, partially applied (with the rejected changes), or rejected — rely on ' +
|
|
@@ -20,11 +66,40 @@ export const proposeEditTool = {
|
|
|
20
66
|
properties: {
|
|
21
67
|
explanation: { type: 'string', description: 'One line describing the change.' },
|
|
22
68
|
new_content: { type: 'string', description: 'The full proposed file content.' },
|
|
69
|
+
path: { type: 'string', description: 'The file being replaced. Must be the file currently open in the editor.' },
|
|
23
70
|
},
|
|
24
|
-
required: ['explanation', 'new_content'],
|
|
71
|
+
required: ['explanation', 'new_content', 'path'],
|
|
25
72
|
},
|
|
26
73
|
async run(args, ctx) {
|
|
27
74
|
const explanation = str(args, 'explanation') || 'Proposed an edit';
|
|
75
|
+
// Fail closed with no open file. This tool carries NO target path -- it replaces
|
|
76
|
+
// whatever the editor has open -- so without one there is nothing to diff
|
|
77
|
+
// against. Proposing anyway showed the user an empty diff, and (worse) content
|
|
78
|
+
// meant for a NEW file got diffed against an unrelated open document, where
|
|
79
|
+
// accepting would have overwritten it. Point the model at the tools that take
|
|
80
|
+
// an explicit path instead.
|
|
81
|
+
if (!ctx.editorFile) {
|
|
82
|
+
return 'Cannot propose an edit: no file is open in the editor, and propose_edit only replaces the open file. '
|
|
83
|
+
+ 'To create a new file use create_file, and to overwrite an existing one use write_file — both take an explicit path.';
|
|
84
|
+
}
|
|
85
|
+
// The target is explicit and must be the open file. Before `path` existed this
|
|
86
|
+
// tool meant "the open file" implicitly, so a proposal authored for some OTHER
|
|
87
|
+
// file was silently diffed against whatever was open -- and accepting wrote the
|
|
88
|
+
// wrong content into it. Stating the target makes that mismatch impossible
|
|
89
|
+
// rather than merely unlikely.
|
|
90
|
+
const target = str(args, 'path');
|
|
91
|
+
if (!target) {
|
|
92
|
+
return `Cannot propose an edit: propose_edit needs a path, and it must be the open file (${ctx.editorFile}). `
|
|
93
|
+
+ 'To create a new file use create_file, or to overwrite a different one use write_file.';
|
|
94
|
+
}
|
|
95
|
+
// Compare on a normalized form so a Windows path or a leading ./ is not a false
|
|
96
|
+
// mismatch — but nothing looser than that: matching on the basename alone would
|
|
97
|
+
// accept a/welcome.prmd for b/welcome.prmd, which is the very confusion this guards.
|
|
98
|
+
const norm = (p) => p.trim().replace(/\\/g, '/').replace(/^\.\//, '');
|
|
99
|
+
if (norm(target) !== norm(ctx.editorFile)) {
|
|
100
|
+
return `Cannot propose an edit: the open file is ${ctx.editorFile}, but this proposal targets ${target}. `
|
|
101
|
+
+ 'propose_edit can only replace the open file. Use create_file for a new file, or write_file to overwrite a different one.';
|
|
102
|
+
}
|
|
28
103
|
// The diff + apply happen on the main thread (the panel renders the proposal
|
|
29
104
|
// from this tool call). Block until the user decides so later turns can rely
|
|
30
105
|
// on the outcome. Without a reviewer (sub-agents, DAG nodes, headless runs)
|
|
@@ -32,6 +107,21 @@ export const proposeEditTool = {
|
|
|
32
107
|
if (!ctx.reviewEdit || !ctx.callId) {
|
|
33
108
|
return `Proposed an edit: ${explanation}. Awaiting the user's review (apply/reject).`;
|
|
34
109
|
}
|
|
110
|
+
// Auto-correct: validate the proposal BEFORE surfacing the diff. If it doesn't
|
|
111
|
+
// compile, bounce the errors back to the model to fix instead of showing the
|
|
112
|
+
// user a broken edit. Bounded — after MAX_AUTOFIX consecutive failures, fall
|
|
113
|
+
// through to human review so a stubbornly-broken proposal isn't hidden forever.
|
|
114
|
+
const newContent = str(args, 'new_content');
|
|
115
|
+
if (shouldValidatePrmd(ctx.editorFile, newContent)) {
|
|
116
|
+
const streak = autofixStreak(ctx);
|
|
117
|
+
const err = await prmdCompileError(newContent, ctx);
|
|
118
|
+
if (err && streak.value < MAX_AUTOFIX) {
|
|
119
|
+
streak.bump();
|
|
120
|
+
return ('This proposal does NOT compile, so it was not shown to the user. ' +
|
|
121
|
+
`Fix these errors and call propose_edit again with corrected content:\n${err}`);
|
|
122
|
+
}
|
|
123
|
+
streak.reset(); // surfacing now (clean, or giving up after the budget)
|
|
124
|
+
}
|
|
35
125
|
const outcome = ctx.signal.aborted ? null : await Promise.race([
|
|
36
126
|
ctx.reviewEdit(ctx.callId),
|
|
37
127
|
new Promise((resolve) => ctx.signal.addEventListener('abort', () => resolve(null), { once: true })),
|
|
@@ -52,21 +142,25 @@ export const proposeEditTool = {
|
|
|
52
142
|
export const saveMemoryTool = {
|
|
53
143
|
name: 'save_memory',
|
|
54
144
|
mutates: true,
|
|
55
|
-
description: 'Persist a durable note to memory for later recall (facts, user preferences, decisions).'
|
|
145
|
+
description: 'Persist a durable note to memory for later recall (facts, user preferences, decisions). ' +
|
|
146
|
+
'Choose scope: "workspace" for facts specific to the current project, "global" for facts ' +
|
|
147
|
+
'about the user or their preferences that apply everywhere.',
|
|
56
148
|
parameters: {
|
|
57
149
|
type: 'object',
|
|
58
150
|
properties: {
|
|
59
151
|
text: { type: 'string', description: 'The note to remember.' },
|
|
152
|
+
scope: { type: 'string', enum: ['workspace', 'global'], description: 'Where to store it: workspace (this project) or global (everywhere).' },
|
|
60
153
|
tags: { type: 'array', items: { type: 'string' }, description: 'Optional tags.' },
|
|
61
154
|
},
|
|
62
|
-
required: ['text'],
|
|
155
|
+
required: ['text', 'scope'],
|
|
63
156
|
},
|
|
64
157
|
// Permission is gated centrally by the loop's policy/permission layer (this
|
|
65
158
|
// tool is `mutates`), so no confirm call here.
|
|
66
159
|
async run(args, ctx) {
|
|
67
160
|
const text = str(args, 'text');
|
|
68
161
|
const tags = Array.isArray(args.tags) ? args.tags.map(String) : [];
|
|
69
|
-
const
|
|
162
|
+
const scope = args.scope === 'global' ? 'global' : 'workspace';
|
|
163
|
+
const item = await ctx.memory.save(text, tags, scope);
|
|
70
164
|
return `Saved memory ${item.id}.`;
|
|
71
165
|
},
|
|
72
166
|
};
|
|
@@ -117,6 +211,19 @@ export const kvSetTool = {
|
|
|
117
211
|
export const recallResultTool = {
|
|
118
212
|
name: 'recall_result',
|
|
119
213
|
noOffload: true, // recalling a large result must never re-offload itself
|
|
214
|
+
/* DECLARED, like todo_write's and ask_user's. This one grants no new access at
|
|
215
|
+
* all: it reads back a tool result THIS SESSION ALREADY PRODUCED, which means
|
|
216
|
+
* the original call already passed the policy check that produced it. The
|
|
217
|
+
* store is session-scoped (`<root>/<sessionId>/blobs/<id>.json`) and the id
|
|
218
|
+
* shape is validated, so it cannot reach another session's data or escape the
|
|
219
|
+
* directory.
|
|
220
|
+
*
|
|
221
|
+
* The deciding argument is that offloading is a CONTEXT-MANAGEMENT detail,
|
|
222
|
+
* triggered by size. Gating recall means the same information costs an
|
|
223
|
+
* approval prompt depending on how large it happened to be - a performance
|
|
224
|
+
* heuristic leaking into governance, and an approval the user cannot act on
|
|
225
|
+
* meaningfully because they already approved the thing that produced it. */
|
|
226
|
+
permission: 'allow',
|
|
120
227
|
description: 'Fetch the full content of a tool result that was offloaded to save context. ' +
|
|
121
228
|
'You saw an "[offloaded tool result … recall_result({ id })]" reference — pass that id.',
|
|
122
229
|
parameters: {
|
|
@@ -133,6 +240,21 @@ export const recallResultTool = {
|
|
|
133
240
|
export const askUserTool = {
|
|
134
241
|
name: 'ask_user',
|
|
135
242
|
noOffload: true,
|
|
243
|
+
/* DECLARED, like todo_write's. Gating this behind an approval prompt is asking
|
|
244
|
+
* the user for permission to ask the user a question — two dialogs for one
|
|
245
|
+
* decision, the first of which carries no information.
|
|
246
|
+
*
|
|
247
|
+
* It changes nothing outside the run: no file, no network, no state. Its whole
|
|
248
|
+
* effect is to put a question in front of a person who is already there.
|
|
249
|
+
*
|
|
250
|
+
* Nothing depended on its previous `ask` tier. The case that looks like it
|
|
251
|
+
* might — an unattended run parking forever on a question nobody will answer —
|
|
252
|
+
* is handled by EXCLUSION rather than by permission: CREW_EXCLUDED_TOOLS drops
|
|
253
|
+
* ask_user from every crew agent (see @corenel/crew's capabilities.ts), and the
|
|
254
|
+
* daemon's executor honours the same set. On a surface where it IS offered,
|
|
255
|
+
* someone is by definition attending. And with no `ask` handler wired, `run`
|
|
256
|
+
* already returns "unavailable in this environment" rather than hanging. */
|
|
257
|
+
permission: 'allow',
|
|
136
258
|
description: 'Ask the user one or more questions when you need a decision you cannot make from context — to choose ' +
|
|
137
259
|
'between approaches, get a missing requirement, or confirm direction. Prefer this over guessing. Give each ' +
|
|
138
260
|
'question a short `header` (a chip label), the `question` text, and 2–4 distinct `options` (each with a ' +
|
|
@@ -274,6 +396,28 @@ export const spawnAgentsTool = {
|
|
|
274
396
|
return results.join('\n\n');
|
|
275
397
|
},
|
|
276
398
|
};
|
|
399
|
+
export const listToolsTool = {
|
|
400
|
+
name: 'list_tools',
|
|
401
|
+
noOffload: true,
|
|
402
|
+
description: 'List the tools available to you this run — their names, one-line descriptions, and top-level parameters. ' +
|
|
403
|
+
'Use this to discover what you can call (including any MCP or connected-host tools) before deciding how to act.',
|
|
404
|
+
parameters: { type: 'object', properties: {} },
|
|
405
|
+
async run(_args, ctx) {
|
|
406
|
+
if (!ctx.listTools)
|
|
407
|
+
return 'Tool discovery is not available here.';
|
|
408
|
+
const tools = ctx.listTools();
|
|
409
|
+
if (!tools.length)
|
|
410
|
+
return 'No tools are available this run.';
|
|
411
|
+
const lines = tools.map((t) => {
|
|
412
|
+
const props = t.parameters && typeof t.parameters === 'object'
|
|
413
|
+
? Object.keys(t.parameters.properties ?? {})
|
|
414
|
+
: [];
|
|
415
|
+
const params = props.length ? ` — params: ${props.join(', ')}` : '';
|
|
416
|
+
return `- \`${t.name}\`: ${t.description}${params}`;
|
|
417
|
+
});
|
|
418
|
+
return `Available tools this run (${tools.length}):\n${lines.join('\n')}`;
|
|
419
|
+
},
|
|
420
|
+
};
|
|
277
421
|
export const compilePrompdTool = {
|
|
278
422
|
name: 'compile_prompd',
|
|
279
423
|
noOffload: true, // the compiled output is what the user asked to see — keep inline
|
|
@@ -313,6 +457,7 @@ export const defaultTools = [
|
|
|
313
457
|
askUserTool,
|
|
314
458
|
spawnAgentTool,
|
|
315
459
|
spawnAgentsTool,
|
|
460
|
+
listToolsTool,
|
|
316
461
|
proposeEditTool,
|
|
317
462
|
saveMemoryTool,
|
|
318
463
|
recallMemoryTool,
|