@code-yeongyu/senpi 2026.5.14 → 2026.5.15-3
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 +1110 -1175
- package/README.md +1 -2
- package/dist/core/agent-session.d.ts +9 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +109 -7
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/dynamic-prompt/verification.d.ts +31 -0
- package/dist/core/dynamic-prompt/verification.d.ts.map +1 -1
- package/dist/core/dynamic-prompt/verification.js +41 -0
- package/dist/core/dynamic-prompt/verification.js.map +1 -1
- package/dist/core/extensions/builtin/compaction/context-reduction.d.ts +97 -0
- package/dist/core/extensions/builtin/compaction/context-reduction.d.ts.map +1 -0
- package/dist/core/extensions/builtin/compaction/context-reduction.js +420 -0
- package/dist/core/extensions/builtin/compaction/context-reduction.js.map +1 -0
- package/dist/core/extensions/builtin/compaction/index.d.ts.map +1 -1
- package/dist/core/extensions/builtin/compaction/index.js +168 -31
- package/dist/core/extensions/builtin/compaction/index.js.map +1 -1
- package/dist/core/extensions/builtin/compaction/openai-remote.d.ts +197 -0
- package/dist/core/extensions/builtin/compaction/openai-remote.d.ts.map +1 -0
- package/dist/core/extensions/builtin/compaction/openai-remote.js +690 -0
- package/dist/core/extensions/builtin/compaction/openai-remote.js.map +1 -0
- package/dist/core/extensions/builtin/compaction/prompts.d.ts +3 -3
- package/dist/core/extensions/builtin/compaction/prompts.d.ts.map +1 -1
- package/dist/core/extensions/builtin/compaction/prompts.js +0 -22
- package/dist/core/extensions/builtin/compaction/prompts.js.map +1 -1
- package/dist/core/extensions/builtin/compaction/repair-tool-pairs.d.ts +4 -0
- package/dist/core/extensions/builtin/compaction/repair-tool-pairs.d.ts.map +1 -0
- package/dist/core/extensions/builtin/compaction/repair-tool-pairs.js +48 -0
- package/dist/core/extensions/builtin/compaction/repair-tool-pairs.js.map +1 -0
- package/dist/core/extensions/builtin/compaction/speculative.d.ts +3 -1
- package/dist/core/extensions/builtin/compaction/speculative.d.ts.map +1 -1
- package/dist/core/extensions/builtin/compaction/speculative.js +82 -33
- package/dist/core/extensions/builtin/compaction/speculative.js.map +1 -1
- package/dist/core/extensions/builtin/compaction/todo-bridge.d.ts +8 -0
- package/dist/core/extensions/builtin/compaction/todo-bridge.d.ts.map +1 -1
- package/dist/core/extensions/builtin/compaction/todo-bridge.js +12 -6
- package/dist/core/extensions/builtin/compaction/todo-bridge.js.map +1 -1
- package/dist/core/extensions/builtin/index.d.ts.map +1 -1
- package/dist/core/extensions/builtin/index.js +0 -2
- package/dist/core/extensions/builtin/index.js.map +1 -1
- package/dist/core/extensions/builtin/openai-web-search/index.d.ts.map +1 -1
- package/dist/core/extensions/builtin/openai-web-search/index.js +26 -1
- package/dist/core/extensions/builtin/openai-web-search/index.js.map +1 -1
- package/dist/core/extensions/builtin/permission-system/prompt.d.ts.map +1 -1
- package/dist/core/extensions/builtin/permission-system/prompt.js +0 -5
- package/dist/core/extensions/builtin/permission-system/prompt.js.map +1 -1
- package/dist/core/extensions/builtin/system-messages.d.ts +7 -7
- package/dist/core/extensions/builtin/system-messages.d.ts.map +1 -1
- package/dist/core/extensions/builtin/system-messages.js +10 -10
- package/dist/core/extensions/builtin/system-messages.js.map +1 -1
- package/dist/core/extensions/builtin/todotools/continuation/prompt.d.ts +1 -1
- package/dist/core/extensions/builtin/todotools/continuation/prompt.d.ts.map +1 -1
- package/dist/core/extensions/builtin/todotools/continuation/prompt.js +1 -1
- package/dist/core/extensions/builtin/todotools/continuation/prompt.js.map +1 -1
- package/dist/core/extensions/builtin/todotools/state.d.ts +1 -1
- package/dist/core/extensions/builtin/todotools/state.d.ts.map +1 -1
- package/dist/core/extensions/builtin/todotools/state.js +1 -1
- package/dist/core/extensions/builtin/todotools/state.js.map +1 -1
- package/dist/core/extensions/builtin/todotools/system-messages.d.ts +3 -3
- package/dist/core/extensions/builtin/todotools/system-messages.d.ts.map +1 -1
- package/dist/core/extensions/builtin/todotools/system-messages.js +6 -6
- package/dist/core/extensions/builtin/todotools/system-messages.js.map +1 -1
- package/dist/core/extensions/builtin/tool-pair-guard/index.d.ts +1 -1
- package/dist/core/extensions/builtin/tool-pair-guard/index.d.ts.map +1 -1
- package/dist/core/extensions/builtin/tool-pair-guard/index.js +8 -4
- package/dist/core/extensions/builtin/tool-pair-guard/index.js.map +1 -1
- package/dist/core/extensions/builtin/tool-pair-guard/sanitize-openai-chat-completions-payload.d.ts +3 -0
- package/dist/core/extensions/builtin/tool-pair-guard/sanitize-openai-chat-completions-payload.d.ts.map +1 -0
- package/dist/core/extensions/builtin/tool-pair-guard/sanitize-openai-chat-completions-payload.js +89 -0
- package/dist/core/extensions/builtin/tool-pair-guard/sanitize-openai-chat-completions-payload.js.map +1 -0
- package/dist/core/extensions/builtin/tool-pair-guard/sanitize-openai-responses-payload.d.ts +3 -0
- package/dist/core/extensions/builtin/tool-pair-guard/sanitize-openai-responses-payload.d.ts.map +1 -0
- package/dist/core/extensions/builtin/tool-pair-guard/sanitize-openai-responses-payload.js +122 -0
- package/dist/core/extensions/builtin/tool-pair-guard/sanitize-openai-responses-payload.js.map +1 -0
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +2 -0
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +3 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +18 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +22 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/messages.d.ts +3 -3
- package/dist/core/messages.d.ts.map +1 -1
- package/dist/core/messages.js +5 -10
- package/dist/core/messages.js.map +1 -1
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +1 -0
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/sdk.d.ts +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +7 -22
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +1 -1
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +0 -5
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/thinking-levels.d.ts +6 -0
- package/dist/core/thinking-levels.d.ts.map +1 -0
- package/dist/core/thinking-levels.js +36 -0
- package/dist/core/thinking-levels.js.map +1 -0
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +15 -1
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.js +20 -2
- package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.js +3 -1
- package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +8 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +137 -49
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/working-status.d.ts +15 -0
- package/dist/modes/interactive/working-status.d.ts.map +1 -0
- package/dist/modes/interactive/working-status.js +60 -0
- package/dist/modes/interactive/working-status.js.map +1 -0
- package/docs/extensions.md +0 -1
- package/docs/index.md +0 -1
- package/docs/sdk.md +0 -1
- package/docs/settings.md +1 -29
- package/docs/termux.md +2 -2
- package/docs/usage.md +1 -1
- package/examples/README.md +1 -1
- package/examples/extensions/README.md +0 -1
- package/examples/extensions/overlay-qa-tests.ts +1 -1
- package/package.json +4 -4
- package/dist/core/extensions/builtin/background-task/cancel-tool.d.ts +0 -10
- package/dist/core/extensions/builtin/background-task/cancel-tool.d.ts.map +0 -1
- package/dist/core/extensions/builtin/background-task/cancel-tool.js +0 -109
- package/dist/core/extensions/builtin/background-task/cancel-tool.js.map +0 -1
- package/dist/core/extensions/builtin/background-task/index.d.ts +0 -3
- package/dist/core/extensions/builtin/background-task/index.d.ts.map +0 -1
- package/dist/core/extensions/builtin/background-task/index.js +0 -207
- package/dist/core/extensions/builtin/background-task/index.js.map +0 -1
- package/dist/core/extensions/builtin/background-task/manager.d.ts +0 -17
- package/dist/core/extensions/builtin/background-task/manager.d.ts.map +0 -1
- package/dist/core/extensions/builtin/background-task/manager.js +0 -114
- package/dist/core/extensions/builtin/background-task/manager.js.map +0 -1
- package/dist/core/extensions/builtin/background-task/notification.d.ts +0 -22
- package/dist/core/extensions/builtin/background-task/notification.d.ts.map +0 -1
- package/dist/core/extensions/builtin/background-task/notification.js +0 -105
- package/dist/core/extensions/builtin/background-task/notification.js.map +0 -1
- package/dist/core/extensions/builtin/background-task/output-tool.d.ts +0 -11
- package/dist/core/extensions/builtin/background-task/output-tool.d.ts.map +0 -1
- package/dist/core/extensions/builtin/background-task/output-tool.js +0 -127
- package/dist/core/extensions/builtin/background-task/output-tool.js.map +0 -1
- package/dist/core/extensions/builtin/background-task/spawner.d.ts +0 -8
- package/dist/core/extensions/builtin/background-task/spawner.d.ts.map +0 -1
- package/dist/core/extensions/builtin/background-task/spawner.js +0 -207
- package/dist/core/extensions/builtin/background-task/spawner.js.map +0 -1
- package/dist/core/extensions/builtin/background-task/task-tool.d.ts +0 -20
- package/dist/core/extensions/builtin/background-task/task-tool.d.ts.map +0 -1
- package/dist/core/extensions/builtin/background-task/task-tool.js +0 -302
- package/dist/core/extensions/builtin/background-task/task-tool.js.map +0 -1
- package/dist/core/extensions/builtin/background-task/types.d.ts +0 -72
- package/dist/core/extensions/builtin/background-task/types.d.ts.map +0 -1
- package/dist/core/extensions/builtin/background-task/types.js +0 -32
- package/dist/core/extensions/builtin/background-task/types.js.map +0 -1
- package/docs/agents.md +0 -348
- package/examples/extensions/subagent/README.md +0 -172
- package/examples/extensions/subagent/agents/planner.md +0 -37
- package/examples/extensions/subagent/agents/reviewer.md +0 -35
- package/examples/extensions/subagent/agents/scout.md +0 -50
- package/examples/extensions/subagent/agents/worker.md +0 -24
- package/examples/extensions/subagent/agents.ts +0 -126
- package/examples/extensions/subagent/index.ts +0 -987
- package/examples/extensions/subagent/prompts/implement-and-review.md +0 -10
- package/examples/extensions/subagent/prompts/implement.md +0 -10
- package/examples/extensions/subagent/prompts/scout-and-plan.md +0 -9
|
@@ -1,2 +1,33 @@
|
|
|
1
|
+
export type TestDisciplineRule = {
|
|
2
|
+
id: "deterministic-tests" | "fixed-wait-ban" | "event-timeout-pattern" | "mock-contract-integrity" | "prompt-behavior-coverage" | "single-pass-runner";
|
|
3
|
+
concern: "test-determinism" | "async-test-orchestration" | "mock-contracts" | "prompt-tests" | "test-runner";
|
|
4
|
+
directive: string;
|
|
5
|
+
};
|
|
6
|
+
export declare const TEST_DISCIPLINE_RULES: readonly [{
|
|
7
|
+
readonly id: "deterministic-tests";
|
|
8
|
+
readonly concern: "test-determinism";
|
|
9
|
+
readonly directive: "When you read or edit test code, treat nondeterminism as a bug; tests must not pass by timing luck.";
|
|
10
|
+
}, {
|
|
11
|
+
readonly id: "fixed-wait-ban";
|
|
12
|
+
readonly concern: "async-test-orchestration";
|
|
13
|
+
readonly directive: "Unless time itself is the behavior under test, fixed sleeps, polling delays, and wait-for-time patterns are forbidden.";
|
|
14
|
+
}, {
|
|
15
|
+
readonly id: "event-timeout-pattern";
|
|
16
|
+
readonly concern: "async-test-orchestration";
|
|
17
|
+
readonly directive: "For async behavior, subscribe to the exact event or state change before triggering the action, then await that signal with a bounded timeout.";
|
|
18
|
+
}, {
|
|
19
|
+
readonly id: "mock-contract-integrity";
|
|
20
|
+
readonly concern: "mock-contracts";
|
|
21
|
+
readonly directive: "Mocks must preserve the contract being asserted; do not isolate so heavily that the integration under test cannot fail.";
|
|
22
|
+
}, {
|
|
23
|
+
readonly id: "prompt-behavior-coverage";
|
|
24
|
+
readonly concern: "prompt-tests";
|
|
25
|
+
readonly directive: "Prompt tests must assert behavior, decisions, structure, or parsed rule data rather than merely pinning an exact prompt sentence.";
|
|
26
|
+
}, {
|
|
27
|
+
readonly id: "single-pass-runner";
|
|
28
|
+
readonly concern: "test-runner";
|
|
29
|
+
readonly directive: "Run the relevant test command once and make that pass reliable; for Bun test targets, bun test must pass in a single run.";
|
|
30
|
+
}];
|
|
31
|
+
export declare function buildTestDisciplineSection(): string;
|
|
1
32
|
export declare function buildVerificationSection(): string;
|
|
2
33
|
//# sourceMappingURL=verification.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verification.d.ts","sourceRoot":"","sources":["../../../src/core/dynamic-prompt/verification.ts"],"names":[],"mappings":"AAAA,wBAAgB,wBAAwB,IAAI,MAAM,
|
|
1
|
+
{"version":3,"file":"verification.d.ts","sourceRoot":"","sources":["../../../src/core/dynamic-prompt/verification.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAAG;IAChC,EAAE,EACC,qBAAqB,GACrB,gBAAgB,GAChB,uBAAuB,GACvB,yBAAyB,GACzB,0BAA0B,GAC1B,oBAAoB,CAAC;IACxB,OAAO,EAAE,kBAAkB,GAAG,0BAA0B,GAAG,gBAAgB,GAAG,cAAc,GAAG,aAAa,CAAC;IAC7G,SAAS,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;EAoCgB,CAAC;AAEnD,wBAAgB,0BAA0B,IAAI,MAAM,CAMnD;AAED,wBAAgB,wBAAwB,IAAI,MAAM,CAYjD","sourcesContent":["export type TestDisciplineRule = {\n\tid:\n\t\t| \"deterministic-tests\"\n\t\t| \"fixed-wait-ban\"\n\t\t| \"event-timeout-pattern\"\n\t\t| \"mock-contract-integrity\"\n\t\t| \"prompt-behavior-coverage\"\n\t\t| \"single-pass-runner\";\n\tconcern: \"test-determinism\" | \"async-test-orchestration\" | \"mock-contracts\" | \"prompt-tests\" | \"test-runner\";\n\tdirective: string;\n};\n\nexport const TEST_DISCIPLINE_RULES = [\n\t{\n\t\tid: \"deterministic-tests\",\n\t\tconcern: \"test-determinism\",\n\t\tdirective: \"When you read or edit test code, treat nondeterminism as a bug; tests must not pass by timing luck.\",\n\t},\n\t{\n\t\tid: \"fixed-wait-ban\",\n\t\tconcern: \"async-test-orchestration\",\n\t\tdirective:\n\t\t\t\"Unless time itself is the behavior under test, fixed sleeps, polling delays, and wait-for-time patterns are forbidden.\",\n\t},\n\t{\n\t\tid: \"event-timeout-pattern\",\n\t\tconcern: \"async-test-orchestration\",\n\t\tdirective:\n\t\t\t\"For async behavior, subscribe to the exact event or state change before triggering the action, then await that signal with a bounded timeout.\",\n\t},\n\t{\n\t\tid: \"mock-contract-integrity\",\n\t\tconcern: \"mock-contracts\",\n\t\tdirective:\n\t\t\t\"Mocks must preserve the contract being asserted; do not isolate so heavily that the integration under test cannot fail.\",\n\t},\n\t{\n\t\tid: \"prompt-behavior-coverage\",\n\t\tconcern: \"prompt-tests\",\n\t\tdirective:\n\t\t\t\"Prompt tests must assert behavior, decisions, structure, or parsed rule data rather than merely pinning an exact prompt sentence.\",\n\t},\n\t{\n\t\tid: \"single-pass-runner\",\n\t\tconcern: \"test-runner\",\n\t\tdirective:\n\t\t\t\"Run the relevant test command once and make that pass reliable; for Bun test targets, bun test must pass in a single run.\",\n\t},\n] as const satisfies readonly TestDisciplineRule[];\n\nexport function buildTestDisciplineSection(): string {\n\tconst lines = [\"### Test Discipline\"];\n\tfor (const rule of TEST_DISCIPLINE_RULES) {\n\t\tlines.push(`- ${rule.directive}`);\n\t}\n\treturn lines.join(\"\\n\");\n}\n\nexport function buildVerificationSection(): string {\n\treturn `## Verification\n\nTier the scope, never the rigor.\n\n- V1 — single-file non-behavioral edits: diagnostics on that file. Done.\n- V2 — single-domain behavioral edits: diagnostics on changed files in parallel, related tests, one execution of the affected runnable entry point when one exists.\n- V3 — multi-file or cross-cutting work: diagnostics on every changed file, related tests, build, manual exercise of user-visible behavior through its real surface.\n\n${buildTestDisciplineSection()}\n\n\"Should pass\" is not verification. Reporting clean output without running the validator is a violation. Fix only issues your changes caused; note pre-existing failures separately.`;\n}\n"]}
|
|
@@ -1,3 +1,42 @@
|
|
|
1
|
+
export const TEST_DISCIPLINE_RULES = [
|
|
2
|
+
{
|
|
3
|
+
id: "deterministic-tests",
|
|
4
|
+
concern: "test-determinism",
|
|
5
|
+
directive: "When you read or edit test code, treat nondeterminism as a bug; tests must not pass by timing luck.",
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
id: "fixed-wait-ban",
|
|
9
|
+
concern: "async-test-orchestration",
|
|
10
|
+
directive: "Unless time itself is the behavior under test, fixed sleeps, polling delays, and wait-for-time patterns are forbidden.",
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: "event-timeout-pattern",
|
|
14
|
+
concern: "async-test-orchestration",
|
|
15
|
+
directive: "For async behavior, subscribe to the exact event or state change before triggering the action, then await that signal with a bounded timeout.",
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: "mock-contract-integrity",
|
|
19
|
+
concern: "mock-contracts",
|
|
20
|
+
directive: "Mocks must preserve the contract being asserted; do not isolate so heavily that the integration under test cannot fail.",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "prompt-behavior-coverage",
|
|
24
|
+
concern: "prompt-tests",
|
|
25
|
+
directive: "Prompt tests must assert behavior, decisions, structure, or parsed rule data rather than merely pinning an exact prompt sentence.",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "single-pass-runner",
|
|
29
|
+
concern: "test-runner",
|
|
30
|
+
directive: "Run the relevant test command once and make that pass reliable; for Bun test targets, bun test must pass in a single run.",
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
export function buildTestDisciplineSection() {
|
|
34
|
+
const lines = ["### Test Discipline"];
|
|
35
|
+
for (const rule of TEST_DISCIPLINE_RULES) {
|
|
36
|
+
lines.push(`- ${rule.directive}`);
|
|
37
|
+
}
|
|
38
|
+
return lines.join("\n");
|
|
39
|
+
}
|
|
1
40
|
export function buildVerificationSection() {
|
|
2
41
|
return `## Verification
|
|
3
42
|
|
|
@@ -7,6 +46,8 @@ Tier the scope, never the rigor.
|
|
|
7
46
|
- V2 — single-domain behavioral edits: diagnostics on changed files in parallel, related tests, one execution of the affected runnable entry point when one exists.
|
|
8
47
|
- V3 — multi-file or cross-cutting work: diagnostics on every changed file, related tests, build, manual exercise of user-visible behavior through its real surface.
|
|
9
48
|
|
|
49
|
+
${buildTestDisciplineSection()}
|
|
50
|
+
|
|
10
51
|
"Should pass" is not verification. Reporting clean output without running the validator is a violation. Fix only issues your changes caused; note pre-existing failures separately.`;
|
|
11
52
|
}
|
|
12
53
|
//# sourceMappingURL=verification.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verification.js","sourceRoot":"","sources":["../../../src/core/dynamic-prompt/verification.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,wBAAwB,GAAW;IAClD,OAAO;;;;;;;;
|
|
1
|
+
{"version":3,"file":"verification.js","sourceRoot":"","sources":["../../../src/core/dynamic-prompt/verification.ts"],"names":[],"mappings":"AAYA,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACpC;QACC,EAAE,EAAE,qBAAqB;QACzB,OAAO,EAAE,kBAAkB;QAC3B,SAAS,EAAE,qGAAqG;KAChH;IACD;QACC,EAAE,EAAE,gBAAgB;QACpB,OAAO,EAAE,0BAA0B;QACnC,SAAS,EACR,wHAAwH;KACzH;IACD;QACC,EAAE,EAAE,uBAAuB;QAC3B,OAAO,EAAE,0BAA0B;QACnC,SAAS,EACR,+IAA+I;KAChJ;IACD;QACC,EAAE,EAAE,yBAAyB;QAC7B,OAAO,EAAE,gBAAgB;QACzB,SAAS,EACR,yHAAyH;KAC1H;IACD;QACC,EAAE,EAAE,0BAA0B;QAC9B,OAAO,EAAE,cAAc;QACvB,SAAS,EACR,mIAAmI;KACpI;IACD;QACC,EAAE,EAAE,oBAAoB;QACxB,OAAO,EAAE,aAAa;QACtB,SAAS,EACR,2HAA2H;KAC5H;CACgD,CAAC;AAEnD,MAAM,UAAU,0BAA0B,GAAW;IACpD,MAAM,KAAK,GAAG,CAAC,qBAAqB,CAAC,CAAC;IACtC,KAAK,MAAM,IAAI,IAAI,qBAAqB,EAAE,CAAC;QAC1C,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;IACnC,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAAA,CACxB;AAED,MAAM,UAAU,wBAAwB,GAAW;IAClD,OAAO;;;;;;;;EAQN,0BAA0B,EAAE;;oLAEsJ,CAAC;AAAA,CACpL","sourcesContent":["export type TestDisciplineRule = {\n\tid:\n\t\t| \"deterministic-tests\"\n\t\t| \"fixed-wait-ban\"\n\t\t| \"event-timeout-pattern\"\n\t\t| \"mock-contract-integrity\"\n\t\t| \"prompt-behavior-coverage\"\n\t\t| \"single-pass-runner\";\n\tconcern: \"test-determinism\" | \"async-test-orchestration\" | \"mock-contracts\" | \"prompt-tests\" | \"test-runner\";\n\tdirective: string;\n};\n\nexport const TEST_DISCIPLINE_RULES = [\n\t{\n\t\tid: \"deterministic-tests\",\n\t\tconcern: \"test-determinism\",\n\t\tdirective: \"When you read or edit test code, treat nondeterminism as a bug; tests must not pass by timing luck.\",\n\t},\n\t{\n\t\tid: \"fixed-wait-ban\",\n\t\tconcern: \"async-test-orchestration\",\n\t\tdirective:\n\t\t\t\"Unless time itself is the behavior under test, fixed sleeps, polling delays, and wait-for-time patterns are forbidden.\",\n\t},\n\t{\n\t\tid: \"event-timeout-pattern\",\n\t\tconcern: \"async-test-orchestration\",\n\t\tdirective:\n\t\t\t\"For async behavior, subscribe to the exact event or state change before triggering the action, then await that signal with a bounded timeout.\",\n\t},\n\t{\n\t\tid: \"mock-contract-integrity\",\n\t\tconcern: \"mock-contracts\",\n\t\tdirective:\n\t\t\t\"Mocks must preserve the contract being asserted; do not isolate so heavily that the integration under test cannot fail.\",\n\t},\n\t{\n\t\tid: \"prompt-behavior-coverage\",\n\t\tconcern: \"prompt-tests\",\n\t\tdirective:\n\t\t\t\"Prompt tests must assert behavior, decisions, structure, or parsed rule data rather than merely pinning an exact prompt sentence.\",\n\t},\n\t{\n\t\tid: \"single-pass-runner\",\n\t\tconcern: \"test-runner\",\n\t\tdirective:\n\t\t\t\"Run the relevant test command once and make that pass reliable; for Bun test targets, bun test must pass in a single run.\",\n\t},\n] as const satisfies readonly TestDisciplineRule[];\n\nexport function buildTestDisciplineSection(): string {\n\tconst lines = [\"### Test Discipline\"];\n\tfor (const rule of TEST_DISCIPLINE_RULES) {\n\t\tlines.push(`- ${rule.directive}`);\n\t}\n\treturn lines.join(\"\\n\");\n}\n\nexport function buildVerificationSection(): string {\n\treturn `## Verification\n\nTier the scope, never the rigor.\n\n- V1 — single-file non-behavioral edits: diagnostics on that file. Done.\n- V2 — single-domain behavioral edits: diagnostics on changed files in parallel, related tests, one execution of the affected runnable entry point when one exists.\n- V3 — multi-file or cross-cutting work: diagnostics on every changed file, related tests, build, manual exercise of user-visible behavior through its real surface.\n\n${buildTestDisciplineSection()}\n\n\"Should pass\" is not verification. Reporting clean output without running the validator is a violation. Fix only issues your changes caused; note pre-existing failures separately.`;\n}\n"]}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic, no-LLM context reductions applied before compaction summarization.
|
|
3
|
+
*
|
|
4
|
+
* Ported from plugsuits' `context-collapse` and `micro-compact` patterns and
|
|
5
|
+
* adapted to the senpi `AgentMessage` shape. Three independent transforms:
|
|
6
|
+
*
|
|
7
|
+
* 1. {@link collapseConsecutiveToolResults} — runs of same-kind read/grep/shell
|
|
8
|
+
* tool result payloads are replaced with a single one-line label so the
|
|
9
|
+
* summarizer pays for the shape, not for the bytes.
|
|
10
|
+
* 2. {@link microCompactAssistantText} — older long assistant text answers are
|
|
11
|
+
* truncated and tagged with a `[response shrunk]` marker.
|
|
12
|
+
* 3. {@link clearOldToolResults} — keep the last N tool results in full, replace
|
|
13
|
+
* older clearable tool result content with `[tool result cleared]`.
|
|
14
|
+
*
|
|
15
|
+
* Each transform is pure (`messages` in → new array out, no in-place mutation
|
|
16
|
+
* beyond freshly cloned messages) and returns aggregated token-savings stats.
|
|
17
|
+
*
|
|
18
|
+
* {@link reduceContextMessages} composes the three transforms in order.
|
|
19
|
+
*/
|
|
20
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
21
|
+
export type CollapsedGroupKind = "read" | "search" | "shell";
|
|
22
|
+
export interface CollapsedGroup {
|
|
23
|
+
type: CollapsedGroupKind;
|
|
24
|
+
count: number;
|
|
25
|
+
label: string;
|
|
26
|
+
originalTokens: number;
|
|
27
|
+
collapsedTokens: number;
|
|
28
|
+
}
|
|
29
|
+
export interface CollapseConsecutiveOptions {
|
|
30
|
+
minGroupSize?: number;
|
|
31
|
+
protectRecentMessages?: number;
|
|
32
|
+
readToolNames?: string[];
|
|
33
|
+
searchToolNames?: string[];
|
|
34
|
+
shellToolNames?: string[];
|
|
35
|
+
}
|
|
36
|
+
export interface CollapseConsecutiveResult {
|
|
37
|
+
messages: AgentMessage[];
|
|
38
|
+
groups: CollapsedGroup[];
|
|
39
|
+
tokensSaved: number;
|
|
40
|
+
}
|
|
41
|
+
export interface MicroCompactAssistantOptions {
|
|
42
|
+
protectRecentTokens?: number;
|
|
43
|
+
maxAssistantTextTokens?: number;
|
|
44
|
+
minSavingsTokens?: number;
|
|
45
|
+
replacementTemplate?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface MicroCompactAssistantResult {
|
|
48
|
+
messages: AgentMessage[];
|
|
49
|
+
tokensSaved: number;
|
|
50
|
+
messagesModified: number;
|
|
51
|
+
}
|
|
52
|
+
export interface ClearOldToolResultsOptions {
|
|
53
|
+
keepRecent?: number;
|
|
54
|
+
clearableToolNames?: string[];
|
|
55
|
+
replacementText?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface ClearOldToolResultsResult {
|
|
58
|
+
messages: AgentMessage[];
|
|
59
|
+
tokensSaved: number;
|
|
60
|
+
toolResultsCleared: number;
|
|
61
|
+
}
|
|
62
|
+
export interface ReduceContextOptions {
|
|
63
|
+
collapse?: false | CollapseConsecutiveOptions;
|
|
64
|
+
shrinkAssistant?: false | MicroCompactAssistantOptions;
|
|
65
|
+
clearToolResults?: false | ClearOldToolResultsOptions;
|
|
66
|
+
}
|
|
67
|
+
export interface ReduceContextResult {
|
|
68
|
+
messages: AgentMessage[];
|
|
69
|
+
tokensSaved: number;
|
|
70
|
+
groupsCollapsed: number;
|
|
71
|
+
messagesShrunk: number;
|
|
72
|
+
toolResultsCleared: number;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Default options passed to {@link reduceContextMessages} when the builtin
|
|
76
|
+
* compaction extension's `context` hook decides to run a reduction pass.
|
|
77
|
+
*
|
|
78
|
+
* Each value is chosen to be strictly more conservative than the corresponding
|
|
79
|
+
* plugsuits default — protect more of the recent tail, raise the per-message
|
|
80
|
+
* shrink threshold, and keep more recent tool results intact — so a single
|
|
81
|
+
* shared default is safe to apply across normal coding sessions without making
|
|
82
|
+
* targeted reductions less effective.
|
|
83
|
+
*/
|
|
84
|
+
export declare const BUILTIN_CONTEXT_REDUCTION_OPTIONS: ReduceContextOptions;
|
|
85
|
+
export declare const BUILTIN_CONTEXT_REDUCTION_GATE_RATIO = 0.5;
|
|
86
|
+
export interface ShouldApplyContextReductionInput {
|
|
87
|
+
usageTokens: number | null;
|
|
88
|
+
contextWindow: number;
|
|
89
|
+
gateRatio?: number;
|
|
90
|
+
isProviderNativeCompactionPath?: boolean;
|
|
91
|
+
}
|
|
92
|
+
export declare function shouldApplyContextReduction(input: ShouldApplyContextReductionInput): boolean;
|
|
93
|
+
export declare function collapseConsecutiveToolResults(messages: AgentMessage[], options?: CollapseConsecutiveOptions): CollapseConsecutiveResult;
|
|
94
|
+
export declare function microCompactAssistantText(messages: AgentMessage[], options?: MicroCompactAssistantOptions): MicroCompactAssistantResult;
|
|
95
|
+
export declare function clearOldToolResults(messages: AgentMessage[], options?: ClearOldToolResultsOptions): ClearOldToolResultsResult;
|
|
96
|
+
export declare function reduceContextMessages(messages: AgentMessage[], options?: ReduceContextOptions): ReduceContextResult;
|
|
97
|
+
//# sourceMappingURL=context-reduction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-reduction.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/compaction/context-reduction.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Deterministic, no-LLM context reductions applied before compaction summarization.\n *\n * Ported from plugsuits' `context-collapse` and `micro-compact` patterns and\n * adapted to the senpi `AgentMessage` shape. Three independent transforms:\n *\n * 1. {@link collapseConsecutiveToolResults} — runs of same-kind read/grep/shell\n * tool result payloads are replaced with a single one-line label so the\n * summarizer pays for the shape, not for the bytes.\n * 2. {@link microCompactAssistantText} — older long assistant text answers are\n * truncated and tagged with a `[response shrunk]` marker.\n * 3. {@link clearOldToolResults} — keep the last N tool results in full, replace\n * older clearable tool result content with `[tool result cleared]`.\n *\n * Each transform is pure (`messages` in → new array out, no in-place mutation\n * beyond freshly cloned messages) and returns aggregated token-savings stats.\n *\n * {@link reduceContextMessages} composes the three transforms in order.\n */\n\nimport type { AgentMessage } from \"@earendil-works/pi-agent-core\";\nimport type { AssistantMessage, ImageContent, TextContent, ToolResultMessage } from \"@earendil-works/pi-ai\";\n\nconst DEFAULT_READ_TOOL_NAMES = [\"read\", \"Read\", \"read_file\"];\nconst DEFAULT_SEARCH_TOOL_NAMES = [\"grep\", \"Grep\", \"glob\", \"Glob\"];\nconst DEFAULT_SHELL_TOOL_NAMES = [\"bash\", \"Bash\", \"shell\", \"shell_execute\"];\nconst DEFAULT_CLEARABLE_TOOL_NAMES = [\n\t\"read\",\n\t\"Read\",\n\t\"read_file\",\n\t\"write\",\n\t\"Write\",\n\t\"edit\",\n\t\"Edit\",\n\t\"grep\",\n\t\"Grep\",\n\t\"glob\",\n\t\"Glob\",\n\t\"bash\",\n\t\"Bash\",\n\t\"shell\",\n];\n\nconst DEFAULT_MIN_GROUP_SIZE = 2;\nconst DEFAULT_PROTECT_RECENT_MESSAGES = 5;\nconst DEFAULT_PROTECT_RECENT_TOKENS = 2000;\nconst DEFAULT_MAX_ASSISTANT_TEXT_TOKENS = 500;\nconst DEFAULT_MIN_SAVINGS_TOKENS = 100;\nconst DEFAULT_KEEP_RECENT_TOOL_RESULTS = 3;\nconst DEFAULT_CLEARED_PLACEHOLDER = \"[tool result cleared]\";\nconst DEFAULT_REPLACEMENT_TEMPLATE = \"[response shrunk — {original_tokens} → {shrunk_tokens} tokens]\";\n\nconst MAX_HINTS_IN_LABEL = 5;\nconst MAX_HINT_LENGTH = 80;\nconst SHRUNK_RESPONSE_RATIO = 0.3;\n\nexport type CollapsedGroupKind = \"read\" | \"search\" | \"shell\";\n\nexport interface CollapsedGroup {\n\ttype: CollapsedGroupKind;\n\tcount: number;\n\tlabel: string;\n\toriginalTokens: number;\n\tcollapsedTokens: number;\n}\n\nexport interface CollapseConsecutiveOptions {\n\tminGroupSize?: number;\n\tprotectRecentMessages?: number;\n\treadToolNames?: string[];\n\tsearchToolNames?: string[];\n\tshellToolNames?: string[];\n}\n\nexport interface CollapseConsecutiveResult {\n\tmessages: AgentMessage[];\n\tgroups: CollapsedGroup[];\n\ttokensSaved: number;\n}\n\nexport interface MicroCompactAssistantOptions {\n\tprotectRecentTokens?: number;\n\tmaxAssistantTextTokens?: number;\n\tminSavingsTokens?: number;\n\treplacementTemplate?: string;\n}\n\nexport interface MicroCompactAssistantResult {\n\tmessages: AgentMessage[];\n\ttokensSaved: number;\n\tmessagesModified: number;\n}\n\nexport interface ClearOldToolResultsOptions {\n\tkeepRecent?: number;\n\tclearableToolNames?: string[];\n\treplacementText?: string;\n}\n\nexport interface ClearOldToolResultsResult {\n\tmessages: AgentMessage[];\n\ttokensSaved: number;\n\ttoolResultsCleared: number;\n}\n\nexport interface ReduceContextOptions {\n\tcollapse?: false | CollapseConsecutiveOptions;\n\tshrinkAssistant?: false | MicroCompactAssistantOptions;\n\tclearToolResults?: false | ClearOldToolResultsOptions;\n}\n\nexport interface ReduceContextResult {\n\tmessages: AgentMessage[];\n\ttokensSaved: number;\n\tgroupsCollapsed: number;\n\tmessagesShrunk: number;\n\ttoolResultsCleared: number;\n}\n\n/**\n * Default options passed to {@link reduceContextMessages} when the builtin\n * compaction extension's `context` hook decides to run a reduction pass.\n *\n * Each value is chosen to be strictly more conservative than the corresponding\n * plugsuits default — protect more of the recent tail, raise the per-message\n * shrink threshold, and keep more recent tool results intact — so a single\n * shared default is safe to apply across normal coding sessions without making\n * targeted reductions less effective.\n */\nexport const BUILTIN_CONTEXT_REDUCTION_OPTIONS: ReduceContextOptions = {\n\tcollapse: {\n\t\tminGroupSize: DEFAULT_MIN_GROUP_SIZE,\n\t\tprotectRecentMessages: DEFAULT_PROTECT_RECENT_MESSAGES,\n\t},\n\tshrinkAssistant: {\n\t\tprotectRecentTokens: 3000,\n\t\tmaxAssistantTextTokens: 800,\n\t\tminSavingsTokens: DEFAULT_MIN_SAVINGS_TOKENS,\n\t},\n\tclearToolResults: {\n\t\tkeepRecent: 6,\n\t},\n};\n\nexport const BUILTIN_CONTEXT_REDUCTION_GATE_RATIO = 0.5;\n\nexport interface ShouldApplyContextReductionInput {\n\tusageTokens: number | null;\n\tcontextWindow: number;\n\tgateRatio?: number;\n\tisProviderNativeCompactionPath?: boolean;\n}\n\nexport function shouldApplyContextReduction(input: ShouldApplyContextReductionInput): boolean {\n\tconst gate = input.gateRatio ?? BUILTIN_CONTEXT_REDUCTION_GATE_RATIO;\n\tif (input.isProviderNativeCompactionPath === true) return false;\n\tif (input.usageTokens === null) return false;\n\tif (input.contextWindow <= 0) return false;\n\treturn input.usageTokens >= input.contextWindow * gate;\n}\n\nfunction approxTextTokens(text: string): number {\n\tif (!text) return 0;\n\treturn Math.ceil(text.length / 4);\n}\n\nfunction extractContentText(content: (TextContent | ImageContent)[] | undefined): string {\n\tif (!Array.isArray(content)) return \"\";\n\tlet out = \"\";\n\tfor (const part of content) {\n\t\tif (part.type === \"text\") out += part.text;\n\t}\n\treturn out;\n}\n\nfunction extractMessageText(message: AgentMessage): string {\n\tif (message.role === \"user\") {\n\t\tif (typeof message.content === \"string\") return message.content;\n\t\treturn extractContentText(message.content as (TextContent | ImageContent)[]);\n\t}\n\tif (message.role === \"assistant\") {\n\t\tlet out = \"\";\n\t\tfor (const block of message.content) {\n\t\t\tif (block.type === \"text\") out += block.text;\n\t\t\telse if (block.type === \"toolCall\") out += `${block.name} ${JSON.stringify(block.arguments)}`;\n\t\t}\n\t\treturn out;\n\t}\n\tif (message.role === \"toolResult\") {\n\t\treturn extractContentText(message.content);\n\t}\n\treturn \"\";\n}\n\ninterface ToolNameSets {\n\tread: Set<string>;\n\tsearch: Set<string>;\n\tshell: Set<string>;\n}\n\nfunction classifyTool(name: string, sets: ToolNameSets): CollapsedGroupKind | null {\n\tif (sets.read.has(name)) return \"read\";\n\tif (sets.search.has(name)) return \"search\";\n\tif (sets.shell.has(name)) return \"shell\";\n\treturn null;\n}\n\ninterface FirstToolCall {\n\tid: string;\n\tname: string;\n\targs: Record<string, unknown>;\n}\n\nfunction getFirstToolCallFromAssistant(message: AgentMessage): FirstToolCall | null {\n\tif (message.role !== \"assistant\") return null;\n\tfor (const block of message.content) {\n\t\tif (block.type === \"toolCall\") {\n\t\t\treturn { id: block.id, name: block.name, args: block.arguments as Record<string, unknown> };\n\t\t}\n\t}\n\treturn null;\n}\n\ninterface CollapsibleOperation {\n\ttype: CollapsedGroupKind;\n\ttoolName: string;\n\tassistantIndex: number;\n\tresultIndex: number;\n\thint?: string;\n\tresultText: string;\n}\n\nfunction truncateHint(value: string): string {\n\tif (value.length <= MAX_HINT_LENGTH) return value;\n\treturn `${value.slice(0, MAX_HINT_LENGTH - 1)}…`;\n}\n\nfunction extractHint(type: CollapsedGroupKind, args: Record<string, unknown>): string | undefined {\n\tif (type === \"read\") {\n\t\tconst path = args.path ?? args.file_path ?? args.filePath;\n\t\tif (typeof path === \"string\" && path.length > 0) return truncateHint(path);\n\t\treturn undefined;\n\t}\n\tif (type === \"search\") {\n\t\tconst path = typeof args.path === \"string\" ? args.path : undefined;\n\t\tconst pattern =\n\t\t\t(typeof args.pattern === \"string\" && args.pattern) ||\n\t\t\t(typeof args.glob === \"string\" && args.glob) ||\n\t\t\t(typeof args.query === \"string\" && args.query) ||\n\t\t\tundefined;\n\t\tif (path && pattern) return truncateHint(`${path}:${pattern}`);\n\t\tif (path) return truncateHint(path);\n\t\tif (pattern) return truncateHint(pattern as string);\n\t\treturn undefined;\n\t}\n\tconst command = args.command ?? args.cmd;\n\tif (typeof command === \"string\" && command.length > 0) return truncateHint(command);\n\treturn undefined;\n}\n\nfunction buildGroupLabel(type: CollapsedGroupKind, operations: CollapsibleOperation[]): string {\n\tconst hints: string[] = [];\n\tfor (const op of operations) {\n\t\tif (op.hint && hints.length < MAX_HINTS_IN_LABEL) hints.push(op.hint);\n\t}\n\tconst noun = type === \"read\" ? \"read results\" : type === \"search\" ? \"search results\" : \"shell results\";\n\tif (hints.length === 0) return `[${operations.length} ${noun}]`;\n\tconst more = operations.length - hints.length;\n\tconst moreSuffix = more > 0 ? `, and ${more} more` : \"\";\n\treturn `[${operations.length} ${noun}: ${hints.join(\", \")}${moreSuffix}]`;\n}\n\nfunction collectCollapsibleOperations(\n\tmessages: AgentMessage[],\n\tcollapseLimit: number,\n\tsets: ToolNameSets,\n): CollapsibleOperation[] {\n\tconst operations: CollapsibleOperation[] = [];\n\tlet i = 0;\n\twhile (i < collapseLimit) {\n\t\tconst assistant = messages[i];\n\t\tconst call = getFirstToolCallFromAssistant(assistant);\n\t\tif (!call) {\n\t\t\ti += 1;\n\t\t\tcontinue;\n\t\t}\n\t\tconst next = messages[i + 1];\n\t\tconst resultInWindow = i + 1 < collapseLimit;\n\t\tif (!next || next.role !== \"toolResult\" || next.toolCallId !== call.id || !resultInWindow) {\n\t\t\ti += 1;\n\t\t\tcontinue;\n\t\t}\n\t\tconst type = classifyTool(call.name, sets);\n\t\tif (!type) {\n\t\t\ti += 1;\n\t\t\tcontinue;\n\t\t}\n\t\toperations.push({\n\t\t\ttype,\n\t\t\ttoolName: call.name,\n\t\t\tassistantIndex: i,\n\t\t\tresultIndex: i + 1,\n\t\t\thint: extractHint(type, call.args),\n\t\t\tresultText: extractContentText((next as ToolResultMessage).content),\n\t\t});\n\t\ti += 2;\n\t}\n\treturn operations;\n}\n\nfunction groupConsecutiveOperations(operations: CollapsibleOperation[]): CollapsibleOperation[][] {\n\tconst groups: CollapsibleOperation[][] = [];\n\tlet current: CollapsibleOperation[] = [];\n\tfor (const op of operations) {\n\t\tif (current.length === 0) {\n\t\t\tcurrent.push(op);\n\t\t\tcontinue;\n\t\t}\n\t\tconst prev = current[current.length - 1];\n\t\tif (op.type === prev.type && op.assistantIndex === prev.resultIndex + 1) {\n\t\t\tcurrent.push(op);\n\t\t\tcontinue;\n\t\t}\n\t\tgroups.push(current);\n\t\tcurrent = [op];\n\t}\n\tif (current.length > 0) groups.push(current);\n\treturn groups;\n}\n\nexport function collapseConsecutiveToolResults(\n\tmessages: AgentMessage[],\n\toptions: CollapseConsecutiveOptions = {},\n): CollapseConsecutiveResult {\n\tconst minGroupSize = Math.max(1, options.minGroupSize ?? DEFAULT_MIN_GROUP_SIZE);\n\tconst protectRecentMessages = Math.max(0, options.protectRecentMessages ?? DEFAULT_PROTECT_RECENT_MESSAGES);\n\tconst sets: ToolNameSets = {\n\t\tread: new Set(options.readToolNames ?? DEFAULT_READ_TOOL_NAMES),\n\t\tsearch: new Set(options.searchToolNames ?? DEFAULT_SEARCH_TOOL_NAMES),\n\t\tshell: new Set(options.shellToolNames ?? DEFAULT_SHELL_TOOL_NAMES),\n\t};\n\n\tconst collapseLimit = Math.max(0, messages.length - protectRecentMessages);\n\tconst operations = collectCollapsibleOperations(messages, collapseLimit, sets);\n\tconst operationGroups = groupConsecutiveOperations(operations);\n\n\tconst collapsedGroups: CollapsedGroup[] = [];\n\tconst nextMessages = messages.slice();\n\tlet tokensSaved = 0;\n\n\tfor (const group of operationGroups) {\n\t\tif (group.length < minGroupSize) continue;\n\t\tconst label = buildGroupLabel(group[0].type, group);\n\t\tlet originalTokens = 0;\n\t\tlet collapsedTokens = 0;\n\t\tfor (const op of group) {\n\t\t\tconst original = approxTextTokens(op.resultText);\n\t\t\tconst collapsed = approxTextTokens(label);\n\t\t\toriginalTokens += original;\n\t\t\tcollapsedTokens += collapsed;\n\t\t\tconst result = nextMessages[op.resultIndex];\n\t\t\tif (result.role !== \"toolResult\") continue;\n\t\t\tconst images = result.content.filter((c): c is ImageContent => c.type === \"image\");\n\t\t\tnextMessages[op.resultIndex] = {\n\t\t\t\t...result,\n\t\t\t\tcontent: [{ type: \"text\", text: label } as TextContent, ...images],\n\t\t\t};\n\t\t}\n\t\ttokensSaved += Math.max(0, originalTokens - collapsedTokens);\n\t\tcollapsedGroups.push({\n\t\t\ttype: group[0].type,\n\t\t\tcount: group.length,\n\t\t\tlabel,\n\t\t\toriginalTokens,\n\t\t\tcollapsedTokens,\n\t\t});\n\t}\n\n\treturn { messages: nextMessages, groups: collapsedGroups, tokensSaved };\n}\n\nfunction resolveProtectedFromIndex(messages: AgentMessage[], protectRecentTokens: number): number {\n\tif (messages.length === 0) return 0;\n\tlet recentTokens = 0;\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst tokens = approxTextTokens(extractMessageText(messages[i]));\n\t\tif (recentTokens + tokens > protectRecentTokens) return i + 1;\n\t\trecentTokens += tokens;\n\t\tif (i === 0) return 0;\n\t}\n\treturn messages.length;\n}\n\nfunction renderReplacementText(template: string, originalTokens: number, shrunkTokens: number): string {\n\treturn template\n\t\t.split(\"{original_tokens}\")\n\t\t.join(String(originalTokens))\n\t\t.split(\"{shrunk_tokens}\")\n\t\t.join(String(shrunkTokens));\n}\n\nfunction buildShrunkText(\n\toriginalText: string,\n\toriginalTokens: number,\n\tmaxAssistantTextTokens: number,\n\ttemplate: string,\n): { text: string; tokens: number } {\n\tconst targetTextTokens = Math.max(1, Math.floor(maxAssistantTextTokens * SHRUNK_RESPONSE_RATIO));\n\tconst ratio = originalTokens > 0 ? targetTextTokens / originalTokens : 0;\n\tconst targetChars = Math.max(0, Math.floor(originalText.length * ratio));\n\tconst truncatedText = originalText.slice(0, targetChars);\n\tlet shrunkTokens = 0;\n\tlet shrunkText = \"\";\n\tfor (let iteration = 0; iteration < 5; iteration += 1) {\n\t\tconst replacement = renderReplacementText(template, originalTokens, shrunkTokens);\n\t\tconst candidate = truncatedText.length > 0 ? `${truncatedText}\\n\\n${replacement}` : replacement;\n\t\tconst tokens = approxTextTokens(candidate);\n\t\tshrunkText = candidate;\n\t\tif (tokens === shrunkTokens) return { text: candidate, tokens };\n\t\tshrunkTokens = tokens;\n\t}\n\treturn { text: shrunkText, tokens: approxTextTokens(shrunkText) };\n}\n\nexport function microCompactAssistantText(\n\tmessages: AgentMessage[],\n\toptions: MicroCompactAssistantOptions = {},\n): MicroCompactAssistantResult {\n\tconst protectRecentTokens = Math.max(0, options.protectRecentTokens ?? DEFAULT_PROTECT_RECENT_TOKENS);\n\tconst maxAssistantTextTokens = Math.max(0, options.maxAssistantTextTokens ?? DEFAULT_MAX_ASSISTANT_TEXT_TOKENS);\n\tconst minSavingsTokens = Math.max(0, options.minSavingsTokens ?? DEFAULT_MIN_SAVINGS_TOKENS);\n\tconst template = options.replacementTemplate ?? DEFAULT_REPLACEMENT_TEMPLATE;\n\n\tconst protectedFromIndex = resolveProtectedFromIndex(messages, protectRecentTokens);\n\tconst result = messages.slice();\n\tlet messagesModified = 0;\n\tlet tokensSaved = 0;\n\n\tfor (let i = 0; i < protectedFromIndex; i += 1) {\n\t\tconst msg = result[i];\n\t\tif (msg.role !== \"assistant\") continue;\n\t\tconst assistant = msg as AssistantMessage;\n\t\tconst allText = assistant.content.length > 0 && assistant.content.every((c) => c.type === \"text\");\n\t\tif (!allText) continue;\n\t\tconst originalText = assistant.content.map((c) => (c.type === \"text\" ? c.text : \"\")).join(\"\\n\");\n\t\tconst originalTokens = approxTextTokens(originalText);\n\t\tif (originalTokens <= maxAssistantTextTokens) continue;\n\t\tconst shrunk = buildShrunkText(originalText, originalTokens, maxAssistantTextTokens, template);\n\t\tconst saved = originalTokens - shrunk.tokens;\n\t\tif (saved < minSavingsTokens) continue;\n\t\tresult[i] = {\n\t\t\t...assistant,\n\t\t\tcontent: [{ type: \"text\", text: shrunk.text } as TextContent],\n\t\t};\n\t\ttokensSaved += saved;\n\t\tmessagesModified += 1;\n\t}\n\n\treturn { messages: result, tokensSaved, messagesModified };\n}\n\nexport function clearOldToolResults(\n\tmessages: AgentMessage[],\n\toptions: ClearOldToolResultsOptions = {},\n): ClearOldToolResultsResult {\n\tconst keepRecent = Math.max(0, options.keepRecent ?? DEFAULT_KEEP_RECENT_TOOL_RESULTS);\n\tconst clearable = new Set(options.clearableToolNames ?? DEFAULT_CLEARABLE_TOOL_NAMES);\n\tconst replacementText = options.replacementText ?? DEFAULT_CLEARED_PLACEHOLDER;\n\n\tconst clearableIndices: number[] = [];\n\tfor (let i = 0; i < messages.length; i += 1) {\n\t\tconst msg = messages[i];\n\t\tif (msg.role === \"toolResult\" && clearable.has(msg.toolName)) {\n\t\t\tclearableIndices.push(i);\n\t\t}\n\t}\n\n\tconst clearUntil = Math.max(0, clearableIndices.length - keepRecent);\n\tif (clearUntil === 0) {\n\t\treturn { messages: messages.slice(), tokensSaved: 0, toolResultsCleared: 0 };\n\t}\n\n\tconst result = messages.slice();\n\tlet tokensSaved = 0;\n\tlet toolResultsCleared = 0;\n\tconst replacementTokens = approxTextTokens(replacementText);\n\n\tfor (let k = 0; k < clearUntil; k += 1) {\n\t\tconst idx = clearableIndices[k];\n\t\tconst msg = result[idx];\n\t\tif (msg.role !== \"toolResult\") continue;\n\t\tconst original = msg as ToolResultMessage;\n\t\tconst originalText = extractContentText(original.content);\n\t\tconst originalTokens = approxTextTokens(originalText);\n\t\tconst images = original.content.filter((c): c is ImageContent => c.type === \"image\");\n\t\tresult[idx] = {\n\t\t\t...original,\n\t\t\tcontent: [{ type: \"text\", text: replacementText } as TextContent, ...images],\n\t\t};\n\t\tconst savings = originalTokens - replacementTokens;\n\t\tif (savings > 0) tokensSaved += savings;\n\t\ttoolResultsCleared += 1;\n\t}\n\n\treturn { messages: result, tokensSaved, toolResultsCleared };\n}\n\nexport function reduceContextMessages(\n\tmessages: AgentMessage[],\n\toptions: ReduceContextOptions = {},\n): ReduceContextResult {\n\tlet current = messages;\n\tlet tokensSaved = 0;\n\tlet groupsCollapsed = 0;\n\tlet messagesShrunk = 0;\n\tlet toolResultsCleared = 0;\n\n\tif (options.collapse !== false) {\n\t\tconst collapsed = collapseConsecutiveToolResults(current, options.collapse ?? undefined);\n\t\tcurrent = collapsed.messages;\n\t\ttokensSaved += collapsed.tokensSaved;\n\t\tgroupsCollapsed += collapsed.groups.length;\n\t}\n\tif (options.shrinkAssistant !== false) {\n\t\tconst shrunk = microCompactAssistantText(current, options.shrinkAssistant ?? undefined);\n\t\tcurrent = shrunk.messages;\n\t\ttokensSaved += shrunk.tokensSaved;\n\t\tmessagesShrunk += shrunk.messagesModified;\n\t}\n\tif (options.clearToolResults !== false) {\n\t\tconst cleared = clearOldToolResults(current, options.clearToolResults ?? undefined);\n\t\tcurrent = cleared.messages;\n\t\ttokensSaved += cleared.tokensSaved;\n\t\ttoolResultsCleared += cleared.toolResultsCleared;\n\t}\n\n\treturn { messages: current, tokensSaved, groupsCollapsed, messagesShrunk, toolResultsCleared };\n}\n"]}
|