@arnilo/prism 0.0.19 → 0.0.22
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/CHANGELOG.md +46 -0
- package/dist/agents.js +17 -2
- package/dist/context-budget.d.ts +5 -1
- package/dist/context-budget.js +55 -9
- package/dist/contracts.d.ts +16 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +4 -1
- package/dist/input.d.ts +6 -0
- package/dist/input.js +42 -8
- package/dist/skill-disclosure.d.ts +35 -0
- package/dist/skill-disclosure.js +101 -0
- package/dist/skill-load.d.ts +25 -0
- package/dist/skill-load.js +112 -0
- package/dist/tool-result-fold.d.ts +40 -0
- package/dist/tool-result-fold.js +164 -0
- package/docs/0.1.0-readiness.md +5 -5
- package/docs/agent-session-runtime.md +2 -0
- package/docs/caveman.md +129 -0
- package/docs/coding-agent-tools.md +123 -21
- package/docs/coding-security.md +3 -3
- package/docs/context-and-skills.md +94 -7
- package/docs/extensions.md +2 -0
- package/docs/index.md +10 -6
- package/docs/migration.md +71 -0
- package/docs/ponytail.md +127 -0
- package/docs/release-and-install.md +78 -15
- package/package.json +1 -1
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { estimateTextBytes } from "./context-budget.js";
|
|
2
|
+
import { HARD_MAX_SKILL_INSTRUCTION_BYTES } from "./skill-disclosure.js";
|
|
3
|
+
export const DEFAULT_LOAD_SKILL_TOOL_NAME = "load_skill";
|
|
4
|
+
export const SKILL_LOAD_ERROR_CODE = "skill_load_failed";
|
|
5
|
+
export const MAX_LOAD_SKILL_RESULT_BYTES = 512;
|
|
6
|
+
export class SkillLoadError extends Error {
|
|
7
|
+
code = SKILL_LOAD_ERROR_CODE;
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "SkillLoadError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function isSkillLoadError(error) {
|
|
14
|
+
return error instanceof Error && error.code === SKILL_LOAD_ERROR_CODE;
|
|
15
|
+
}
|
|
16
|
+
export function resolveSkillLoad(options) {
|
|
17
|
+
const name = options.name.trim();
|
|
18
|
+
if (!name)
|
|
19
|
+
throw new SkillLoadError("Skill name is required");
|
|
20
|
+
let skill;
|
|
21
|
+
try {
|
|
22
|
+
skill = options.registry.resolve(name);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new SkillLoadError(`Unknown skill: ${name}`);
|
|
26
|
+
}
|
|
27
|
+
if (options.activeSkillNames && !options.activeSkillNames.includes(skill.name)) {
|
|
28
|
+
throw new SkillLoadError(`Skill ${skill.name} is not active for this run`);
|
|
29
|
+
}
|
|
30
|
+
const toolNames = new Set((options.tools ?? []).map((tool) => tool.name));
|
|
31
|
+
const missingTool = skill.toolNames?.find((toolName) => !toolNames.has(toolName));
|
|
32
|
+
if (missingTool)
|
|
33
|
+
throw new SkillLoadError(`Skill ${skill.name} requires inactive tool: ${missingTool}`);
|
|
34
|
+
if (!skill.instructions?.trim())
|
|
35
|
+
throw new SkillLoadError(`Skill ${skill.name} has no instructions to load`);
|
|
36
|
+
const bytes = estimateTextBytes(skill.instructions);
|
|
37
|
+
if (bytes > HARD_MAX_SKILL_INSTRUCTION_BYTES) {
|
|
38
|
+
throw new SkillLoadError(`Skill ${skill.name} instructions exceed hard cap (${HARD_MAX_SKILL_INSTRUCTION_BYTES} bytes)`);
|
|
39
|
+
}
|
|
40
|
+
if (options.loaded?.has(skill.name))
|
|
41
|
+
throw new SkillLoadError(`Skill ${skill.name} is already loaded`);
|
|
42
|
+
return skill;
|
|
43
|
+
}
|
|
44
|
+
function skillLoadMetadata(context) {
|
|
45
|
+
const metadata = context.metadata;
|
|
46
|
+
if (!metadata || typeof metadata !== "object")
|
|
47
|
+
return undefined;
|
|
48
|
+
return metadata;
|
|
49
|
+
}
|
|
50
|
+
function capLoadSkillText(text) {
|
|
51
|
+
const bytes = estimateTextBytes(text);
|
|
52
|
+
if (bytes <= MAX_LOAD_SKILL_RESULT_BYTES)
|
|
53
|
+
return text;
|
|
54
|
+
const suffix = "…";
|
|
55
|
+
let end = MAX_LOAD_SKILL_RESULT_BYTES - new TextEncoder().encode(suffix).length;
|
|
56
|
+
const encoded = new TextEncoder().encode(text);
|
|
57
|
+
while (end > 0 && (encoded[end] & 0xc0) === 0x80)
|
|
58
|
+
end--;
|
|
59
|
+
return new TextDecoder().decode(encoded.slice(0, end)) + suffix;
|
|
60
|
+
}
|
|
61
|
+
export function createLoadSkillTool(options) {
|
|
62
|
+
const toolName = options.name ?? DEFAULT_LOAD_SKILL_TOOL_NAME;
|
|
63
|
+
return {
|
|
64
|
+
name: toolName,
|
|
65
|
+
description: "Load the full instructions for an active skill by exact name.",
|
|
66
|
+
parameters: {
|
|
67
|
+
type: "object",
|
|
68
|
+
properties: {
|
|
69
|
+
name: { type: "string", description: "Exact skill name from the catalog." },
|
|
70
|
+
},
|
|
71
|
+
required: ["name"],
|
|
72
|
+
},
|
|
73
|
+
execute(args, context) {
|
|
74
|
+
const fail = (reason, text) => {
|
|
75
|
+
const bounded = capLoadSkillText(text);
|
|
76
|
+
return {
|
|
77
|
+
toolCallId: context.toolCallId,
|
|
78
|
+
name: toolName,
|
|
79
|
+
value: { ok: false, reason, text: bounded },
|
|
80
|
+
content: [{ type: "text", text: bounded }],
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
const name = typeof args.name === "string" ? args.name : "";
|
|
84
|
+
const metadata = skillLoadMetadata(context);
|
|
85
|
+
const loaded = metadata?.loadedSkills ?? options.loaded;
|
|
86
|
+
if (!loaded)
|
|
87
|
+
return fail("no_loaded_set", "Skill load state is unavailable for this session.");
|
|
88
|
+
try {
|
|
89
|
+
const skill = resolveSkillLoad({
|
|
90
|
+
registry: options.registry,
|
|
91
|
+
name,
|
|
92
|
+
tools: metadata?.activeTools ?? options.tools,
|
|
93
|
+
loaded,
|
|
94
|
+
activeSkillNames: metadata?.activeSkillNames,
|
|
95
|
+
});
|
|
96
|
+
loaded.add(skill.name);
|
|
97
|
+
const text = capLoadSkillText(`Loaded skill ${skill.name} for this session.`);
|
|
98
|
+
return {
|
|
99
|
+
toolCallId: context.toolCallId,
|
|
100
|
+
name: toolName,
|
|
101
|
+
value: { ok: true, name: skill.name, text },
|
|
102
|
+
content: [{ type: "text", text }],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
const message = error instanceof Error ? error.message : "Skill load failed";
|
|
107
|
+
return fail("skill_load_failed", message);
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=skill-load.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Message, ToolResult } from "./contracts.js";
|
|
2
|
+
export declare const DEFAULT_TOOL_RESULT_FOLD_MIN_AGE_TURNS = 2;
|
|
3
|
+
export declare const DEFAULT_TOOL_RESULT_FOLD_MIN_BYTES = 4096;
|
|
4
|
+
export declare const DEFAULT_TOOL_RESULT_FOLD_MAX_SUMMARY_BYTES = 512;
|
|
5
|
+
export declare const HARD_TOOL_RESULT_FOLD_MAX_SUMMARY_BYTES = 4096;
|
|
6
|
+
export declare const TOOL_RESULT_FOLD_TURN_METADATA_KEY: "prismToolResultTurn";
|
|
7
|
+
export interface ToolResultFoldInput {
|
|
8
|
+
readonly sessionId: string;
|
|
9
|
+
readonly runId: string;
|
|
10
|
+
readonly turn: number;
|
|
11
|
+
readonly toolCallId: string;
|
|
12
|
+
readonly toolName: string;
|
|
13
|
+
readonly text: string;
|
|
14
|
+
}
|
|
15
|
+
export interface ToolResultFoldOptions {
|
|
16
|
+
readonly minAgeTurns?: number;
|
|
17
|
+
readonly minBytes?: number;
|
|
18
|
+
readonly maxSummaryBytes?: number;
|
|
19
|
+
readonly summarize: (input: ToolResultFoldInput) => Promise<string> | string;
|
|
20
|
+
}
|
|
21
|
+
export interface ResolvedToolResultFoldOptions {
|
|
22
|
+
readonly minAgeTurns: number;
|
|
23
|
+
readonly minBytes: number;
|
|
24
|
+
readonly maxSummaryBytes: number;
|
|
25
|
+
readonly summarize: (input: ToolResultFoldInput) => Promise<string> | string;
|
|
26
|
+
}
|
|
27
|
+
export interface FoldToolResultsContext {
|
|
28
|
+
readonly sessionId: string;
|
|
29
|
+
readonly runId: string;
|
|
30
|
+
readonly turn: number;
|
|
31
|
+
readonly signal?: AbortSignal;
|
|
32
|
+
}
|
|
33
|
+
/** Run overrides agent; disabled when neither supplies `summarize`. */
|
|
34
|
+
export declare function resolveToolResultFold(run?: ToolResultFoldOptions, agent?: ToolResultFoldOptions): ResolvedToolResultFoldOptions | undefined;
|
|
35
|
+
/** Projection-only fold for history tool messages; does not mutate the input array. */
|
|
36
|
+
export declare function foldToolResultHistory(history: readonly Message[], options: ResolvedToolResultFoldOptions, context: FoldToolResultsContext): Promise<readonly Message[]>;
|
|
37
|
+
/** Projection-only fold for in-flight tool results before message conversion. */
|
|
38
|
+
export declare function foldToolResults(results: readonly ToolResult[], options: ResolvedToolResultFoldOptions, context: FoldToolResultsContext): Promise<readonly ToolResult[]>;
|
|
39
|
+
export declare function formatFoldedToolResult(summary: string): string;
|
|
40
|
+
export declare function foldedToolResultHeader(toolName: string, toolCallId: string, summary: string): string;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { estimateTextBytes } from "./context-budget.js";
|
|
2
|
+
export const DEFAULT_TOOL_RESULT_FOLD_MIN_AGE_TURNS = 2;
|
|
3
|
+
export const DEFAULT_TOOL_RESULT_FOLD_MIN_BYTES = 4_096;
|
|
4
|
+
export const DEFAULT_TOOL_RESULT_FOLD_MAX_SUMMARY_BYTES = 512;
|
|
5
|
+
export const HARD_TOOL_RESULT_FOLD_MAX_SUMMARY_BYTES = 4_096;
|
|
6
|
+
export const TOOL_RESULT_FOLD_TURN_METADATA_KEY = "prismToolResultTurn";
|
|
7
|
+
/** Run overrides agent; disabled when neither supplies `summarize`. */
|
|
8
|
+
export function resolveToolResultFold(run, agent) {
|
|
9
|
+
const options = run ?? agent;
|
|
10
|
+
if (!options?.summarize)
|
|
11
|
+
return undefined;
|
|
12
|
+
const minAgeTurns = options.minAgeTurns ?? DEFAULT_TOOL_RESULT_FOLD_MIN_AGE_TURNS;
|
|
13
|
+
const minBytes = options.minBytes ?? DEFAULT_TOOL_RESULT_FOLD_MIN_BYTES;
|
|
14
|
+
const maxSummaryBytes = options.maxSummaryBytes ?? DEFAULT_TOOL_RESULT_FOLD_MAX_SUMMARY_BYTES;
|
|
15
|
+
assertPositiveInt(minAgeTurns, "minAgeTurns", 1, 1_024);
|
|
16
|
+
assertPositiveInt(minBytes, "minBytes", 1, 32 * 1024 * 1024);
|
|
17
|
+
assertPositiveInt(maxSummaryBytes, "maxSummaryBytes", 1, HARD_TOOL_RESULT_FOLD_MAX_SUMMARY_BYTES);
|
|
18
|
+
return { minAgeTurns, minBytes, maxSummaryBytes, summarize: options.summarize };
|
|
19
|
+
}
|
|
20
|
+
/** Projection-only fold for history tool messages; does not mutate the input array. */
|
|
21
|
+
export async function foldToolResultHistory(history, options, context) {
|
|
22
|
+
if (history.length === 0)
|
|
23
|
+
return history;
|
|
24
|
+
const turns = inferToolResultTurns(history);
|
|
25
|
+
const out = [];
|
|
26
|
+
for (let index = 0; index < history.length; index += 1) {
|
|
27
|
+
const message = history[index];
|
|
28
|
+
const folded = await foldToolResultMessage(message, options, {
|
|
29
|
+
...context,
|
|
30
|
+
toolResultTurn: turns[index] ?? context.turn,
|
|
31
|
+
});
|
|
32
|
+
out.push(folded);
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
/** Projection-only fold for in-flight tool results before message conversion. */
|
|
37
|
+
export async function foldToolResults(results, options, context) {
|
|
38
|
+
if (results.length === 0)
|
|
39
|
+
return results;
|
|
40
|
+
const out = [];
|
|
41
|
+
for (const result of results) {
|
|
42
|
+
const folded = await foldToolResultValue(result, options, { ...context, toolResultTurn: context.turn });
|
|
43
|
+
out.push(folded);
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
async function foldToolResultMessage(message, options, context) {
|
|
48
|
+
if (message.role !== "tool")
|
|
49
|
+
return message;
|
|
50
|
+
const block = message.content.find((part) => part.type === "tool_result");
|
|
51
|
+
if (!block || block.type !== "tool_result")
|
|
52
|
+
return message;
|
|
53
|
+
const text = toolResultText(block.result, block.error, message.content);
|
|
54
|
+
const folded = await maybeFold({
|
|
55
|
+
options,
|
|
56
|
+
context,
|
|
57
|
+
toolCallId: block.toolCallId,
|
|
58
|
+
toolName: block.name,
|
|
59
|
+
text,
|
|
60
|
+
apply: (summary) => ({
|
|
61
|
+
...message,
|
|
62
|
+
content: message.content.map((part) => part.type === "tool_result"
|
|
63
|
+
? { ...part, result: foldedToolResultHeader(block.name, block.toolCallId, summary), error: undefined }
|
|
64
|
+
: part),
|
|
65
|
+
metadata: { ...message.metadata, prismFolded: true },
|
|
66
|
+
}),
|
|
67
|
+
});
|
|
68
|
+
return folded ?? message;
|
|
69
|
+
}
|
|
70
|
+
async function foldToolResultValue(result, options, context) {
|
|
71
|
+
const text = toolResultText(result.value, result.error, result.content);
|
|
72
|
+
const folded = await maybeFold({
|
|
73
|
+
options,
|
|
74
|
+
context,
|
|
75
|
+
toolCallId: result.toolCallId,
|
|
76
|
+
toolName: result.name,
|
|
77
|
+
text,
|
|
78
|
+
apply: (summary) => ({
|
|
79
|
+
...result,
|
|
80
|
+
value: foldedToolResultHeader(result.name, result.toolCallId, summary),
|
|
81
|
+
error: undefined,
|
|
82
|
+
metadata: { ...result.metadata, prismFolded: true },
|
|
83
|
+
}),
|
|
84
|
+
});
|
|
85
|
+
return folded ?? result;
|
|
86
|
+
}
|
|
87
|
+
async function maybeFold(input) {
|
|
88
|
+
const age = input.context.turn - input.context.toolResultTurn;
|
|
89
|
+
if (age < input.options.minAgeTurns)
|
|
90
|
+
return undefined;
|
|
91
|
+
if (estimateTextBytes(input.text) < input.options.minBytes)
|
|
92
|
+
return undefined;
|
|
93
|
+
throwIfAborted(input.context.signal);
|
|
94
|
+
try {
|
|
95
|
+
const summary = await input.options.summarize({
|
|
96
|
+
sessionId: input.context.sessionId,
|
|
97
|
+
runId: input.context.runId,
|
|
98
|
+
turn: input.context.toolResultTurn,
|
|
99
|
+
toolCallId: input.toolCallId,
|
|
100
|
+
toolName: input.toolName,
|
|
101
|
+
text: input.text,
|
|
102
|
+
});
|
|
103
|
+
return input.apply(capSummaryBytes(String(summary), input.options.maxSummaryBytes));
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export function formatFoldedToolResult(summary) {
|
|
110
|
+
return summary;
|
|
111
|
+
}
|
|
112
|
+
export function foldedToolResultHeader(toolName, toolCallId, summary) {
|
|
113
|
+
return `Tool result ${toolName} [${toolCallId}]: ${summary}`;
|
|
114
|
+
}
|
|
115
|
+
function toolResultText(result, error, extra) {
|
|
116
|
+
const parts = [JSON.stringify(error ?? result ?? null)];
|
|
117
|
+
for (const block of extra ?? []) {
|
|
118
|
+
if (block.type === "text" && "text" in block && typeof block.text === "string")
|
|
119
|
+
parts.push(block.text);
|
|
120
|
+
}
|
|
121
|
+
return parts.join("\n");
|
|
122
|
+
}
|
|
123
|
+
function capSummaryBytes(summary, maxBytes) {
|
|
124
|
+
const bytes = estimateTextBytes(summary);
|
|
125
|
+
if (bytes <= maxBytes)
|
|
126
|
+
return summary;
|
|
127
|
+
const encoded = new TextEncoder().encode(summary);
|
|
128
|
+
const suffix = new TextEncoder().encode("…");
|
|
129
|
+
let end = Math.max(0, maxBytes - suffix.length);
|
|
130
|
+
while (end > 0 && (encoded[end] & 0xc0) === 0x80)
|
|
131
|
+
end--;
|
|
132
|
+
return new TextDecoder().decode(encoded.slice(0, end)) + "…";
|
|
133
|
+
}
|
|
134
|
+
function inferToolResultTurns(history) {
|
|
135
|
+
const turns = new Array(history.length).fill(1);
|
|
136
|
+
let providerTurn = 0;
|
|
137
|
+
let toolTurn = 1;
|
|
138
|
+
for (let index = 0; index < history.length; index += 1) {
|
|
139
|
+
const message = history[index];
|
|
140
|
+
const stamped = readToolResultTurn(message.metadata);
|
|
141
|
+
if (message.role === "assistant") {
|
|
142
|
+
providerTurn += 1;
|
|
143
|
+
toolTurn = providerTurn;
|
|
144
|
+
}
|
|
145
|
+
if (message.role === "tool") {
|
|
146
|
+
turns[index] = stamped ?? toolTurn;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return turns;
|
|
150
|
+
}
|
|
151
|
+
function readToolResultTurn(metadata) {
|
|
152
|
+
const value = metadata?.[TOOL_RESULT_FOLD_TURN_METADATA_KEY];
|
|
153
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
154
|
+
}
|
|
155
|
+
function assertPositiveInt(value, name, min, max) {
|
|
156
|
+
if (!Number.isSafeInteger(value) || value < min || value > max) {
|
|
157
|
+
throw new TypeError(`toolResultFold.${name} must be a safe integer from ${min} to ${max}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function throwIfAborted(signal) {
|
|
161
|
+
if (signal?.aborted)
|
|
162
|
+
throw signal.reason instanceof Error ? signal.reason : new Error("Tool result fold aborted");
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=tool-result-fold.js.map
|
package/docs/0.1.0-readiness.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 0.1.0 / 1.0 Readiness Gates
|
|
2
2
|
|
|
3
|
-
Status: **0.0.
|
|
3
|
+
Status: **0.0.22** is the current release line (Phase 5 third-party behavior integrations); **1.0** readiness remains operator-gated, not automatic.
|
|
4
4
|
|
|
5
5
|
This page distills runnable readiness gates into one command-per-gate table. The
|
|
6
6
|
**Last evidence** column records the 2026-07-26 **0.0.16** baseline snapshot
|
|
@@ -14,13 +14,13 @@ Evidence trail: [`docs/review-coverage-2026-07-26-phase-11.md`](./review-coverag
|
|
|
14
14
|
(addenda 0–9), [`docs/release-and-install.md`](./release-and-install.md),
|
|
15
15
|
[`docs/migration.md`](./migration.md), [`docs/performance.md`](./performance.md).
|
|
16
16
|
|
|
17
|
-
## Current line (0.0.
|
|
17
|
+
## Current line (0.0.21)
|
|
18
18
|
|
|
19
19
|
| Item | Status |
|
|
20
20
|
|---|---|
|
|
21
|
-
| Published graph | **44** publishable manifests at **0.0.
|
|
22
|
-
| Phase
|
|
23
|
-
| Docs tripwires | `node --test dist/__tests__/docs.test.js` — migration section `0.0.
|
|
21
|
+
| Published graph | **44** publishable manifests at **0.0.21** (`docs/release-and-install.md`) |
|
|
22
|
+
| Phase 4 coding-tool gaps | `outputMode`, bounded `glob`, optional read-before-write, bounded `delete`/`move`, approval/sandbox wiring |
|
|
23
|
+
| Docs tripwires | `node --test dist/__tests__/docs.test.js` — migration section `0.0.20 → 0.0.21 coding-tool capability gaps` documents intentional breaks |
|
|
24
24
|
| Readiness table below | **0.0.16 measured values** — refresh evidence columns when 1.0 RC gates are recorded |
|
|
25
25
|
|
|
26
26
|
## Gate table
|
|
@@ -51,6 +51,8 @@ string | Message | readonly Message[]
|
|
|
51
51
|
|
|
52
52
|
`RunOptions.model` can override the request model for a run. Model overrides append a `model_change` entry. `AgentConfig.inputLayout` selects the default input assembly layout (`"cache_aware"` by default, or opt-in `"legacy"`); `RunOptions.inputLayout` wins for one run. `AgentConfig.providerOptions`/`RunOptions.providerOptions` supply generic provider request options; `timeoutMs`, `maxRetries`, and `maxRetryDelayMs` are deprecated inert provider-level hints in first-party providers. Use `RunOptions.signal`/host abort controllers for timeouts and `AgentConfig.retry`/`RunOptions.retry` for retry. `AgentConfig.providerRequestPolicies`/`RunOptions.providerRequestPolicies` run before `AIProvider.generate()` and before `provider_request` middleware. `AgentConfig.systemPrompt` and `RunOptions.systemPrompt` add explicit layered system prompt contributions; `RunOptions.systemPrompt: false` disables configured prompt layers for that run while keeping `AgentConfig.instructions` as the base path. `RunOptions.compaction` can enable auto-compaction for that run or use `false` to disable configured auto-compaction. `RunOptions.retry` can enable provider-turn retry for that run or use `false` to disable configured retry. `RunOptions.metadata` is merged with agent/session metadata for assembly, provider requests, and tool contexts. Deprecated `RunOptions.maxToolRounds` narrows `limits.maxToolRounds`. `RunOptions.signal` is bridged into the per-run abort signal passed to assembly, providers, tools, auto-compaction, and retry backoff.
|
|
53
53
|
|
|
54
|
+
`RunOptions.activeSkills` selects named skills from a configured `SkillRegistry`; `RunOptions.skills` replaces a plain `Skill[]` config for one run. When `AgentConfig.skills` is a registry and neither is set, **no skills activate** unless `activateAllSkills: true` (run or agent). `skillsDisclosure` (`"progressive"` default, `"eager"` opt-in; run wins) controls catalog vs full instruction bodies; the session-owned `LoadedSkillSet` is populated by `load_skill` when the host registers `createLoadSkillTool`. `toolResultFold` (off unless the host supplies `summarize`) optionally folds aged large tool results in provider input only. See [Context and skills](context-and-skills.md).
|
|
55
|
+
|
|
54
56
|
## Outputs / response / events
|
|
55
57
|
|
|
56
58
|
`session.run()` / `session.prompt()` resolve to an `AgentRunResult` with `sessionId`, `runId`, `status`, `text`, `content`, optional `message`/`usage`/`leafId`, and terminal `error`/`abortReason` when applicable. Callers may ignore the return value. Failed and aborted runs still emit their terminal events, then reject with `AgentRunError` whose `.result` carries the same shape.
|
package/docs/caveman.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Caveman behavior integration
|
|
2
|
+
|
|
3
|
+
## What it does
|
|
4
|
+
|
|
5
|
+
`@arnilo/prism-caveman` is an optional package that wires [juliusbrussee/caveman](https://github.com/juliusbrussee/caveman) into Prism contribution contracts.
|
|
6
|
+
|
|
7
|
+
It registers upstream skills and commands, injects active level prompt slices via `InstructionInjector`, and persists level as session custom `caveman-level` entries. Import and extension `setup` without a resolvable upstream path fail closed with a bounded redacted error and register zero contributions.
|
|
8
|
+
|
|
9
|
+
Upstream prompt fragments, skill bodies, and rules load from the host-supplied upstream checkout — Prism does not reimplement or vendor Caveman content.
|
|
10
|
+
|
|
11
|
+
## When to use it
|
|
12
|
+
|
|
13
|
+
Use it when a host wants terse token-efficient communication modes (`lite`, `full`, `ultra`, wenyan variants, `micro`) with upstream Caveman skills (`caveman-commit`, `caveman-review`, `caveman-stats`, `caveman-compress`, `caveman-help`, `cavecrew`) in a Prism extension kernel.
|
|
14
|
+
|
|
15
|
+
Skip it when you do not have a local Caveman checkout (Caveman is not published on npm) or when you only need progressive skill catalog without mode injection.
|
|
16
|
+
|
|
17
|
+
Pair with Phase 3 progressive disclosure: register `createLoadSkillTool` and keep `skillsDisclosure: "progressive"` so full `SKILL.md` bodies stay catalog-only; mode slices come from the `caveman-mode` injector, not eager skill bodies.
|
|
18
|
+
|
|
19
|
+
## Inputs / request
|
|
20
|
+
|
|
21
|
+
`createCavemanExtension(options)`:
|
|
22
|
+
|
|
23
|
+
| Field | Type | Required | Purpose |
|
|
24
|
+
| --- | --- | --- | --- |
|
|
25
|
+
| `upstreamPath` | `string` | yes | Absolute path to a Caveman checkout containing `skills/`. |
|
|
26
|
+
| `defaultLevel` | `CavemanLevel` | no | Initial level when no session entry exists (default upstream: `full`). |
|
|
27
|
+
| `showStatus` | `boolean` | no | Emit `caveman:status` extension events on level changes. |
|
|
28
|
+
| `appendEntry` | `(entry, opts?) => Promise<void>` | yes | Host session append (OM `attach` pattern). |
|
|
29
|
+
| `getEntries` | `() => readonly SessionEntry[] \| Promise<...>` | yes | Current branch entries for level restore. |
|
|
30
|
+
| `configPath` | `string` | no | Bounded local config file for `defaultLevel` / `showStatus`. |
|
|
31
|
+
|
|
32
|
+
`CavemanLevel`: `off` \| `lite` \| `full` \| `ultra` \| `wenyan-lite` \| `wenyan` \| `wenyan-ultra` \| `micro`.
|
|
33
|
+
|
|
34
|
+
Session custom entry shape:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{ "kind": "custom", "data": { "type": "caveman-level", "level": "full" } }
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Registered skills: `caveman`, `caveman-commit`, `caveman-review`, `caveman-stats`, `caveman-compress`, `caveman-help`, `cavecrew`.
|
|
41
|
+
|
|
42
|
+
Registered commands: `caveman`, `caveman-init`, `caveman-commit`, `caveman-review`, `caveman-stats`, `caveman-compress`.
|
|
43
|
+
|
|
44
|
+
## Outputs / response / events
|
|
45
|
+
|
|
46
|
+
| Export | Purpose |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| `createCavemanExtension(options)` | Returns an inert `Extension` until `kernel.load([...])`. |
|
|
49
|
+
| `caveman-mode` injector | `InstructionInjector` — upstream filtered `skills/caveman/SKILL.md` slice when level ≠ `off`. |
|
|
50
|
+
| `caveman` command | Set level (`/caveman lite\|full\|ultra\|wenyan\|micro\|off`) or toggle `off`↔`full`. |
|
|
51
|
+
| Alias commands | Dispatch `{ skill, dispatch: "load_skill" }` metadata for companion skills. |
|
|
52
|
+
| `caveman:status` event | Optional metadata when `showStatus: true`. |
|
|
53
|
+
|
|
54
|
+
Deactivation phrases `stop caveman` and `normal mode` clear active injection without erasing session history.
|
|
55
|
+
|
|
56
|
+
## Request/response example
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{ "command": "caveman", "args": { "level": "ultra" }, "sessionId": "s1" }
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{ "kind": "custom", "data": { "type": "caveman-level", "level": "ultra" } }
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Implementation example
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { createCavemanExtension } from "@arnilo/prism-caveman";
|
|
70
|
+
import {
|
|
71
|
+
createExtensionKernel,
|
|
72
|
+
createLoadSkillTool,
|
|
73
|
+
createLoadedSkillSet,
|
|
74
|
+
createMemorySessionStore,
|
|
75
|
+
createSkillRegistry,
|
|
76
|
+
createSessionEntry,
|
|
77
|
+
} from "@arnilo/prism";
|
|
78
|
+
|
|
79
|
+
const store = createMemorySessionStore();
|
|
80
|
+
const callbacks = {
|
|
81
|
+
appendEntry: async (entry, options) => store.append(entry, options),
|
|
82
|
+
getEntries: async () => store.list("s1"),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const kernel = createExtensionKernel({ errorPolicy: "throw" });
|
|
86
|
+
await kernel.load([
|
|
87
|
+
createCavemanExtension({
|
|
88
|
+
upstreamPath: "/path/to/juliusbrussee-caveman",
|
|
89
|
+
defaultLevel: "full",
|
|
90
|
+
...callbacks,
|
|
91
|
+
}),
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
const registry = createSkillRegistry(kernel.registries.skills.list());
|
|
95
|
+
const loaded = createLoadedSkillSet();
|
|
96
|
+
const loadSkill = createLoadSkillTool({ registry, loaded });
|
|
97
|
+
|
|
98
|
+
await kernel.registries.commands.get("caveman")!.execute({ level: "lite" }, { sessionId: "s1" });
|
|
99
|
+
// Select instructionInjectors: ["caveman-mode"] on runs that should receive level slices.
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
See `examples/caveman-ponytail.ts` for progressive catalog + `load_skill` wiring with fixture upstream trees (network-free).
|
|
103
|
+
|
|
104
|
+
## Extension and configuration notes
|
|
105
|
+
|
|
106
|
+
- Import alone registers nothing and starts no timers, watchers, or network I/O (`sideEffects: false`).
|
|
107
|
+
- `kernel.load` calls `setup`, which resolves upstream first; failure throws before any `register*`.
|
|
108
|
+
- Level restore scans `getEntries()` for the latest `data.type === "caveman-level"` — same OM attach pattern; core does not auto-emit `session_start`.
|
|
109
|
+
- Host must register `createLoadSkillTool` and pass `skillsDisclosure: "progressive"` for catalog-only skill bodies.
|
|
110
|
+
- `caveman-stats` dispatches skill metadata only; full stats need host session-log integration.
|
|
111
|
+
- `caveman-init` returns upstream guidance text; it does not write files in the host repo.
|
|
112
|
+
- No TUI status bar; optional `caveman:status` events for host UI.
|
|
113
|
+
|
|
114
|
+
## Security and performance notes
|
|
115
|
+
|
|
116
|
+
- Upstream `SKILL.md` and injected text are untrusted host-supplied content; reads are size-bounded (`MAX_SKILL_FILE_BYTES` 256 KiB, `MAX_INJECTED_INSTRUCTION_BYTES` 32 KiB).
|
|
117
|
+
- Config read/write is bounded (`MAX_CONFIG_FILE_BYTES` 16 KiB) at host-owned `configPath` only.
|
|
118
|
+
- Errors redact home directories and absolute paths.
|
|
119
|
+
- Setup is O(skills) directory scan; mode read/write is O(1) per change; injection is O(1) upstream lookup per turn.
|
|
120
|
+
- Session custom entries respect host session ownership and redaction policies.
|
|
121
|
+
|
|
122
|
+
## Related APIs
|
|
123
|
+
|
|
124
|
+
- [Ponytail behavior integration](ponytail.md): complementary lazy-minimalism mode package.
|
|
125
|
+
- [Extension kernel and event bus](extensions.md): `kernel.load` and contribution registration.
|
|
126
|
+
- [Context and skills](context-and-skills.md): progressive disclosure + `createLoadSkillTool`.
|
|
127
|
+
- [Instruction injection](instruction-injection.md): `caveman-mode` injector selection.
|
|
128
|
+
- [Observational memory compaction package](compaction-observational-memory.md): `appendEntry` / `getEntries` attach precedent.
|
|
129
|
+
- [Migration guide](migration.md): `0.0.21 → 0.0.22` install and opt-in notes.
|