@xandout/libra-harness 0.1.34 → 0.1.91
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/README.md +30 -30
- package/dist/agent.d.ts +8 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +205 -193
- package/dist/agent.js.map +1 -1
- package/dist/ai-sdk-model.d.ts.map +1 -1
- package/dist/ai-sdk-model.js +91 -78
- package/dist/ai-sdk-model.js.map +1 -1
- package/dist/context.d.ts +2 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/{extras/extensions → extensions}/auto-steer/extension.d.ts +1 -1
- package/dist/extensions/auto-steer/extension.d.ts.map +1 -0
- package/dist/extensions/auto-steer/extension.js.map +1 -0
- package/dist/extensions/auto-steer/index.d.ts.map +1 -0
- package/dist/extensions/auto-steer/index.js.map +1 -0
- package/dist/extensions/code-tools/extension.d.ts +15 -0
- package/dist/extensions/code-tools/extension.d.ts.map +1 -0
- package/dist/extensions/code-tools/extension.js +73 -0
- package/dist/extensions/code-tools/extension.js.map +1 -0
- package/dist/extensions/code-tools/extension.test.d.ts.map +1 -0
- package/dist/extensions/code-tools/extension.test.js +504 -0
- package/dist/extensions/code-tools/extension.test.js.map +1 -0
- package/dist/extensions/code-tools/index.d.ts.map +1 -0
- package/dist/extensions/code-tools/index.js.map +1 -0
- package/dist/{extras/extensions → extensions}/code-tools/tools/code-search.d.ts +2 -2
- package/dist/extensions/code-tools/tools/code-search.d.ts.map +1 -0
- package/dist/{extras/extensions → extensions}/code-tools/tools/code-search.js +1 -1
- package/dist/extensions/code-tools/tools/code-search.js.map +1 -0
- package/dist/extensions/code-tools/tools/find.d.ts.map +1 -0
- package/dist/extensions/code-tools/tools/find.js +83 -0
- package/dist/extensions/code-tools/tools/find.js.map +1 -0
- package/dist/extensions/code-tools/tools/grep.d.ts.map +1 -0
- package/dist/extensions/code-tools/tools/grep.js +90 -0
- package/dist/extensions/code-tools/tools/grep.js.map +1 -0
- package/dist/extensions/code-tools/tools/read.d.ts.map +1 -0
- package/dist/{extras/extensions → extensions}/code-tools/tools/read.js +17 -25
- package/dist/extensions/code-tools/tools/read.js.map +1 -0
- package/dist/{extras/extensions → extensions}/code-tools/tools/shared.d.ts +5 -1
- package/dist/extensions/code-tools/tools/shared.d.ts.map +1 -0
- package/dist/{extras/extensions → extensions}/code-tools/tools/shared.js +68 -0
- package/dist/extensions/code-tools/tools/shared.js.map +1 -0
- package/dist/extensions/code-tools/tools/shell.d.ts +39 -0
- package/dist/extensions/code-tools/tools/shell.d.ts.map +1 -0
- package/dist/extensions/code-tools/tools/shell.js +441 -0
- package/dist/extensions/code-tools/tools/shell.js.map +1 -0
- package/dist/{extras/extensions → extensions}/code-tools/tools/todo.d.ts +0 -3
- package/dist/extensions/code-tools/tools/todo.d.ts.map +1 -0
- package/dist/{extras/extensions → extensions}/code-tools/tools/todo.js +0 -8
- package/dist/extensions/code-tools/tools/todo.js.map +1 -0
- package/dist/extensions/code-tools/tools/view-image.d.ts +10 -0
- package/dist/extensions/code-tools/tools/view-image.d.ts.map +1 -0
- package/dist/extensions/code-tools/tools/view-image.js +87 -0
- package/dist/extensions/code-tools/tools/view-image.js.map +1 -0
- package/dist/extensions/code-tools/tools/write.d.ts.map +1 -0
- package/dist/{extras/extensions → extensions}/code-tools/tools/write.js +47 -37
- package/dist/extensions/code-tools/tools/write.js.map +1 -0
- package/dist/{extras/extensions → extensions}/disk-session/extension.d.ts +45 -2
- package/dist/extensions/disk-session/extension.d.ts.map +1 -0
- package/dist/extensions/disk-session/extension.js +818 -0
- package/dist/extensions/disk-session/extension.js.map +1 -0
- package/dist/extensions/disk-session/extension.test.d.ts.map +1 -0
- package/dist/{extras/extensions → extensions}/disk-session/extension.test.js +445 -16
- package/dist/extensions/disk-session/extension.test.js.map +1 -0
- package/dist/extensions/disk-session/index.d.ts +3 -0
- package/dist/extensions/disk-session/index.d.ts.map +1 -0
- package/dist/extensions/disk-session/index.js +2 -0
- package/dist/extensions/disk-session/index.js.map +1 -0
- package/dist/{extras/extensions → extensions}/emoji/extension.d.ts +1 -1
- package/dist/extensions/emoji/extension.d.ts.map +1 -0
- package/dist/extensions/emoji/extension.js.map +1 -0
- package/dist/extensions/emoji/index.d.ts.map +1 -0
- package/dist/extensions/emoji/index.js.map +1 -0
- package/dist/extensions/filesystem/extension.d.ts.map +1 -0
- package/dist/extensions/filesystem/extension.js.map +1 -0
- package/dist/extensions/filesystem/extension.test.d.ts.map +1 -0
- package/dist/extensions/filesystem/extension.test.js.map +1 -0
- package/dist/extensions/filesystem/index.d.ts.map +1 -0
- package/dist/extensions/filesystem/index.js.map +1 -0
- package/dist/extensions/index.d.ts +3 -0
- package/dist/extensions/index.d.ts.map +1 -0
- package/dist/extensions/index.js +2 -0
- package/dist/extensions/index.js.map +1 -0
- package/dist/{extras/extensions → extensions}/keyword-extractor/extension.d.ts +1 -1
- package/dist/extensions/keyword-extractor/extension.d.ts.map +1 -0
- package/dist/{extras/extensions → extensions}/keyword-extractor/extension.js +1 -1
- package/dist/extensions/keyword-extractor/extension.js.map +1 -0
- package/dist/extensions/keyword-extractor/extension.test.d.ts.map +1 -0
- package/dist/extensions/keyword-extractor/extension.test.js.map +1 -0
- package/dist/extensions/keyword-extractor/index.d.ts.map +1 -0
- package/dist/extensions/keyword-extractor/index.js.map +1 -0
- package/dist/extensions/keyword-extractor/nlp.d.ts.map +1 -0
- package/dist/extensions/keyword-extractor/nlp.js.map +1 -0
- package/dist/extensions/keyword-extractor/nlp.test.d.ts.map +1 -0
- package/dist/extensions/keyword-extractor/nlp.test.js.map +1 -0
- package/dist/{extras/extension-loader.d.ts → extensions/loader.d.ts} +1 -1
- package/dist/extensions/loader.d.ts.map +1 -0
- package/dist/{extras/extension-loader.js → extensions/loader.js} +2 -2
- package/dist/extensions/loader.js.map +1 -0
- package/dist/{extras/extensions → extensions}/logger/extension.d.ts +1 -1
- package/dist/extensions/logger/extension.d.ts.map +1 -0
- package/dist/{extras/extensions → extensions}/logger/extension.js +1 -1
- package/dist/extensions/logger/extension.js.map +1 -0
- package/dist/extensions/logger/index.d.ts.map +1 -0
- package/dist/extensions/logger/index.js.map +1 -0
- package/dist/{extras/extensions → extensions}/mcp/extension.d.ts +1 -1
- package/dist/extensions/mcp/extension.d.ts.map +1 -0
- package/dist/extensions/mcp/extension.js.map +1 -0
- package/dist/extensions/mcp/extension.test.d.ts.map +1 -0
- package/dist/{extras/extensions → extensions}/mcp/extension.test.js +1 -1
- package/dist/extensions/mcp/extension.test.js.map +1 -0
- package/dist/extensions/mcp/index.d.ts.map +1 -0
- package/dist/extensions/mcp/index.js.map +1 -0
- package/dist/{extras/extensions → extensions}/mem-session/extension.d.ts +2 -2
- package/dist/extensions/mem-session/extension.d.ts.map +1 -0
- package/dist/extensions/mem-session/extension.js.map +1 -0
- package/dist/extensions/mem-session/index.d.ts.map +1 -0
- package/dist/extensions/mem-session/index.js.map +1 -0
- package/dist/{extras/extensions → extensions}/memory/extension.d.ts +1 -1
- package/dist/extensions/memory/extension.d.ts.map +1 -0
- package/dist/{extras/extensions → extensions}/memory/extension.js +1 -1
- package/dist/extensions/memory/extension.js.map +1 -0
- package/dist/extensions/memory/index.d.ts +4 -0
- package/dist/extensions/memory/index.d.ts.map +1 -0
- package/dist/extensions/memory/index.js.map +1 -0
- package/dist/{extras/extensions → extensions}/memory/llm-extractor.d.ts +1 -1
- package/dist/extensions/memory/llm-extractor.d.ts.map +1 -0
- package/dist/{extras/extensions → extensions}/memory/llm-extractor.js +1 -1
- package/dist/extensions/memory/llm-extractor.js.map +1 -0
- package/dist/{extras/extensions → extensions}/memory/types.d.ts +2 -2
- package/dist/extensions/memory/types.d.ts.map +1 -0
- package/dist/extensions/memory/types.js.map +1 -0
- package/dist/{extras/extensions → extensions}/otel/extension.d.ts +1 -1
- package/dist/extensions/otel/extension.d.ts.map +1 -0
- package/dist/{extras/extensions → extensions}/otel/extension.js +1 -1
- package/dist/extensions/otel/extension.js.map +1 -0
- package/dist/extensions/otel/extension.test.d.ts.map +1 -0
- package/dist/{extras/extensions → extensions}/otel/extension.test.js +1 -1
- package/dist/extensions/otel/extension.test.js.map +1 -0
- package/dist/extensions/otel/index.d.ts.map +1 -0
- package/dist/extensions/otel/index.js.map +1 -0
- package/dist/extensions/otel/jsonl-span-exporter.d.ts.map +1 -0
- package/dist/extensions/otel/jsonl-span-exporter.js.map +1 -0
- package/dist/{extras/extensions → extensions}/scripts/extension.d.ts +1 -1
- package/dist/extensions/scripts/extension.d.ts.map +1 -0
- package/dist/extensions/scripts/extension.js.map +1 -0
- package/dist/extensions/scripts/extension.test.d.ts.map +1 -0
- package/dist/extensions/scripts/extension.test.js.map +1 -0
- package/dist/extensions/scripts/index.d.ts.map +1 -0
- package/dist/extensions/scripts/index.js.map +1 -0
- package/dist/{extras/extensions → extensions}/skills/extension.d.ts +1 -1
- package/dist/extensions/skills/extension.d.ts.map +1 -0
- package/dist/extensions/skills/extension.js.map +1 -0
- package/dist/extensions/skills/extension.test.d.ts.map +1 -0
- package/dist/{extras/extensions → extensions}/skills/extension.test.js +1 -1
- package/dist/extensions/skills/extension.test.js.map +1 -0
- package/dist/extensions/skills/index.d.ts.map +1 -0
- package/dist/extensions/skills/index.js.map +1 -0
- package/dist/{extras/extensions → extensions}/streaming/extension.d.ts +1 -1
- package/dist/extensions/streaming/extension.d.ts.map +1 -0
- package/dist/extensions/streaming/extension.js.map +1 -0
- package/dist/extensions/streaming/index.d.ts.map +1 -0
- package/dist/extensions/streaming/index.js.map +1 -0
- package/dist/{extras/extensions → extensions}/structured-output/extension.d.ts +1 -1
- package/dist/extensions/structured-output/extension.d.ts.map +1 -0
- package/dist/extensions/structured-output/extension.js.map +1 -0
- package/dist/extensions/structured-output/index.d.ts.map +1 -0
- package/dist/extensions/structured-output/index.js.map +1 -0
- package/dist/{extras/extensions → extensions}/timestamp/extension.d.ts +1 -1
- package/dist/extensions/timestamp/extension.d.ts.map +1 -0
- package/dist/extensions/timestamp/extension.js.map +1 -0
- package/dist/extensions/timestamp/index.d.ts.map +1 -0
- package/dist/extensions/timestamp/index.js.map +1 -0
- package/dist/{extras/extensions → extensions}/token-stats/extension.d.ts +1 -1
- package/dist/extensions/token-stats/extension.d.ts.map +1 -0
- package/dist/extensions/token-stats/extension.js.map +1 -0
- package/dist/extensions/token-stats/extension.test.d.ts.map +1 -0
- package/dist/{extras/extensions → extensions}/token-stats/extension.test.js +1 -1
- package/dist/extensions/token-stats/extension.test.js.map +1 -0
- package/dist/extensions/token-stats/index.d.ts.map +1 -0
- package/dist/extensions/token-stats/index.js.map +1 -0
- package/dist/{extras/extensions → extensions}/tool-buffer/extension.d.ts +1 -1
- package/dist/extensions/tool-buffer/extension.d.ts.map +1 -0
- package/dist/extensions/tool-buffer/extension.js.map +1 -0
- package/dist/extensions/tool-buffer/extension.test.d.ts.map +1 -0
- package/dist/{extras/extensions → extensions}/tool-buffer/extension.test.js +1 -1
- package/dist/extensions/tool-buffer/extension.test.js.map +1 -0
- package/dist/extensions/tool-buffer/index.d.ts.map +1 -0
- package/dist/extensions/tool-buffer/index.js.map +1 -0
- package/dist/{extras/extensions → extensions}/weather-tool/extension.d.ts +1 -1
- package/dist/extensions/weather-tool/extension.d.ts.map +1 -0
- package/dist/extensions/weather-tool/extension.js.map +1 -0
- package/dist/extensions/weather-tool/index.d.ts.map +1 -0
- package/dist/extensions/weather-tool/index.js.map +1 -0
- package/dist/handle.d.ts +4 -0
- package/dist/handle.d.ts.map +1 -1
- package/dist/{extras/models → models}/ai-sdk-resolver.d.ts +1 -1
- package/dist/models/ai-sdk-resolver.d.ts.map +1 -0
- package/dist/{extras/models → models}/ai-sdk-resolver.js +1 -1
- package/dist/models/ai-sdk-resolver.js.map +1 -0
- package/dist/models/index.d.ts.map +1 -0
- package/dist/models/index.js.map +1 -0
- package/dist/{extras/models → models}/routing-model.d.ts +1 -1
- package/dist/models/routing-model.d.ts.map +1 -0
- package/dist/{extras/models → models}/routing-model.js +1 -1
- package/dist/models/routing-model.js.map +1 -0
- package/dist/openai-provider/index.d.ts.map +1 -0
- package/dist/openai-provider/index.js.map +1 -0
- package/dist/{extras/openai-provider → openai-provider}/server.d.ts +1 -1
- package/dist/openai-provider/server.d.ts.map +1 -0
- package/dist/openai-provider/server.js.map +1 -0
- package/package.json +108 -83
- package/dist/extras/extension-loader.d.ts.map +0 -1
- package/dist/extras/extension-loader.js.map +0 -1
- package/dist/extras/extensions/auto-steer/extension.d.ts.map +0 -1
- package/dist/extras/extensions/auto-steer/extension.js.map +0 -1
- package/dist/extras/extensions/auto-steer/index.d.ts.map +0 -1
- package/dist/extras/extensions/auto-steer/index.js.map +0 -1
- package/dist/extras/extensions/code-tools/extension.d.ts +0 -83
- package/dist/extras/extensions/code-tools/extension.d.ts.map +0 -1
- package/dist/extras/extensions/code-tools/extension.js +0 -93
- package/dist/extras/extensions/code-tools/extension.js.map +0 -1
- package/dist/extras/extensions/code-tools/extension.test.d.ts.map +0 -1
- package/dist/extras/extensions/code-tools/extension.test.js +0 -656
- package/dist/extras/extensions/code-tools/extension.test.js.map +0 -1
- package/dist/extras/extensions/code-tools/index.d.ts.map +0 -1
- package/dist/extras/extensions/code-tools/index.js.map +0 -1
- package/dist/extras/extensions/code-tools/tools/code-search.d.ts.map +0 -1
- package/dist/extras/extensions/code-tools/tools/code-search.js.map +0 -1
- package/dist/extras/extensions/code-tools/tools/find.d.ts.map +0 -1
- package/dist/extras/extensions/code-tools/tools/find.js +0 -161
- package/dist/extras/extensions/code-tools/tools/find.js.map +0 -1
- package/dist/extras/extensions/code-tools/tools/grep.d.ts.map +0 -1
- package/dist/extras/extensions/code-tools/tools/grep.js +0 -307
- package/dist/extras/extensions/code-tools/tools/grep.js.map +0 -1
- package/dist/extras/extensions/code-tools/tools/read.d.ts.map +0 -1
- package/dist/extras/extensions/code-tools/tools/read.js.map +0 -1
- package/dist/extras/extensions/code-tools/tools/shared.d.ts.map +0 -1
- package/dist/extras/extensions/code-tools/tools/shared.js.map +0 -1
- package/dist/extras/extensions/code-tools/tools/shell.d.ts +0 -49
- package/dist/extras/extensions/code-tools/tools/shell.d.ts.map +0 -1
- package/dist/extras/extensions/code-tools/tools/shell.js +0 -623
- package/dist/extras/extensions/code-tools/tools/shell.js.map +0 -1
- package/dist/extras/extensions/code-tools/tools/todo.d.ts.map +0 -1
- package/dist/extras/extensions/code-tools/tools/todo.js.map +0 -1
- package/dist/extras/extensions/code-tools/tools/write.d.ts.map +0 -1
- package/dist/extras/extensions/code-tools/tools/write.js.map +0 -1
- package/dist/extras/extensions/disk-session/extension.d.ts.map +0 -1
- package/dist/extras/extensions/disk-session/extension.js +0 -640
- package/dist/extras/extensions/disk-session/extension.js.map +0 -1
- package/dist/extras/extensions/disk-session/extension.test.d.ts.map +0 -1
- package/dist/extras/extensions/disk-session/extension.test.js.map +0 -1
- package/dist/extras/extensions/disk-session/index.d.ts +0 -3
- package/dist/extras/extensions/disk-session/index.d.ts.map +0 -1
- package/dist/extras/extensions/disk-session/index.js +0 -2
- package/dist/extras/extensions/disk-session/index.js.map +0 -1
- package/dist/extras/extensions/emoji/extension.d.ts.map +0 -1
- package/dist/extras/extensions/emoji/extension.js.map +0 -1
- package/dist/extras/extensions/emoji/index.d.ts.map +0 -1
- package/dist/extras/extensions/emoji/index.js.map +0 -1
- package/dist/extras/extensions/filesystem/extension.d.ts.map +0 -1
- package/dist/extras/extensions/filesystem/extension.js.map +0 -1
- package/dist/extras/extensions/filesystem/extension.test.d.ts.map +0 -1
- package/dist/extras/extensions/filesystem/extension.test.js.map +0 -1
- package/dist/extras/extensions/filesystem/index.d.ts.map +0 -1
- package/dist/extras/extensions/filesystem/index.js.map +0 -1
- package/dist/extras/extensions/keyword-extractor/extension.d.ts.map +0 -1
- package/dist/extras/extensions/keyword-extractor/extension.js.map +0 -1
- package/dist/extras/extensions/keyword-extractor/extension.test.d.ts.map +0 -1
- package/dist/extras/extensions/keyword-extractor/extension.test.js.map +0 -1
- package/dist/extras/extensions/keyword-extractor/index.d.ts.map +0 -1
- package/dist/extras/extensions/keyword-extractor/index.js.map +0 -1
- package/dist/extras/extensions/keyword-extractor/nlp.d.ts.map +0 -1
- package/dist/extras/extensions/keyword-extractor/nlp.js.map +0 -1
- package/dist/extras/extensions/keyword-extractor/nlp.test.d.ts.map +0 -1
- package/dist/extras/extensions/keyword-extractor/nlp.test.js.map +0 -1
- package/dist/extras/extensions/logger/extension.d.ts.map +0 -1
- package/dist/extras/extensions/logger/extension.js.map +0 -1
- package/dist/extras/extensions/logger/index.d.ts.map +0 -1
- package/dist/extras/extensions/logger/index.js.map +0 -1
- package/dist/extras/extensions/mcp/extension.d.ts.map +0 -1
- package/dist/extras/extensions/mcp/extension.js.map +0 -1
- package/dist/extras/extensions/mcp/extension.test.d.ts.map +0 -1
- package/dist/extras/extensions/mcp/extension.test.js.map +0 -1
- package/dist/extras/extensions/mcp/index.d.ts.map +0 -1
- package/dist/extras/extensions/mcp/index.js.map +0 -1
- package/dist/extras/extensions/mem-session/extension.d.ts.map +0 -1
- package/dist/extras/extensions/mem-session/extension.js.map +0 -1
- package/dist/extras/extensions/mem-session/index.d.ts.map +0 -1
- package/dist/extras/extensions/mem-session/index.js.map +0 -1
- package/dist/extras/extensions/memory/extension.d.ts.map +0 -1
- package/dist/extras/extensions/memory/extension.js.map +0 -1
- package/dist/extras/extensions/memory/index.d.ts +0 -5
- package/dist/extras/extensions/memory/index.d.ts.map +0 -1
- package/dist/extras/extensions/memory/index.js.map +0 -1
- package/dist/extras/extensions/memory/llm-extractor.d.ts.map +0 -1
- package/dist/extras/extensions/memory/llm-extractor.js.map +0 -1
- package/dist/extras/extensions/memory/types.d.ts.map +0 -1
- package/dist/extras/extensions/memory/types.js.map +0 -1
- package/dist/extras/extensions/otel/extension.d.ts.map +0 -1
- package/dist/extras/extensions/otel/extension.js.map +0 -1
- package/dist/extras/extensions/otel/extension.test.d.ts.map +0 -1
- package/dist/extras/extensions/otel/extension.test.js.map +0 -1
- package/dist/extras/extensions/otel/index.d.ts.map +0 -1
- package/dist/extras/extensions/otel/index.js.map +0 -1
- package/dist/extras/extensions/otel/jsonl-span-exporter.d.ts.map +0 -1
- package/dist/extras/extensions/otel/jsonl-span-exporter.js.map +0 -1
- package/dist/extras/extensions/scripts/extension.d.ts.map +0 -1
- package/dist/extras/extensions/scripts/extension.js.map +0 -1
- package/dist/extras/extensions/scripts/extension.test.d.ts.map +0 -1
- package/dist/extras/extensions/scripts/extension.test.js.map +0 -1
- package/dist/extras/extensions/scripts/index.d.ts.map +0 -1
- package/dist/extras/extensions/scripts/index.js.map +0 -1
- package/dist/extras/extensions/skills/extension.d.ts.map +0 -1
- package/dist/extras/extensions/skills/extension.js.map +0 -1
- package/dist/extras/extensions/skills/extension.test.d.ts.map +0 -1
- package/dist/extras/extensions/skills/extension.test.js.map +0 -1
- package/dist/extras/extensions/skills/index.d.ts.map +0 -1
- package/dist/extras/extensions/skills/index.js.map +0 -1
- package/dist/extras/extensions/streaming/extension.d.ts.map +0 -1
- package/dist/extras/extensions/streaming/extension.js.map +0 -1
- package/dist/extras/extensions/streaming/index.d.ts.map +0 -1
- package/dist/extras/extensions/streaming/index.js.map +0 -1
- package/dist/extras/extensions/structured-output/extension.d.ts.map +0 -1
- package/dist/extras/extensions/structured-output/extension.js.map +0 -1
- package/dist/extras/extensions/structured-output/index.d.ts.map +0 -1
- package/dist/extras/extensions/structured-output/index.js.map +0 -1
- package/dist/extras/extensions/timestamp/extension.d.ts.map +0 -1
- package/dist/extras/extensions/timestamp/extension.js.map +0 -1
- package/dist/extras/extensions/timestamp/index.d.ts.map +0 -1
- package/dist/extras/extensions/timestamp/index.js.map +0 -1
- package/dist/extras/extensions/token-stats/extension.d.ts.map +0 -1
- package/dist/extras/extensions/token-stats/extension.js.map +0 -1
- package/dist/extras/extensions/token-stats/extension.test.d.ts.map +0 -1
- package/dist/extras/extensions/token-stats/extension.test.js.map +0 -1
- package/dist/extras/extensions/token-stats/index.d.ts.map +0 -1
- package/dist/extras/extensions/token-stats/index.js.map +0 -1
- package/dist/extras/extensions/tool-buffer/extension.d.ts.map +0 -1
- package/dist/extras/extensions/tool-buffer/extension.js.map +0 -1
- package/dist/extras/extensions/tool-buffer/extension.test.d.ts.map +0 -1
- package/dist/extras/extensions/tool-buffer/extension.test.js.map +0 -1
- package/dist/extras/extensions/tool-buffer/index.d.ts.map +0 -1
- package/dist/extras/extensions/tool-buffer/index.js.map +0 -1
- package/dist/extras/extensions/weather-tool/extension.d.ts.map +0 -1
- package/dist/extras/extensions/weather-tool/extension.js.map +0 -1
- package/dist/extras/extensions/weather-tool/index.d.ts.map +0 -1
- package/dist/extras/extensions/weather-tool/index.js.map +0 -1
- package/dist/extras/models/ai-sdk-resolver.d.ts.map +0 -1
- package/dist/extras/models/ai-sdk-resolver.js.map +0 -1
- package/dist/extras/models/index.d.ts.map +0 -1
- package/dist/extras/models/index.js.map +0 -1
- package/dist/extras/models/routing-model.d.ts.map +0 -1
- package/dist/extras/models/routing-model.js.map +0 -1
- package/dist/extras/openai-provider/index.d.ts.map +0 -1
- package/dist/extras/openai-provider/index.js.map +0 -1
- package/dist/extras/openai-provider/server.d.ts.map +0 -1
- package/dist/extras/openai-provider/server.js.map +0 -1
- /package/dist/{extras/extensions → extensions}/auto-steer/extension.js +0 -0
- /package/dist/{extras/extensions → extensions}/auto-steer/extension.json +0 -0
- /package/dist/{extras/extensions → extensions}/auto-steer/index.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/auto-steer/index.js +0 -0
- /package/dist/{extras/extensions → extensions}/code-tools/extension.json +0 -0
- /package/dist/{extras/extensions → extensions}/code-tools/extension.test.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/code-tools/index.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/code-tools/index.js +0 -0
- /package/dist/{extras/extensions → extensions}/code-tools/tools/find.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/code-tools/tools/grep.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/code-tools/tools/read.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/code-tools/tools/write.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/disk-session/extension.json +0 -0
- /package/dist/{extras/extensions → extensions}/disk-session/extension.test.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/emoji/extension.js +0 -0
- /package/dist/{extras/extensions → extensions}/emoji/extension.json +0 -0
- /package/dist/{extras/extensions → extensions}/emoji/index.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/emoji/index.js +0 -0
- /package/dist/{extras/extensions → extensions}/filesystem/extension.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/filesystem/extension.js +0 -0
- /package/dist/{extras/extensions → extensions}/filesystem/extension.json +0 -0
- /package/dist/{extras/extensions → extensions}/filesystem/extension.test.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/filesystem/extension.test.js +0 -0
- /package/dist/{extras/extensions → extensions}/filesystem/index.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/filesystem/index.js +0 -0
- /package/dist/{extras/extensions → extensions}/keyword-extractor/extension.json +0 -0
- /package/dist/{extras/extensions → extensions}/keyword-extractor/extension.test.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/keyword-extractor/extension.test.js +0 -0
- /package/dist/{extras/extensions → extensions}/keyword-extractor/index.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/keyword-extractor/index.js +0 -0
- /package/dist/{extras/extensions → extensions}/keyword-extractor/nlp.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/keyword-extractor/nlp.js +0 -0
- /package/dist/{extras/extensions → extensions}/keyword-extractor/nlp.test.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/keyword-extractor/nlp.test.js +0 -0
- /package/dist/{extras/extensions → extensions}/logger/extension.json +0 -0
- /package/dist/{extras/extensions → extensions}/logger/index.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/logger/index.js +0 -0
- /package/dist/{extras/extensions → extensions}/mcp/extension.js +0 -0
- /package/dist/{extras/extensions → extensions}/mcp/extension.json +0 -0
- /package/dist/{extras/extensions → extensions}/mcp/extension.test.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/mcp/index.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/mcp/index.js +0 -0
- /package/dist/{extras/extensions → extensions}/mem-session/extension.js +0 -0
- /package/dist/{extras/extensions → extensions}/mem-session/extension.json +0 -0
- /package/dist/{extras/extensions → extensions}/mem-session/index.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/mem-session/index.js +0 -0
- /package/dist/{extras/extensions → extensions}/memory/extension.json +0 -0
- /package/dist/{extras/extensions → extensions}/memory/index.js +0 -0
- /package/dist/{extras/extensions → extensions}/memory/types.js +0 -0
- /package/dist/{extras/extensions → extensions}/otel/extension.json +0 -0
- /package/dist/{extras/extensions → extensions}/otel/extension.test.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/otel/index.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/otel/index.js +0 -0
- /package/dist/{extras/extensions → extensions}/otel/jsonl-span-exporter.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/otel/jsonl-span-exporter.js +0 -0
- /package/dist/{extras/extensions → extensions}/scripts/extension.js +0 -0
- /package/dist/{extras/extensions → extensions}/scripts/extension.json +0 -0
- /package/dist/{extras/extensions → extensions}/scripts/extension.test.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/scripts/extension.test.js +0 -0
- /package/dist/{extras/extensions → extensions}/scripts/index.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/scripts/index.js +0 -0
- /package/dist/{extras/extensions → extensions}/skills/extension.js +0 -0
- /package/dist/{extras/extensions → extensions}/skills/extension.json +0 -0
- /package/dist/{extras/extensions → extensions}/skills/extension.test.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/skills/index.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/skills/index.js +0 -0
- /package/dist/{extras/extensions → extensions}/streaming/extension.js +0 -0
- /package/dist/{extras/extensions → extensions}/streaming/extension.json +0 -0
- /package/dist/{extras/extensions → extensions}/streaming/index.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/streaming/index.js +0 -0
- /package/dist/{extras/extensions → extensions}/structured-output/extension.js +0 -0
- /package/dist/{extras/extensions → extensions}/structured-output/extension.json +0 -0
- /package/dist/{extras/extensions → extensions}/structured-output/index.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/structured-output/index.js +0 -0
- /package/dist/{extras/extensions → extensions}/timestamp/extension.js +0 -0
- /package/dist/{extras/extensions → extensions}/timestamp/extension.json +0 -0
- /package/dist/{extras/extensions → extensions}/timestamp/index.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/timestamp/index.js +0 -0
- /package/dist/{extras/extensions → extensions}/token-stats/extension.js +0 -0
- /package/dist/{extras/extensions → extensions}/token-stats/extension.json +0 -0
- /package/dist/{extras/extensions → extensions}/token-stats/extension.test.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/token-stats/index.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/token-stats/index.js +0 -0
- /package/dist/{extras/extensions → extensions}/tool-buffer/extension.js +0 -0
- /package/dist/{extras/extensions → extensions}/tool-buffer/extension.json +0 -0
- /package/dist/{extras/extensions → extensions}/tool-buffer/extension.test.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/tool-buffer/index.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/tool-buffer/index.js +0 -0
- /package/dist/{extras/extensions → extensions}/weather-tool/extension.js +0 -0
- /package/dist/{extras/extensions → extensions}/weather-tool/extension.json +0 -0
- /package/dist/{extras/extensions → extensions}/weather-tool/index.d.ts +0 -0
- /package/dist/{extras/extensions → extensions}/weather-tool/index.js +0 -0
- /package/dist/{extras/models → models}/index.d.ts +0 -0
- /package/dist/{extras/models → models}/index.js +0 -0
- /package/dist/{extras/openai-provider → openai-provider}/index.d.ts +0 -0
- /package/dist/{extras/openai-provider → openai-provider}/index.js +0 -0
- /package/dist/{extras/openai-provider → openai-provider}/server.js +0 -0
|
@@ -0,0 +1,818 @@
|
|
|
1
|
+
import { readFileSync, appendFileSync, mkdirSync, readdirSync, writeFileSync, existsSync, renameSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { messageContentToText } from '@xandout/libra-harness';
|
|
4
|
+
/**
|
|
5
|
+
* Default resolver: reads `metadata.session` as a `SessionIdentity`,
|
|
6
|
+
* falls back to `metadata.sessionId` as a plain key, then to `'default'`.
|
|
7
|
+
*/
|
|
8
|
+
const defaultResolver = {
|
|
9
|
+
resolve(metadata) {
|
|
10
|
+
const session = metadata.session;
|
|
11
|
+
if (session && typeof session.key === 'string')
|
|
12
|
+
return session;
|
|
13
|
+
const sessionId = metadata.sessionId;
|
|
14
|
+
if (sessionId)
|
|
15
|
+
return { key: sessionId, messageTs: '' };
|
|
16
|
+
return { key: 'default', messageTs: '' };
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Sanitizes a message sequence to strictly conform to LLM tool-call schemas:
|
|
21
|
+
* 1. Every message with role 'tool' MUST directly follow an assistant message
|
|
22
|
+
* with a matching tool call id (or follow valid sibling tool messages for that same assistant).
|
|
23
|
+
* Any orphan or duplicate tool messages are dropped.
|
|
24
|
+
* 2. If an assistant message has toolCalls, but some or all tool calls were never
|
|
25
|
+
* fulfilled (e.g. session interrupted mid-turn or tool execution halted), its
|
|
26
|
+
* toolCalls array is pruned to only the fulfilled tool calls.
|
|
27
|
+
* 3. If an assistant message has toolCalls but none were fulfilled:
|
|
28
|
+
* - If the assistant message has non-empty text content, toolCalls is stripped.
|
|
29
|
+
* - If the assistant message has no text content, it is removed entirely.
|
|
30
|
+
*/
|
|
31
|
+
export function sanitizeConversationMessages(messages) {
|
|
32
|
+
const result = [];
|
|
33
|
+
let pendingAssistantIdx = -1;
|
|
34
|
+
let pendingToolCallIds = null;
|
|
35
|
+
let fulfilledToolCallIds = new Set();
|
|
36
|
+
function finalizePendingAssistant() {
|
|
37
|
+
if (pendingAssistantIdx === -1 || !pendingToolCallIds)
|
|
38
|
+
return;
|
|
39
|
+
const assistantMsg = result[pendingAssistantIdx];
|
|
40
|
+
if (assistantMsg && assistantMsg.toolCalls) {
|
|
41
|
+
const keptCalls = assistantMsg.toolCalls.filter((tc) => fulfilledToolCallIds.has(tc.id));
|
|
42
|
+
if (keptCalls.length > 0) {
|
|
43
|
+
assistantMsg.toolCalls = keptCalls;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
delete assistantMsg.toolCalls;
|
|
47
|
+
const text = messageContentToText(assistantMsg.content).trim();
|
|
48
|
+
if (!text) {
|
|
49
|
+
result.splice(pendingAssistantIdx, 1);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
pendingAssistantIdx = -1;
|
|
54
|
+
pendingToolCallIds = null;
|
|
55
|
+
fulfilledToolCallIds = new Set();
|
|
56
|
+
}
|
|
57
|
+
for (const msg of messages) {
|
|
58
|
+
if (msg.role === 'tool') {
|
|
59
|
+
if (!pendingToolCallIds || !msg.toolCallId || !pendingToolCallIds.has(msg.toolCallId)) {
|
|
60
|
+
// Orphan tool message with no matching preceding assistant tool call — drop it
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (fulfilledToolCallIds.has(msg.toolCallId)) {
|
|
64
|
+
// Duplicate tool message for the same tool call id — drop it
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
fulfilledToolCallIds.add(msg.toolCallId);
|
|
68
|
+
result.push(msg);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
// Any non-tool message finalizes the preceding assistant's tool-calls
|
|
72
|
+
finalizePendingAssistant();
|
|
73
|
+
if (msg.role === 'assistant') {
|
|
74
|
+
if (msg.toolCalls && msg.toolCalls.length > 0) {
|
|
75
|
+
pendingAssistantIdx = result.length;
|
|
76
|
+
pendingToolCallIds = new Set(msg.toolCalls.map((tc) => tc.id));
|
|
77
|
+
fulfilledToolCallIds = new Set();
|
|
78
|
+
result.push({ ...msg, toolCalls: [...msg.toolCalls] });
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
result.push(msg);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// user or system message
|
|
86
|
+
result.push(msg);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
finalizePendingAssistant();
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
export function isContextLengthError(err) {
|
|
93
|
+
if (!err)
|
|
94
|
+
return false;
|
|
95
|
+
const msg = (err instanceof Error ? err.message : String(err)).toLowerCase();
|
|
96
|
+
return (msg.includes('context_length') ||
|
|
97
|
+
msg.includes('context length') ||
|
|
98
|
+
msg.includes('prompt is too long') ||
|
|
99
|
+
msg.includes('maximum context length') ||
|
|
100
|
+
msg.includes('exceeds the context window') ||
|
|
101
|
+
msg.includes('too many tokens') ||
|
|
102
|
+
msg.includes('token limit') ||
|
|
103
|
+
msg.includes('max_tokens') ||
|
|
104
|
+
msg.includes('string_above_max_length'));
|
|
105
|
+
}
|
|
106
|
+
export async function generateSessionSummary(model, records) {
|
|
107
|
+
const summarySystemPrompt = 'You are a precise conversation summarizer for an AI agent. ' +
|
|
108
|
+
'Provide a dense, structured summary of the provided conversation history. ' +
|
|
109
|
+
'Preserve all essential technical details, user goals, key decisions, file paths, ' +
|
|
110
|
+
'code changes, commands executed, tool outputs, errors, and pending tasks. ' +
|
|
111
|
+
'Do not include conversational filler. Output only the summary.';
|
|
112
|
+
const transcript = records
|
|
113
|
+
.map((r) => {
|
|
114
|
+
const text = messageContentToText(r.content).trim();
|
|
115
|
+
const role = r.role.toUpperCase();
|
|
116
|
+
const tools = r.toolCalls ? ` [called: ${r.toolCalls.map((tc) => tc.name).join(', ')}]` : '';
|
|
117
|
+
return `[${role}${tools}]: ${text}`;
|
|
118
|
+
})
|
|
119
|
+
.filter((line) => line.length > 0)
|
|
120
|
+
.join('\n\n');
|
|
121
|
+
const response = await model.generate({
|
|
122
|
+
messages: [
|
|
123
|
+
{
|
|
124
|
+
role: 'user',
|
|
125
|
+
content: `Please summarize this earlier conversation segment to preserve context:\n\n${transcript}`,
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
systemPrompt: summarySystemPrompt,
|
|
129
|
+
});
|
|
130
|
+
return messageContentToText(response.message.content).trim();
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Disk-backed session extension.
|
|
134
|
+
*
|
|
135
|
+
* ## Architecture
|
|
136
|
+
*
|
|
137
|
+
* **One JSONL file per session** (`<sessionKey>.jsonl`). Append-only —
|
|
138
|
+
* every message the agent sees (human or bot, top-level or threaded) is
|
|
139
|
+
* appended. This log is the source of truth.
|
|
140
|
+
*
|
|
141
|
+
* **Snapshot at beforeTurn**: when a turn starts, the extension takes a
|
|
142
|
+
* read-only snapshot of the session log at that moment. The turn runs
|
|
143
|
+
* against the snapshot. Concurrent turns each take their own snapshot
|
|
144
|
+
* and don't see each other's in-progress work.
|
|
145
|
+
*
|
|
146
|
+
* **Fork for threads**: when a thread reply comes in (identity has
|
|
147
|
+
* `threadTs`), the snapshot is filtered to include only:
|
|
148
|
+
* 1. Top-level messages before the thread parent (channel context)
|
|
149
|
+
* 2. All messages in that thread (parent + replies)
|
|
150
|
+
* 3. Recent top-level messages after the last thread reply
|
|
151
|
+
*
|
|
152
|
+
* **Append after turn**: when the turn finishes, new messages (user +
|
|
153
|
+
* assistant + tool calls) are appended to the session log. No data is
|
|
154
|
+
* ever rewritten or reordered.
|
|
155
|
+
*
|
|
156
|
+
* **Stable prefix for LLM caching**: the messages array is built as
|
|
157
|
+
* [system prompt] + [stable session history] + [new user message]
|
|
158
|
+
* The session history grows monotonically (append-only), so earlier
|
|
159
|
+
* tokens stay cached upstream. Per-turn metadata is injected at the end
|
|
160
|
+
* of the context (by a host-side `beforeContext` hook), not the
|
|
161
|
+
* beginning, to preserve the cache prefix.
|
|
162
|
+
*
|
|
163
|
+
* ## Host integration
|
|
164
|
+
*
|
|
165
|
+
* The host supplies a {@link SessionResolver} (or writes
|
|
166
|
+
* `metadata.session`) so the extension knows which session a turn
|
|
167
|
+
* belongs to. The extension itself is host-agnostic — it doesn't know
|
|
168
|
+
* about Slack, Discord, or any other platform.
|
|
169
|
+
*/
|
|
170
|
+
export default function createDiskSessionExtension(config) {
|
|
171
|
+
const dir = config?.sessionDir ?? './sessions';
|
|
172
|
+
const maxRecords = config?.maxRecords ?? 1000;
|
|
173
|
+
const maxContextMessages = config?.maxContextMessages ?? 50;
|
|
174
|
+
const contextEvictionStep = config?.contextEvictionStep ?? Math.max(1, Math.floor(maxContextMessages / 2));
|
|
175
|
+
const autoSummarize = config?.autoSummarize ?? true;
|
|
176
|
+
const fallbackThresholds = config?.fallbackThresholds ?? [
|
|
177
|
+
maxContextMessages,
|
|
178
|
+
Math.max(10, Math.floor(maxContextMessages / 2)),
|
|
179
|
+
Math.max(5, Math.floor(maxContextMessages / 4)),
|
|
180
|
+
];
|
|
181
|
+
const channelContextMessages = config?.channelContextMessages ?? 10;
|
|
182
|
+
const recentChannelMessages = config?.recentChannelMessages ?? 5;
|
|
183
|
+
const loadOnStartup = config?.loadOnStartup ?? true;
|
|
184
|
+
const resolver = config?.resolver ?? defaultResolver;
|
|
185
|
+
mkdirSync(dir, { recursive: true });
|
|
186
|
+
// In-memory cache: sessionKey → SessionRecord[]
|
|
187
|
+
// This is the live log. Reads take a snapshot (copy); writes append.
|
|
188
|
+
const store = new Map();
|
|
189
|
+
// Effective context limit per session for fallback backoff
|
|
190
|
+
const sessionLimits = new Map();
|
|
191
|
+
// ── Load existing sessions from disk ──────────────────────────
|
|
192
|
+
if (loadOnStartup) {
|
|
193
|
+
try {
|
|
194
|
+
const files = readdirSync(dir).filter((f) => f.endsWith('.jsonl'));
|
|
195
|
+
let totalRecords = 0;
|
|
196
|
+
for (const file of files) {
|
|
197
|
+
try {
|
|
198
|
+
const raw = readFileSync(join(dir, file), 'utf-8');
|
|
199
|
+
const lines = raw.split('\n').filter((l) => l.trim());
|
|
200
|
+
const records = lines.map((l) => JSON.parse(l));
|
|
201
|
+
const sessionKey = file.replace(/\.jsonl$/, '');
|
|
202
|
+
store.set(sessionKey, records.slice(-maxRecords));
|
|
203
|
+
totalRecords += records.length;
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// Skip corrupt files.
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (store.size > 0 && config?.verbose !== false) {
|
|
210
|
+
console.log(`[disk-session] loaded ${store.size} session(s), ${totalRecords} record(s) from ${dir}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// Directory doesn't exist — start empty.
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function filePath(sessionKey) {
|
|
218
|
+
const safe = sessionKey.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
219
|
+
return join(dir, `${safe}.jsonl`);
|
|
220
|
+
}
|
|
221
|
+
function appendToFile(sessionKey, records) {
|
|
222
|
+
if (records.length === 0)
|
|
223
|
+
return;
|
|
224
|
+
const lines = records.map((r) => JSON.stringify(r)).join('\n') + '\n';
|
|
225
|
+
try {
|
|
226
|
+
appendFileSync(filePath(sessionKey), lines);
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
console.error(`[disk-session] failed to append to ${sessionKey}:`, err);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function persistRecords(sessionKey, newRecords) {
|
|
233
|
+
const records = store.get(sessionKey) ?? [];
|
|
234
|
+
records.push(...newRecords);
|
|
235
|
+
if (records.length > maxRecords) {
|
|
236
|
+
records.splice(0, records.length - maxRecords);
|
|
237
|
+
}
|
|
238
|
+
store.set(sessionKey, records);
|
|
239
|
+
appendToFile(sessionKey, newRecords);
|
|
240
|
+
}
|
|
241
|
+
function handleCompaction(turn, key, messageTs) {
|
|
242
|
+
const summary = turn.response?.message ?? '';
|
|
243
|
+
const oldPath = filePath(key);
|
|
244
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
245
|
+
const archivedName = `${key.replace(/[^a-zA-Z0-9_-]/g, '_')}_${timestamp}.jsonl`;
|
|
246
|
+
const archivedPath = join(dir, archivedName);
|
|
247
|
+
if (existsSync(oldPath)) {
|
|
248
|
+
renameSync(oldPath, archivedPath);
|
|
249
|
+
}
|
|
250
|
+
const summaryRecord = {
|
|
251
|
+
role: 'assistant',
|
|
252
|
+
content: `[Session summary — prior conversation compacted ${new Date().toISOString()}]\n\n${summary}`,
|
|
253
|
+
ts: messageTs || String(Date.now() / 1000),
|
|
254
|
+
recordedAt: new Date().toISOString(),
|
|
255
|
+
};
|
|
256
|
+
store.set(key, [summaryRecord]);
|
|
257
|
+
appendToFile(key, [summaryRecord]);
|
|
258
|
+
turn.metadata['_compactArchivedFile'] = archivedName;
|
|
259
|
+
}
|
|
260
|
+
// ── Convert between Message and SessionRecord ──────────────────
|
|
261
|
+
function toMessage(r) {
|
|
262
|
+
return {
|
|
263
|
+
// `toMessage` is only called on records that passed
|
|
264
|
+
// `isConversationMessage`, which excludes 'control' (and 'tool').
|
|
265
|
+
role: r.role,
|
|
266
|
+
content: r.content,
|
|
267
|
+
...(r.toolCalls ? { toolCalls: r.toolCalls } : {}),
|
|
268
|
+
...(r.toolCallId ? { toolCallId: r.toolCallId } : {}),
|
|
269
|
+
...(r.name ? { name: r.name } : {}),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function toRecord(msg, ts, threadTs) {
|
|
273
|
+
return {
|
|
274
|
+
role: msg.role,
|
|
275
|
+
content: msg.content,
|
|
276
|
+
...(msg.toolCalls ? { toolCalls: msg.toolCalls } : {}),
|
|
277
|
+
...(msg.toolCallId ? { toolCallId: msg.toolCallId } : {}),
|
|
278
|
+
...(msg.name ? { name: msg.name } : {}),
|
|
279
|
+
ts,
|
|
280
|
+
...(threadTs ? { threadTs } : {}),
|
|
281
|
+
recordedAt: new Date().toISOString(),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
// Filter for context windows: keep user, system, assistant, and tool messages.
|
|
285
|
+
// We no longer filter out tool calls here. Instead, we rely on turn-boundary
|
|
286
|
+
// slicing to ensure we don't split a tool sequence.
|
|
287
|
+
const isConversationMessage = (r) => {
|
|
288
|
+
if (r.role === 'control')
|
|
289
|
+
return false;
|
|
290
|
+
return true;
|
|
291
|
+
};
|
|
292
|
+
// Drop trailing user messages that don't have a following assistant
|
|
293
|
+
// response. This happens when a turn was interrupted (crash, halt,
|
|
294
|
+
// kill -9). Including an unanswered user request from a prior session
|
|
295
|
+
// confuses the agent — it sees a request it never responded to.
|
|
296
|
+
function dropIncompleteTrailingTurn(records) {
|
|
297
|
+
const result = [...records];
|
|
298
|
+
while (result.length > 0 && result[result.length - 1].role === 'user') {
|
|
299
|
+
result.pop();
|
|
300
|
+
}
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
303
|
+
function dropIncompleteTrailingMessages(messages) {
|
|
304
|
+
const result = [...messages];
|
|
305
|
+
while (result.length > 0 && result[result.length - 1].role === 'user') {
|
|
306
|
+
result.pop();
|
|
307
|
+
}
|
|
308
|
+
return result;
|
|
309
|
+
}
|
|
310
|
+
function sliceAtTurnBoundary(records, max, step) {
|
|
311
|
+
if (records.length <= max)
|
|
312
|
+
return records;
|
|
313
|
+
const evictionStep = step && step > 0 ? step : 1;
|
|
314
|
+
let startIndex;
|
|
315
|
+
if (evictionStep <= 1) {
|
|
316
|
+
startIndex = records.length - max;
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
const baseline = Math.max(0, max - evictionStep);
|
|
320
|
+
const excess = records.length - baseline;
|
|
321
|
+
const chunks = Math.floor(excess / evictionStep);
|
|
322
|
+
startIndex = chunks * evictionStep;
|
|
323
|
+
}
|
|
324
|
+
// Skip forward until we find a turn boundary ('user' or 'system')
|
|
325
|
+
while (startIndex < records.length &&
|
|
326
|
+
records[startIndex].role !== 'user' &&
|
|
327
|
+
records[startIndex].role !== 'system') {
|
|
328
|
+
startIndex++;
|
|
329
|
+
}
|
|
330
|
+
return records.slice(startIndex);
|
|
331
|
+
}
|
|
332
|
+
// ── Build context (the "fork") from a snapshot ────────────────
|
|
333
|
+
// The snapshot is a read-only copy of the session records at the
|
|
334
|
+
// moment the turn started. This function builds the messages array
|
|
335
|
+
// that the agent will see.
|
|
336
|
+
//
|
|
337
|
+
// For top-level messages and DMs: last N records (sliced at turn boundary).
|
|
338
|
+
// For thread replies: channel context before parent + thread history.
|
|
339
|
+
function buildContext(snapshot, threadTs, isDirect, maxContext = maxContextMessages, step = contextEvictionStep) {
|
|
340
|
+
if (snapshot.length === 0)
|
|
341
|
+
return [];
|
|
342
|
+
let rawRecords;
|
|
343
|
+
// DM or top-level message: last N messages, but safe-sliced to not
|
|
344
|
+
// split tool-call sequences.
|
|
345
|
+
if (isDirect || !threadTs) {
|
|
346
|
+
const filtered = snapshot.filter(isConversationMessage);
|
|
347
|
+
const complete = dropIncompleteTrailingTurn(filtered);
|
|
348
|
+
rawRecords = sliceAtTurnBoundary(complete, maxContext, step);
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
// Thread: fork from channel context.
|
|
352
|
+
const parentIdx = snapshot.findIndex((r) => r.ts === threadTs);
|
|
353
|
+
if (parentIdx === -1) {
|
|
354
|
+
// Parent not in snapshot (evicted from cache or very old).
|
|
355
|
+
// Fall back to last N messages.
|
|
356
|
+
const filtered = snapshot.filter(isConversationMessage);
|
|
357
|
+
const complete = dropIncompleteTrailingTurn(filtered);
|
|
358
|
+
rawRecords = sliceAtTurnBoundary(complete, maxContext, step);
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
// Top-level messages before the parent (channel context at fork point).
|
|
362
|
+
const topLevelBefore = snapshot
|
|
363
|
+
.slice(0, parentIdx)
|
|
364
|
+
.filter((r) => !r.threadTs && isConversationMessage(r));
|
|
365
|
+
const slicedTopLevelBefore = sliceAtTurnBoundary(topLevelBefore, channelContextMessages);
|
|
366
|
+
// All messages in this thread (including the parent).
|
|
367
|
+
const threadMessages = snapshot
|
|
368
|
+
.filter((r) => (r.ts === threadTs || r.threadTs === threadTs) && isConversationMessage(r));
|
|
369
|
+
// Recent top-level messages after the last thread reply.
|
|
370
|
+
const lastThreadTs = threadMessages[threadMessages.length - 1]?.ts ?? threadTs;
|
|
371
|
+
let lastThreadIdx = parentIdx;
|
|
372
|
+
for (let i = snapshot.length - 1; i >= 0; i--) {
|
|
373
|
+
if (snapshot[i].ts === lastThreadTs) {
|
|
374
|
+
lastThreadIdx = i;
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const recentTopLevel = snapshot
|
|
379
|
+
.slice(lastThreadIdx + 1)
|
|
380
|
+
.filter((r) => !r.threadTs && isConversationMessage(r));
|
|
381
|
+
const slicedRecentTopLevel = recentChannelMessages > 0
|
|
382
|
+
? sliceAtTurnBoundary(recentTopLevel, recentChannelMessages)
|
|
383
|
+
: [];
|
|
384
|
+
rawRecords = [...slicedTopLevelBefore, ...threadMessages, ...slicedRecentTopLevel];
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const messages = rawRecords.map(toMessage);
|
|
388
|
+
const sanitized = sanitizeConversationMessages(messages);
|
|
389
|
+
return dropIncompleteTrailingMessages(sanitized);
|
|
390
|
+
}
|
|
391
|
+
// ── Resolve session identity from turn metadata ───────────────
|
|
392
|
+
function identityFromCtx(ctx) {
|
|
393
|
+
return resolver.resolve(ctx.turn.request.metadata ?? {});
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
name: 'disk-session',
|
|
397
|
+
priority: -100,
|
|
398
|
+
install(agent) {
|
|
399
|
+
const activeModel = config?.model ?? agent.model;
|
|
400
|
+
// ── beforeTurn: take snapshot, build context, prepend ─────
|
|
401
|
+
// The snapshot is a copy of the session records at this moment.
|
|
402
|
+
// Concurrent turns each get their own snapshot — they don't
|
|
403
|
+
// see each other's in-progress work.
|
|
404
|
+
agent.hook('beforeTurn', 'disk-session', async (ctx) => {
|
|
405
|
+
const identity = identityFromCtx(ctx);
|
|
406
|
+
if (!identity)
|
|
407
|
+
return;
|
|
408
|
+
const { key, messageTs, threadTs, isDirect } = identity;
|
|
409
|
+
const isDm = isDirect ?? false;
|
|
410
|
+
const effectiveMax = sessionLimits.get(key) ?? maxContextMessages;
|
|
411
|
+
// ── Auto-summarization ──────────────────────────────────
|
|
412
|
+
// When conversation history reaches effectiveMax, auto-summarize the
|
|
413
|
+
// oldest chunk so earlier context is retained without thrashing prompt cache.
|
|
414
|
+
const liveRecords = store.get(key) ?? [];
|
|
415
|
+
const convCount = liveRecords.filter(isConversationMessage).length;
|
|
416
|
+
let autoCompacted = false;
|
|
417
|
+
let compactedCount = 0;
|
|
418
|
+
if (autoSummarize && activeModel && convCount >= effectiveMax) {
|
|
419
|
+
const step = config?.contextEvictionStep ?? Math.max(1, Math.floor(effectiveMax / 2));
|
|
420
|
+
let cutIndex = Math.min(step, liveRecords.length - 1);
|
|
421
|
+
while (cutIndex < liveRecords.length &&
|
|
422
|
+
liveRecords[cutIndex].role !== 'user' &&
|
|
423
|
+
liveRecords[cutIndex].role !== 'system') {
|
|
424
|
+
cutIndex++;
|
|
425
|
+
}
|
|
426
|
+
if (cutIndex > 0 && cutIndex < liveRecords.length) {
|
|
427
|
+
const recordsToSummarize = liveRecords.slice(0, cutIndex);
|
|
428
|
+
const recordsToKeep = liveRecords.slice(cutIndex);
|
|
429
|
+
try {
|
|
430
|
+
const summaryText = await generateSessionSummary(activeModel, recordsToSummarize);
|
|
431
|
+
if (summaryText) {
|
|
432
|
+
const summaryRecord = {
|
|
433
|
+
role: 'assistant',
|
|
434
|
+
content: `[Conversation summary — prior history compacted ${new Date().toISOString()}]\n\n${summaryText}`,
|
|
435
|
+
ts: recordsToSummarize[recordsToSummarize.length - 1]?.ts || String(Date.now() / 1000),
|
|
436
|
+
recordedAt: new Date().toISOString(),
|
|
437
|
+
};
|
|
438
|
+
const compactedRecords = [summaryRecord, ...recordsToKeep];
|
|
439
|
+
store.set(key, compactedRecords);
|
|
440
|
+
const oldPath = filePath(key);
|
|
441
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
442
|
+
const archivedName = `${key.replace(/[^a-zA-Z0-9_-]/g, '_')}_${timestamp}.jsonl`;
|
|
443
|
+
const archivedPath = join(dir, archivedName);
|
|
444
|
+
if (existsSync(oldPath)) {
|
|
445
|
+
try {
|
|
446
|
+
renameSync(oldPath, archivedPath);
|
|
447
|
+
}
|
|
448
|
+
catch { }
|
|
449
|
+
}
|
|
450
|
+
appendToFile(key, compactedRecords);
|
|
451
|
+
autoCompacted = true;
|
|
452
|
+
compactedCount = recordsToSummarize.length;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
catch (err) {
|
|
456
|
+
console.warn('[disk-session] auto-summarization failed, using window slice:', err);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
// Snapshot: copy the current records (read-only).
|
|
461
|
+
const currentRecords = store.get(key) ?? [];
|
|
462
|
+
const snapshot = [...currentRecords];
|
|
463
|
+
// Build the forked context from the snapshot.
|
|
464
|
+
const history = buildContext(snapshot, threadTs, isDm, effectiveMax, contextEvictionStep);
|
|
465
|
+
// ── Persist the user message immediately ────────────────
|
|
466
|
+
// Write the incoming user message to disk NOW, before the
|
|
467
|
+
// agent runs. If the process crashes mid-turn, the user's
|
|
468
|
+
// message is already saved. The assistant response is
|
|
469
|
+
// appended later in afterTurn.
|
|
470
|
+
//
|
|
471
|
+
// The system prompt is captured at afterTurn (not here) because
|
|
472
|
+
// beforeContext hooks (which run after beforeTurn) may modify
|
|
473
|
+
// ctx.turn.systemPrompt. Recording it here would miss those
|
|
474
|
+
// modifications. See afterTurn for the capture.
|
|
475
|
+
//
|
|
476
|
+
// Enrichment bag: any extension that ran before us in
|
|
477
|
+
// beforeTurn may have written into `sessionMeta`. We persist
|
|
478
|
+
// it opaquely — disk-session does not inspect the contents.
|
|
479
|
+
const sessionMeta = ctx.turn.metadata.sessionMeta;
|
|
480
|
+
const userRecord = {
|
|
481
|
+
...toRecord({ role: 'user', content: ctx.turn.request.message }, messageTs, threadTs),
|
|
482
|
+
...(sessionMeta ? { meta: sessionMeta } : {}),
|
|
483
|
+
};
|
|
484
|
+
persistRecords(key, [userRecord]);
|
|
485
|
+
// Track how many history messages we prepended so afterTurn
|
|
486
|
+
// knows where new messages start.
|
|
487
|
+
ctx.turn.metadata['_diskSessionHistoryLen'] = history.length;
|
|
488
|
+
// Immediately before the model acts: if auto-compaction occurred,
|
|
489
|
+
// inform the model so it can act accordingly. Never inform the human.
|
|
490
|
+
const messagesToPrepend = [...history];
|
|
491
|
+
if (autoCompacted) {
|
|
492
|
+
const modelNotice = {
|
|
493
|
+
role: 'system',
|
|
494
|
+
content: `[System Notice: Earlier conversation history (${compactedCount} messages) was automatically compacted into the summary above to preserve context. Refer to the summary for earlier decisions, code changes, and context.]`,
|
|
495
|
+
};
|
|
496
|
+
messagesToPrepend.push(modelNotice);
|
|
497
|
+
ctx.turn.metadata['_autoCompacted'] = true;
|
|
498
|
+
}
|
|
499
|
+
if (messagesToPrepend.length > 0) {
|
|
500
|
+
ctx.turn.messages = [...messagesToPrepend, ...ctx.turn.messages];
|
|
501
|
+
}
|
|
502
|
+
// Track all messages currently in ctx.turn.messages (both history and the
|
|
503
|
+
// initial user message just written) so beforeLLM only persists new steering messages.
|
|
504
|
+
const writtenMessages = new Set(ctx.turn.messages);
|
|
505
|
+
ctx.turn.metadata['_diskSessionWrittenMessages'] = writtenMessages;
|
|
506
|
+
});
|
|
507
|
+
// ── beforeLLM: persist any mid-turn steering messages ─────
|
|
508
|
+
// When turn.steer() is called, agent.ts injects steering messages
|
|
509
|
+
// into turn.messages with a [steering] prefix. We persist them to disk
|
|
510
|
+
// right before the LLM runs to respond to them.
|
|
511
|
+
agent.hook('beforeLLM', 'disk-session', async (ctx) => {
|
|
512
|
+
const identity = identityFromCtx(ctx);
|
|
513
|
+
if (!identity)
|
|
514
|
+
return;
|
|
515
|
+
const { key, messageTs, threadTs } = identity;
|
|
516
|
+
const written = ctx.turn.metadata['_diskSessionWrittenMessages'];
|
|
517
|
+
const sessionMeta = ctx.turn.metadata.sessionMeta;
|
|
518
|
+
const newSteering = [];
|
|
519
|
+
for (const msg of ctx.turn.messages) {
|
|
520
|
+
if (msg.role === 'user' && (!written || !written.has(msg))) {
|
|
521
|
+
newSteering.push({
|
|
522
|
+
...toRecord(msg, messageTs, threadTs),
|
|
523
|
+
...(sessionMeta ? { meta: sessionMeta } : {}),
|
|
524
|
+
});
|
|
525
|
+
written?.add(msg);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (newSteering.length > 0) {
|
|
529
|
+
const records = store.get(key) ?? [];
|
|
530
|
+
records.push(...newSteering);
|
|
531
|
+
if (records.length > maxRecords) {
|
|
532
|
+
records.splice(0, records.length - maxRecords);
|
|
533
|
+
}
|
|
534
|
+
store.set(key, records);
|
|
535
|
+
appendToFile(key, newSteering);
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
// ── afterLLM: accumulate token usage + append assistant msg ──
|
|
539
|
+
// Each LLM call returns usage (prompt/completion tokens). We
|
|
540
|
+
// accumulate them across all iterations in the turn so the final
|
|
541
|
+
// assistant record has the total cost.
|
|
542
|
+
//
|
|
543
|
+
// We also append the assistant message (including tool calls) to
|
|
544
|
+
// the session log immediately, so the JSONL file is updated
|
|
545
|
+
// iteration by iteration rather than only at afterTurn. Tool
|
|
546
|
+
// results are appended in the afterTool hook below.
|
|
547
|
+
agent.hook('afterLLM', 'disk-session', async (ctx) => {
|
|
548
|
+
const identity = identityFromCtx(ctx);
|
|
549
|
+
if (!identity)
|
|
550
|
+
return;
|
|
551
|
+
const { key, messageTs, threadTs } = identity;
|
|
552
|
+
const usage = ctx.modelResponse?.usage;
|
|
553
|
+
if (usage) {
|
|
554
|
+
const prev = ctx.turn.metadata['_diskSessionUsage'] ?? { promptTokens: 0, completionTokens: 0, iterations: 0 };
|
|
555
|
+
ctx.turn.metadata['_diskSessionUsage'] = {
|
|
556
|
+
promptTokens: prev.promptTokens + usage.promptTokens,
|
|
557
|
+
completionTokens: prev.completionTokens + usage.completionTokens,
|
|
558
|
+
iterations: prev.iterations + 1,
|
|
559
|
+
...(usage.cachedPromptTokens && {
|
|
560
|
+
cachedPromptTokens: (prev.cachedPromptTokens ?? 0) + usage.cachedPromptTokens,
|
|
561
|
+
}),
|
|
562
|
+
...(usage.reasoningTokens && {
|
|
563
|
+
reasoningTokens: (prev.reasoningTokens ?? 0) + usage.reasoningTokens,
|
|
564
|
+
}),
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
// ── Incremental append: write the assistant message now ──
|
|
568
|
+
// The agent core pushes the assistant message to turn.messages
|
|
569
|
+
// AFTER afterLLM fires, so we can't read it from there yet.
|
|
570
|
+
// Instead, we build the record from modelResponse.message.
|
|
571
|
+
const modelResponse = ctx.modelResponse;
|
|
572
|
+
if (modelResponse?.message) {
|
|
573
|
+
const sessionMeta = ctx.turn.metadata.sessionMeta;
|
|
574
|
+
const usage = modelResponse.usage;
|
|
575
|
+
const record = {
|
|
576
|
+
...toRecord(modelResponse.message, messageTs, threadTs),
|
|
577
|
+
...(usage ? { usage: {
|
|
578
|
+
promptTokens: usage.promptTokens,
|
|
579
|
+
completionTokens: usage.completionTokens,
|
|
580
|
+
iterations: 1,
|
|
581
|
+
...(usage.cachedPromptTokens !== undefined && { cachedPromptTokens: usage.cachedPromptTokens }),
|
|
582
|
+
...(usage.reasoningTokens !== undefined && { reasoningTokens: usage.reasoningTokens }),
|
|
583
|
+
} } : {}),
|
|
584
|
+
...(sessionMeta ? { meta: sessionMeta } : {}),
|
|
585
|
+
};
|
|
586
|
+
persistRecords(key, [record]);
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
// ── afterTool: append tool results incrementally ──────────────
|
|
590
|
+
// Each tool result is written to disk as soon as it completes,
|
|
591
|
+
// so the JSONL file reflects progress in real time.
|
|
592
|
+
agent.hook('afterTool', 'disk-session', async (ctx) => {
|
|
593
|
+
const identity = identityFromCtx(ctx);
|
|
594
|
+
if (!identity)
|
|
595
|
+
return;
|
|
596
|
+
const { key, messageTs, threadTs } = identity;
|
|
597
|
+
const toolResult = ctx.toolResult;
|
|
598
|
+
const toolCall = ctx.toolCall;
|
|
599
|
+
if (!toolResult || !toolCall)
|
|
600
|
+
return;
|
|
601
|
+
const sessionMeta = ctx.turn.metadata.sessionMeta;
|
|
602
|
+
const record = {
|
|
603
|
+
role: 'tool',
|
|
604
|
+
content: toolResult.content,
|
|
605
|
+
toolCallId: toolResult.toolCallId,
|
|
606
|
+
name: toolCall.name,
|
|
607
|
+
ts: messageTs,
|
|
608
|
+
...(threadTs ? { threadTs } : {}),
|
|
609
|
+
recordedAt: new Date().toISOString(),
|
|
610
|
+
...(sessionMeta ? { meta: sessionMeta } : {}),
|
|
611
|
+
};
|
|
612
|
+
persistRecords(key, [record]);
|
|
613
|
+
});
|
|
614
|
+
// ── afterTurn: finalize session turn ──────────────────────────
|
|
615
|
+
agent.hook('afterTurn', 'disk-session', async (ctx) => {
|
|
616
|
+
const identity = identityFromCtx(ctx);
|
|
617
|
+
if (!identity)
|
|
618
|
+
return;
|
|
619
|
+
const { key, messageTs, threadTs } = identity;
|
|
620
|
+
delete ctx.turn.metadata['_diskSessionHistoryLen'];
|
|
621
|
+
// ── Compaction: rotate the session file ──────────────────
|
|
622
|
+
// When metadata.compacting is set, this turn was a compaction
|
|
623
|
+
// request (triggered by /compact). The agent's response is the
|
|
624
|
+
// summary. Instead of appending to the old session, we:
|
|
625
|
+
// 1. Rename the old JSONL file with a timestamp suffix
|
|
626
|
+
// 2. Seed a new session with the summary as the first record
|
|
627
|
+
// The compaction request/response are already in the old file
|
|
628
|
+
// (written by beforeTurn + the normal append below would have
|
|
629
|
+
// run, but we return early before that).
|
|
630
|
+
const isCompacting = ctx.turn.request.metadata?.compacting === true;
|
|
631
|
+
if (isCompacting) {
|
|
632
|
+
handleCompaction(ctx.turn, key, messageTs);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
// ── Persist any trailing unwritten steering messages ─────
|
|
636
|
+
// (in case a steering message arrived and the turn halted before beforeLLM ran)
|
|
637
|
+
const written = ctx.turn.metadata['_diskSessionWrittenMessages'];
|
|
638
|
+
delete ctx.turn.metadata['_diskSessionWrittenMessages'];
|
|
639
|
+
const sessionMeta = ctx.turn.metadata.sessionMeta;
|
|
640
|
+
const unwrittenSteering = [];
|
|
641
|
+
for (const msg of ctx.turn.messages) {
|
|
642
|
+
if (msg.role === 'user' && (!written || !written.has(msg))) {
|
|
643
|
+
unwrittenSteering.push({
|
|
644
|
+
...toRecord(msg, messageTs, threadTs),
|
|
645
|
+
...(sessionMeta ? { meta: sessionMeta } : {}),
|
|
646
|
+
});
|
|
647
|
+
written?.add(msg);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (unwrittenSteering.length > 0) {
|
|
651
|
+
persistRecords(key, unwrittenSteering);
|
|
652
|
+
}
|
|
653
|
+
// Pull accumulated usage for this turn (set by afterLLM hook).
|
|
654
|
+
const usage = ctx.turn.metadata['_diskSessionUsage'];
|
|
655
|
+
delete ctx.turn.metadata['_diskSessionUsage'];
|
|
656
|
+
// Attach accumulated usage to the last assistant record in memory.
|
|
657
|
+
if (usage) {
|
|
658
|
+
const records = store.get(key) ?? [];
|
|
659
|
+
for (let i = records.length - 1; i >= 0; i--) {
|
|
660
|
+
if (records[i].role === 'assistant') {
|
|
661
|
+
records[i].usage = usage;
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
// Backfill system prompt on the last user record (in-memory only).
|
|
667
|
+
// The system prompt wasn't available at beforeTurn (beforeContext
|
|
668
|
+
// hooks hadn't run yet), so we update it now.
|
|
669
|
+
const finalSystemPrompt = ctx.turn.systemPrompt;
|
|
670
|
+
if (finalSystemPrompt) {
|
|
671
|
+
const records = store.get(key) ?? [];
|
|
672
|
+
for (let i = records.length - 1; i >= 0; i--) {
|
|
673
|
+
if (records[i].role === 'user') {
|
|
674
|
+
records[i].systemPrompt = finalSystemPrompt;
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
// Trim in-memory cache (file keeps full history).
|
|
680
|
+
const records = store.get(key) ?? [];
|
|
681
|
+
if (records.length > maxRecords) {
|
|
682
|
+
records.splice(0, records.length - maxRecords);
|
|
683
|
+
store.set(key, records);
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
// ── onError: context length backoff fallback ───────────────
|
|
687
|
+
// If the model provider rejects a request due to context length,
|
|
688
|
+
// step down to the next fallback limit (e.g. 100 -> 50 -> 25)
|
|
689
|
+
// and compact the session immediately.
|
|
690
|
+
agent.hook('onError', 'disk-session', async (ctx) => {
|
|
691
|
+
const identity = identityFromCtx(ctx);
|
|
692
|
+
if (!identity)
|
|
693
|
+
return;
|
|
694
|
+
const { key } = identity;
|
|
695
|
+
if (isContextLengthError(ctx.error)) {
|
|
696
|
+
const currentLimit = sessionLimits.get(key) ?? maxContextMessages;
|
|
697
|
+
const nextLimit = fallbackThresholds.find((t) => t < currentLimit);
|
|
698
|
+
if (nextLimit && nextLimit > 0) {
|
|
699
|
+
sessionLimits.set(key, nextLimit);
|
|
700
|
+
const records = store.get(key) ?? [];
|
|
701
|
+
const convRecords = records.filter(isConversationMessage);
|
|
702
|
+
if (convRecords.length > nextLimit && activeModel) {
|
|
703
|
+
try {
|
|
704
|
+
const toEvict = convRecords.length - nextLimit;
|
|
705
|
+
let cut = Math.min(toEvict + Math.max(1, Math.floor(nextLimit / 2)), records.length - 1);
|
|
706
|
+
while (cut < records.length && records[cut].role !== 'user' && records[cut].role !== 'system') {
|
|
707
|
+
cut++;
|
|
708
|
+
}
|
|
709
|
+
if (cut > 0 && cut < records.length) {
|
|
710
|
+
const chunk = records.slice(0, cut);
|
|
711
|
+
const keep = records.slice(cut);
|
|
712
|
+
const summary = await generateSessionSummary(activeModel, chunk);
|
|
713
|
+
const summaryRecord = {
|
|
714
|
+
role: 'assistant',
|
|
715
|
+
content: `[Conversation summary — context limit backed off to ${nextLimit} messages ${new Date().toISOString()}]\n\n${summary}`,
|
|
716
|
+
ts: chunk[chunk.length - 1]?.ts || String(Date.now() / 1000),
|
|
717
|
+
recordedAt: new Date().toISOString(),
|
|
718
|
+
};
|
|
719
|
+
const compacted = [summaryRecord, ...keep];
|
|
720
|
+
store.set(key, compacted);
|
|
721
|
+
appendToFile(key, compacted);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
catch { }
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
},
|
|
730
|
+
/** Get current effective message limit for a session (reflects any fallback backoff). */
|
|
731
|
+
getEffectiveLimit(sessionKey = 'default') {
|
|
732
|
+
return sessionLimits.get(sessionKey) ?? maxContextMessages;
|
|
733
|
+
},
|
|
734
|
+
/** Manually set effective message limit for a session. */
|
|
735
|
+
setEffectiveLimit(sessionKey, limit) {
|
|
736
|
+
sessionLimits.set(sessionKey, limit);
|
|
737
|
+
},
|
|
738
|
+
/** Get all records for a session. */
|
|
739
|
+
getRecords(sessionKey = 'default') {
|
|
740
|
+
return store.get(sessionKey) ?? [];
|
|
741
|
+
},
|
|
742
|
+
/**
|
|
743
|
+
* Append a control record (e.g. /halt) to the session log.
|
|
744
|
+
* Control records are persisted to disk but never included in the
|
|
745
|
+
* LLM context — they're for audit/logging only.
|
|
746
|
+
*/
|
|
747
|
+
appendControl(sessionKey, content, ts, threadTs) {
|
|
748
|
+
const record = {
|
|
749
|
+
role: 'control',
|
|
750
|
+
content,
|
|
751
|
+
ts: ts ?? String(Date.now() / 1000),
|
|
752
|
+
...(threadTs ? { threadTs } : {}),
|
|
753
|
+
recordedAt: new Date().toISOString(),
|
|
754
|
+
};
|
|
755
|
+
persistRecords(sessionKey, [record]);
|
|
756
|
+
},
|
|
757
|
+
/**
|
|
758
|
+
* Append a background message to the session log. Stored as a
|
|
759
|
+
* `system` record so the agent sees it as context ("here's what
|
|
760
|
+
* the channel discussed") rather than a message to respond to.
|
|
761
|
+
*
|
|
762
|
+
* Unlike `appendControl`, system records ARE included in the LLM
|
|
763
|
+
* context window — they appear in the conversation history when
|
|
764
|
+
* the agent is triggered, giving it awareness of recent channel
|
|
765
|
+
* activity without needing to be mentioned on every message.
|
|
766
|
+
*
|
|
767
|
+
* The host should include sender info in the content (e.g.
|
|
768
|
+
* `[U123]: hey anyone seen the invoice?`) so the agent can
|
|
769
|
+
* distinguish who said what. Additional metadata (files, blocks,
|
|
770
|
+
* etc.) goes in `opts.meta` and is persisted to the JSONL for
|
|
771
|
+
* audit/debugging but not shown to the LLM.
|
|
772
|
+
*/
|
|
773
|
+
appendMessage(sessionKey, content, opts) {
|
|
774
|
+
const record = {
|
|
775
|
+
role: 'system',
|
|
776
|
+
content,
|
|
777
|
+
ts: opts?.ts ?? String(Date.now() / 1000),
|
|
778
|
+
...(opts?.threadTs ? { threadTs: opts.threadTs } : {}),
|
|
779
|
+
recordedAt: new Date().toISOString(),
|
|
780
|
+
...(opts?.meta ? { meta: opts.meta } : {}),
|
|
781
|
+
};
|
|
782
|
+
persistRecords(sessionKey, [record]);
|
|
783
|
+
},
|
|
784
|
+
/** List all session keys. */
|
|
785
|
+
getSessions() {
|
|
786
|
+
return Array.from(store.keys());
|
|
787
|
+
},
|
|
788
|
+
/** Clear a single session (memory + disk). */
|
|
789
|
+
clear(sessionKey = 'default') {
|
|
790
|
+
store.delete(sessionKey);
|
|
791
|
+
const path = filePath(sessionKey);
|
|
792
|
+
if (existsSync(path)) {
|
|
793
|
+
try {
|
|
794
|
+
writeFileSync(path, '');
|
|
795
|
+
}
|
|
796
|
+
catch {
|
|
797
|
+
// ignore
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
},
|
|
801
|
+
/** Clear all sessions. */
|
|
802
|
+
clearAll() {
|
|
803
|
+
for (const key of store.keys()) {
|
|
804
|
+
const path = filePath(key);
|
|
805
|
+
if (existsSync(path)) {
|
|
806
|
+
try {
|
|
807
|
+
writeFileSync(path, '');
|
|
808
|
+
}
|
|
809
|
+
catch {
|
|
810
|
+
// ignore
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
store.clear();
|
|
815
|
+
},
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
//# sourceMappingURL=extension.js.map
|