@xandout/libra-harness 0.1.8
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/LICENSE +21 -0
- package/README.md +486 -0
- package/dist/agent.d.ts +174 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +575 -0
- package/dist/agent.js.map +1 -0
- package/dist/ai-sdk-model.d.ts +26 -0
- package/dist/ai-sdk-model.d.ts.map +1 -0
- package/dist/ai-sdk-model.js +260 -0
- package/dist/ai-sdk-model.js.map +1 -0
- package/dist/context.d.ts +75 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +2 -0
- package/dist/context.js.map +1 -0
- package/dist/errors.d.ts +33 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +59 -0
- package/dist/errors.js.map +1 -0
- package/dist/extension.d.ts +63 -0
- package/dist/extension.d.ts.map +1 -0
- package/dist/extension.js +2 -0
- package/dist/extension.js.map +1 -0
- package/dist/extras/extension-loader.d.ts +205 -0
- package/dist/extras/extension-loader.d.ts.map +1 -0
- package/dist/extras/extension-loader.js +315 -0
- package/dist/extras/extension-loader.js.map +1 -0
- package/dist/extras/extensions/auto-steer/extension.d.ts +46 -0
- package/dist/extras/extensions/auto-steer/extension.d.ts.map +1 -0
- package/dist/extras/extensions/auto-steer/extension.js +52 -0
- package/dist/extras/extensions/auto-steer/extension.js.map +1 -0
- package/dist/extras/extensions/auto-steer/extension.json +7 -0
- package/dist/extras/extensions/auto-steer/index.d.ts +3 -0
- package/dist/extras/extensions/auto-steer/index.d.ts.map +1 -0
- package/dist/extras/extensions/auto-steer/index.js +2 -0
- package/dist/extras/extensions/auto-steer/index.js.map +1 -0
- package/dist/extras/extensions/disk-session/extension.d.ts +167 -0
- package/dist/extras/extensions/disk-session/extension.d.ts.map +1 -0
- package/dist/extras/extensions/disk-session/extension.js +492 -0
- package/dist/extras/extensions/disk-session/extension.js.map +1 -0
- package/dist/extras/extensions/disk-session/extension.json +6 -0
- package/dist/extras/extensions/disk-session/extension.test.d.ts +2 -0
- package/dist/extras/extensions/disk-session/extension.test.d.ts.map +1 -0
- package/dist/extras/extensions/disk-session/extension.test.js +672 -0
- package/dist/extras/extensions/disk-session/extension.test.js.map +1 -0
- package/dist/extras/extensions/disk-session/index.d.ts +3 -0
- package/dist/extras/extensions/disk-session/index.d.ts.map +1 -0
- package/dist/extras/extensions/disk-session/index.js +2 -0
- package/dist/extras/extensions/disk-session/index.js.map +1 -0
- package/dist/extras/extensions/emoji/extension.d.ts +20 -0
- package/dist/extras/extensions/emoji/extension.d.ts.map +1 -0
- package/dist/extras/extensions/emoji/extension.js +22 -0
- package/dist/extras/extensions/emoji/extension.js.map +1 -0
- package/dist/extras/extensions/emoji/extension.json +7 -0
- package/dist/extras/extensions/emoji/index.d.ts +3 -0
- package/dist/extras/extensions/emoji/index.d.ts.map +1 -0
- package/dist/extras/extensions/emoji/index.js +2 -0
- package/dist/extras/extensions/emoji/index.js.map +1 -0
- package/dist/extras/extensions/filesystem/extension.d.ts +53 -0
- package/dist/extras/extensions/filesystem/extension.d.ts.map +1 -0
- package/dist/extras/extensions/filesystem/extension.js +350 -0
- package/dist/extras/extensions/filesystem/extension.js.map +1 -0
- package/dist/extras/extensions/filesystem/extension.json +6 -0
- package/dist/extras/extensions/filesystem/extension.test.d.ts +2 -0
- package/dist/extras/extensions/filesystem/extension.test.d.ts.map +1 -0
- package/dist/extras/extensions/filesystem/extension.test.js +387 -0
- package/dist/extras/extensions/filesystem/extension.test.js.map +1 -0
- package/dist/extras/extensions/filesystem/index.d.ts +2 -0
- package/dist/extras/extensions/filesystem/index.d.ts.map +1 -0
- package/dist/extras/extensions/filesystem/index.js +2 -0
- package/dist/extras/extensions/filesystem/index.js.map +1 -0
- package/dist/extras/extensions/keyword-extractor/extension.d.ts +53 -0
- package/dist/extras/extensions/keyword-extractor/extension.d.ts.map +1 -0
- package/dist/extras/extensions/keyword-extractor/extension.js +61 -0
- package/dist/extras/extensions/keyword-extractor/extension.js.map +1 -0
- package/dist/extras/extensions/keyword-extractor/extension.json +6 -0
- package/dist/extras/extensions/keyword-extractor/extension.test.d.ts +2 -0
- package/dist/extras/extensions/keyword-extractor/extension.test.d.ts.map +1 -0
- package/dist/extras/extensions/keyword-extractor/extension.test.js +105 -0
- package/dist/extras/extensions/keyword-extractor/extension.test.js.map +1 -0
- package/dist/extras/extensions/keyword-extractor/index.d.ts +5 -0
- package/dist/extras/extensions/keyword-extractor/index.d.ts.map +1 -0
- package/dist/extras/extensions/keyword-extractor/index.js +3 -0
- package/dist/extras/extensions/keyword-extractor/index.js.map +1 -0
- package/dist/extras/extensions/keyword-extractor/nlp.d.ts +68 -0
- package/dist/extras/extensions/keyword-extractor/nlp.d.ts.map +1 -0
- package/dist/extras/extensions/keyword-extractor/nlp.js +156 -0
- package/dist/extras/extensions/keyword-extractor/nlp.js.map +1 -0
- package/dist/extras/extensions/keyword-extractor/nlp.test.d.ts +2 -0
- package/dist/extras/extensions/keyword-extractor/nlp.test.d.ts.map +1 -0
- package/dist/extras/extensions/keyword-extractor/nlp.test.js +176 -0
- package/dist/extras/extensions/keyword-extractor/nlp.test.js.map +1 -0
- package/dist/extras/extensions/logger/extension.d.ts +52 -0
- package/dist/extras/extensions/logger/extension.d.ts.map +1 -0
- package/dist/extras/extensions/logger/extension.js +68 -0
- package/dist/extras/extensions/logger/extension.js.map +1 -0
- package/dist/extras/extensions/logger/extension.json +7 -0
- package/dist/extras/extensions/logger/index.d.ts +3 -0
- package/dist/extras/extensions/logger/index.d.ts.map +1 -0
- package/dist/extras/extensions/logger/index.js +2 -0
- package/dist/extras/extensions/logger/index.js.map +1 -0
- package/dist/extras/extensions/mcp/extension.d.ts +63 -0
- package/dist/extras/extensions/mcp/extension.d.ts.map +1 -0
- package/dist/extras/extensions/mcp/extension.js +437 -0
- package/dist/extras/extensions/mcp/extension.js.map +1 -0
- package/dist/extras/extensions/mcp/extension.json +7 -0
- package/dist/extras/extensions/mcp/extension.test.d.ts +2 -0
- package/dist/extras/extensions/mcp/extension.test.d.ts.map +1 -0
- package/dist/extras/extensions/mcp/extension.test.js +286 -0
- package/dist/extras/extensions/mcp/extension.test.js.map +1 -0
- package/dist/extras/extensions/mcp/index.d.ts +3 -0
- package/dist/extras/extensions/mcp/index.d.ts.map +1 -0
- package/dist/extras/extensions/mcp/index.js +2 -0
- package/dist/extras/extensions/mcp/index.js.map +1 -0
- package/dist/extras/extensions/mem-session/extension.d.ts +24 -0
- package/dist/extras/extensions/mem-session/extension.d.ts.map +1 -0
- package/dist/extras/extensions/mem-session/extension.js +49 -0
- package/dist/extras/extensions/mem-session/extension.js.map +1 -0
- package/dist/extras/extensions/mem-session/extension.json +6 -0
- package/dist/extras/extensions/mem-session/index.d.ts +2 -0
- package/dist/extras/extensions/mem-session/index.d.ts.map +1 -0
- package/dist/extras/extensions/mem-session/index.js +2 -0
- package/dist/extras/extensions/mem-session/index.js.map +1 -0
- package/dist/extras/extensions/memory/extension.d.ts +54 -0
- package/dist/extras/extensions/memory/extension.d.ts.map +1 -0
- package/dist/extras/extensions/memory/extension.js +143 -0
- package/dist/extras/extensions/memory/extension.js.map +1 -0
- package/dist/extras/extensions/memory/extension.json +7 -0
- package/dist/extras/extensions/memory/index.d.ts +5 -0
- package/dist/extras/extensions/memory/index.d.ts.map +1 -0
- package/dist/extras/extensions/memory/index.js +3 -0
- package/dist/extras/extensions/memory/index.js.map +1 -0
- package/dist/extras/extensions/memory/llm-extractor.d.ts +42 -0
- package/dist/extras/extensions/memory/llm-extractor.d.ts.map +1 -0
- package/dist/extras/extensions/memory/llm-extractor.js +89 -0
- package/dist/extras/extensions/memory/llm-extractor.js.map +1 -0
- package/dist/extras/extensions/memory/types.d.ts +142 -0
- package/dist/extras/extensions/memory/types.d.ts.map +1 -0
- package/dist/extras/extensions/memory/types.js +2 -0
- package/dist/extras/extensions/memory/types.js.map +1 -0
- package/dist/extras/extensions/otel/extension.d.ts +57 -0
- package/dist/extras/extensions/otel/extension.d.ts.map +1 -0
- package/dist/extras/extensions/otel/extension.js +253 -0
- package/dist/extras/extensions/otel/extension.js.map +1 -0
- package/dist/extras/extensions/otel/extension.json +7 -0
- package/dist/extras/extensions/otel/extension.test.d.ts +2 -0
- package/dist/extras/extensions/otel/extension.test.d.ts.map +1 -0
- package/dist/extras/extensions/otel/extension.test.js +399 -0
- package/dist/extras/extensions/otel/extension.test.js.map +1 -0
- package/dist/extras/extensions/otel/index.d.ts +4 -0
- package/dist/extras/extensions/otel/index.d.ts.map +1 -0
- package/dist/extras/extensions/otel/index.js +3 -0
- package/dist/extras/extensions/otel/index.js.map +1 -0
- package/dist/extras/extensions/otel/jsonl-span-exporter.d.ts +60 -0
- package/dist/extras/extensions/otel/jsonl-span-exporter.d.ts.map +1 -0
- package/dist/extras/extensions/otel/jsonl-span-exporter.js +134 -0
- package/dist/extras/extensions/otel/jsonl-span-exporter.js.map +1 -0
- package/dist/extras/extensions/scripts/extension.d.ts +180 -0
- package/dist/extras/extensions/scripts/extension.d.ts.map +1 -0
- package/dist/extras/extensions/scripts/extension.js +618 -0
- package/dist/extras/extensions/scripts/extension.js.map +1 -0
- package/dist/extras/extensions/scripts/extension.json +7 -0
- package/dist/extras/extensions/scripts/extension.test.d.ts +2 -0
- package/dist/extras/extensions/scripts/extension.test.d.ts.map +1 -0
- package/dist/extras/extensions/scripts/extension.test.js +552 -0
- package/dist/extras/extensions/scripts/extension.test.js.map +1 -0
- package/dist/extras/extensions/scripts/index.d.ts +3 -0
- package/dist/extras/extensions/scripts/index.d.ts.map +1 -0
- package/dist/extras/extensions/scripts/index.js +2 -0
- package/dist/extras/extensions/scripts/index.js.map +1 -0
- package/dist/extras/extensions/skills/extension.d.ts +105 -0
- package/dist/extras/extensions/skills/extension.d.ts.map +1 -0
- package/dist/extras/extensions/skills/extension.js +419 -0
- package/dist/extras/extensions/skills/extension.js.map +1 -0
- package/dist/extras/extensions/skills/extension.json +7 -0
- package/dist/extras/extensions/skills/extension.test.d.ts +2 -0
- package/dist/extras/extensions/skills/extension.test.d.ts.map +1 -0
- package/dist/extras/extensions/skills/extension.test.js +184 -0
- package/dist/extras/extensions/skills/extension.test.js.map +1 -0
- package/dist/extras/extensions/skills/index.d.ts +4 -0
- package/dist/extras/extensions/skills/index.d.ts.map +1 -0
- package/dist/extras/extensions/skills/index.js +3 -0
- package/dist/extras/extensions/skills/index.js.map +1 -0
- package/dist/extras/extensions/streaming/extension.d.ts +15 -0
- package/dist/extras/extensions/streaming/extension.d.ts.map +1 -0
- package/dist/extras/extensions/streaming/extension.js +41 -0
- package/dist/extras/extensions/streaming/extension.js.map +1 -0
- package/dist/extras/extensions/streaming/extension.json +6 -0
- package/dist/extras/extensions/streaming/index.d.ts +2 -0
- package/dist/extras/extensions/streaming/index.d.ts.map +1 -0
- package/dist/extras/extensions/streaming/index.js +2 -0
- package/dist/extras/extensions/streaming/index.js.map +1 -0
- package/dist/extras/extensions/structured-output/extension.d.ts +11 -0
- package/dist/extras/extensions/structured-output/extension.d.ts.map +1 -0
- package/dist/extras/extensions/structured-output/extension.js +102 -0
- package/dist/extras/extensions/structured-output/extension.js.map +1 -0
- package/dist/extras/extensions/structured-output/extension.json +7 -0
- package/dist/extras/extensions/structured-output/index.d.ts +3 -0
- package/dist/extras/extensions/structured-output/index.d.ts.map +1 -0
- package/dist/extras/extensions/structured-output/index.js +2 -0
- package/dist/extras/extensions/structured-output/index.js.map +1 -0
- package/dist/extras/extensions/timestamp/extension.d.ts +11 -0
- package/dist/extras/extensions/timestamp/extension.d.ts.map +1 -0
- package/dist/extras/extensions/timestamp/extension.js +21 -0
- package/dist/extras/extensions/timestamp/extension.js.map +1 -0
- package/dist/extras/extensions/timestamp/extension.json +6 -0
- package/dist/extras/extensions/timestamp/index.d.ts +2 -0
- package/dist/extras/extensions/timestamp/index.d.ts.map +1 -0
- package/dist/extras/extensions/timestamp/index.js +2 -0
- package/dist/extras/extensions/timestamp/index.js.map +1 -0
- package/dist/extras/extensions/token-stats/extension.d.ts +50 -0
- package/dist/extras/extensions/token-stats/extension.d.ts.map +1 -0
- package/dist/extras/extensions/token-stats/extension.js +99 -0
- package/dist/extras/extensions/token-stats/extension.js.map +1 -0
- package/dist/extras/extensions/token-stats/extension.json +7 -0
- package/dist/extras/extensions/token-stats/extension.test.d.ts +2 -0
- package/dist/extras/extensions/token-stats/extension.test.d.ts.map +1 -0
- package/dist/extras/extensions/token-stats/extension.test.js +197 -0
- package/dist/extras/extensions/token-stats/extension.test.js.map +1 -0
- package/dist/extras/extensions/token-stats/index.d.ts +3 -0
- package/dist/extras/extensions/token-stats/index.d.ts.map +1 -0
- package/dist/extras/extensions/token-stats/index.js +2 -0
- package/dist/extras/extensions/token-stats/index.js.map +1 -0
- package/dist/extras/extensions/tool-buffer/extension.d.ts +89 -0
- package/dist/extras/extensions/tool-buffer/extension.d.ts.map +1 -0
- package/dist/extras/extensions/tool-buffer/extension.js +182 -0
- package/dist/extras/extensions/tool-buffer/extension.js.map +1 -0
- package/dist/extras/extensions/tool-buffer/extension.json +7 -0
- package/dist/extras/extensions/tool-buffer/extension.test.d.ts +2 -0
- package/dist/extras/extensions/tool-buffer/extension.test.d.ts.map +1 -0
- package/dist/extras/extensions/tool-buffer/extension.test.js +549 -0
- package/dist/extras/extensions/tool-buffer/extension.test.js.map +1 -0
- package/dist/extras/extensions/tool-buffer/index.d.ts +3 -0
- package/dist/extras/extensions/tool-buffer/index.d.ts.map +1 -0
- package/dist/extras/extensions/tool-buffer/index.js +2 -0
- package/dist/extras/extensions/tool-buffer/index.js.map +1 -0
- package/dist/extras/extensions/weather-tool/extension.d.ts +22 -0
- package/dist/extras/extensions/weather-tool/extension.d.ts.map +1 -0
- package/dist/extras/extensions/weather-tool/extension.js +33 -0
- package/dist/extras/extensions/weather-tool/extension.js.map +1 -0
- package/dist/extras/extensions/weather-tool/extension.json +7 -0
- package/dist/extras/extensions/weather-tool/index.d.ts +3 -0
- package/dist/extras/extensions/weather-tool/index.d.ts.map +1 -0
- package/dist/extras/extensions/weather-tool/index.js +2 -0
- package/dist/extras/extensions/weather-tool/index.js.map +1 -0
- package/dist/extras/models/ai-sdk-resolver.d.ts +15 -0
- package/dist/extras/models/ai-sdk-resolver.d.ts.map +1 -0
- package/dist/extras/models/ai-sdk-resolver.js +57 -0
- package/dist/extras/models/ai-sdk-resolver.js.map +1 -0
- package/dist/extras/models/index.d.ts +5 -0
- package/dist/extras/models/index.d.ts.map +1 -0
- package/dist/extras/models/index.js +3 -0
- package/dist/extras/models/index.js.map +1 -0
- package/dist/extras/models/routing-model.d.ts +18 -0
- package/dist/extras/models/routing-model.d.ts.map +1 -0
- package/dist/extras/models/routing-model.js +24 -0
- package/dist/extras/models/routing-model.js.map +1 -0
- package/dist/extras/openai-provider/index.d.ts +3 -0
- package/dist/extras/openai-provider/index.d.ts.map +1 -0
- package/dist/extras/openai-provider/index.js +2 -0
- package/dist/extras/openai-provider/index.js.map +1 -0
- package/dist/extras/openai-provider/server.d.ts +34 -0
- package/dist/extras/openai-provider/server.d.ts.map +1 -0
- package/dist/extras/openai-provider/server.js +335 -0
- package/dist/extras/openai-provider/server.js.map +1 -0
- package/dist/handle.d.ts +56 -0
- package/dist/handle.d.ts.map +1 -0
- package/dist/handle.js +2 -0
- package/dist/handle.js.map +1 -0
- package/dist/hooks.d.ts +98 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +53 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/model.d.ts +75 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +2 -0
- package/dist/model.js.map +1 -0
- package/dist/tool.d.ts +78 -0
- package/dist/tool.d.ts.map +1 -0
- package/dist/tool.js +78 -0
- package/dist/tool.js.map +1 -0
- package/dist/types.d.ts +83 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +21 -0
- package/dist/types.js.map +1 -0
- package/package.json +178 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { Extension } from '@xandout/libra-harness';
|
|
2
|
+
import type { MessageContent, ToolCall } from '@xandout/libra-harness';
|
|
3
|
+
/**
|
|
4
|
+
* A single record in a session's JSONL log.
|
|
5
|
+
*
|
|
6
|
+
* Maps directly to a Message plus correlation metadata. The system
|
|
7
|
+
* prompt that was active for a turn is captured on the user record's
|
|
8
|
+
* `systemPrompt` field. Background channel messages (observed but not
|
|
9
|
+
* directed at the agent) are stored as `system` records so the agent
|
|
10
|
+
* sees them as context, not as messages to respond to.
|
|
11
|
+
*/
|
|
12
|
+
export interface SessionRecord {
|
|
13
|
+
role: 'user' | 'assistant' | 'tool' | 'control' | 'system';
|
|
14
|
+
content: MessageContent;
|
|
15
|
+
toolCalls?: ToolCall[];
|
|
16
|
+
toolCallId?: string;
|
|
17
|
+
name?: string;
|
|
18
|
+
/** Message correlation id (e.g. a Slack message timestamp, a Discord message id). */
|
|
19
|
+
ts: string;
|
|
20
|
+
/** Thread parent correlation id — undefined for top-level messages. */
|
|
21
|
+
threadTs?: string;
|
|
22
|
+
/** ISO timestamp for internal tracking. */
|
|
23
|
+
recordedAt: string;
|
|
24
|
+
/**
|
|
25
|
+
* The system prompt that was active for this turn. Persisted on the
|
|
26
|
+
* user record (the first record of each turn) so every turn's JSONL
|
|
27
|
+
* entry shows exactly what instructions the model was operating under.
|
|
28
|
+
* Absent on assistant, tool, and control records. Lets you audit
|
|
29
|
+
* "why did the model respond this way?" without replaying config +
|
|
30
|
+
* extension state.
|
|
31
|
+
*/
|
|
32
|
+
systemPrompt?: string;
|
|
33
|
+
/** Token usage from the LLM provider (accumulated across iterations). */
|
|
34
|
+
usage?: {
|
|
35
|
+
promptTokens: number;
|
|
36
|
+
completionTokens: number;
|
|
37
|
+
iterations: number;
|
|
38
|
+
/** Prompt tokens served from provider cache (accumulated). */
|
|
39
|
+
cachedPromptTokens?: number;
|
|
40
|
+
/** Reasoning/thinking tokens (accumulated). */
|
|
41
|
+
reasoningTokens?: number;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Generic enrichment bag. Populated opaquely from
|
|
45
|
+
* `ctx.turn.metadata.sessionMeta` — any extension may write into
|
|
46
|
+
* that metadata key during `beforeTurn`/`afterLLM` and disk-session
|
|
47
|
+
* persists it here without inspecting the contents. Keys are owned
|
|
48
|
+
* by the extension that writes them (e.g. `keywords`, `sentiment`).
|
|
49
|
+
*/
|
|
50
|
+
meta?: Record<string, unknown>;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Identifies which session a turn belongs to.
|
|
54
|
+
*
|
|
55
|
+
* The host (or a resolver) produces this from turn metadata — the
|
|
56
|
+
* disk-session extension never reads host-specific types directly.
|
|
57
|
+
*/
|
|
58
|
+
export interface SessionIdentity {
|
|
59
|
+
/** Stable session key — used as the JSONL filename and in-memory cache key. e.g. 'slack_C1', 'dm_U1', 'issue_42'.
|
|
60
|
+
* Should be filesystem-safe (alphanumeric, underscore, hyphen). Characters outside [a-zA-Z0-9_-] are replaced with '_' for the filename, so keys with colons or dots won't round-trip identically through a restart. */
|
|
61
|
+
key: string;
|
|
62
|
+
/** Correlation id for the incoming message (persisted as `record.ts`). */
|
|
63
|
+
messageTs: string;
|
|
64
|
+
/** Parent correlation id — when set, this message is a reply in a thread/sub-conversation. Persisted as `record.threadTs` and used as the context fork point. */
|
|
65
|
+
threadTs?: string;
|
|
66
|
+
/** True for 1:1 conversations (DMs) — disables thread forking, uses simple last-N history. */
|
|
67
|
+
isDirect?: boolean;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Extracts a {@link SessionIdentity} from turn metadata.
|
|
71
|
+
*
|
|
72
|
+
* The host supplies a resolver so disk-session doesn't need to know
|
|
73
|
+
* about Slack channels, Discord channels, or any other host concept.
|
|
74
|
+
* Return `undefined` to skip session handling for a turn.
|
|
75
|
+
*/
|
|
76
|
+
export interface SessionResolver {
|
|
77
|
+
resolve(metadata: Record<string, unknown>): SessionIdentity | undefined;
|
|
78
|
+
}
|
|
79
|
+
export interface DiskSessionConfig {
|
|
80
|
+
/** Directory for session JSONL files. Default: ./sessions */
|
|
81
|
+
sessionDir?: string;
|
|
82
|
+
/**
|
|
83
|
+
* Max records to keep in memory per session. Older records are
|
|
84
|
+
* evicted from the cache but remain in the JSONL file.
|
|
85
|
+
* Default: 1000.
|
|
86
|
+
*/
|
|
87
|
+
maxRecords?: number;
|
|
88
|
+
/**
|
|
89
|
+
* Max messages to include in the agent's context per turn.
|
|
90
|
+
* Default: 50.
|
|
91
|
+
*/
|
|
92
|
+
maxContextMessages?: number;
|
|
93
|
+
/**
|
|
94
|
+
* Number of top-level (non-thread) messages to include as channel
|
|
95
|
+
* context when forking a thread. These are messages that came BEFORE
|
|
96
|
+
* the thread parent — what the channel was discussing when the thread
|
|
97
|
+
* started. Default: 10.
|
|
98
|
+
*/
|
|
99
|
+
channelContextMessages?: number;
|
|
100
|
+
/**
|
|
101
|
+
* Number of recent top-level messages to include after the last thread
|
|
102
|
+
* reply. This gives the agent awareness of what's happened in the
|
|
103
|
+
* channel since the thread was last active (e.g. if someone comes back
|
|
104
|
+
* to a thread a week later, the agent sees recent channel activity).
|
|
105
|
+
* Default: 5.
|
|
106
|
+
*/
|
|
107
|
+
recentChannelMessages?: number;
|
|
108
|
+
/** Load existing JSONL files into memory on startup. Default: true. */
|
|
109
|
+
loadOnStartup?: boolean;
|
|
110
|
+
/**
|
|
111
|
+
* Resolve a {@link SessionIdentity} from turn metadata. Default:
|
|
112
|
+
* reads `metadata.session` as a `SessionIdentity`, falls back to
|
|
113
|
+
* `metadata.sessionId` as a plain key, then to `'default'`.
|
|
114
|
+
*/
|
|
115
|
+
resolver?: SessionResolver;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Disk-backed session extension.
|
|
119
|
+
*
|
|
120
|
+
* ## Architecture
|
|
121
|
+
*
|
|
122
|
+
* **One JSONL file per session** (`<sessionKey>.jsonl`). Append-only —
|
|
123
|
+
* every message the agent sees (human or bot, top-level or threaded) is
|
|
124
|
+
* appended. This log is the source of truth.
|
|
125
|
+
*
|
|
126
|
+
* **Snapshot at beforeTurn**: when a turn starts, the extension takes a
|
|
127
|
+
* read-only snapshot of the session log at that moment. The turn runs
|
|
128
|
+
* against the snapshot. Concurrent turns each take their own snapshot
|
|
129
|
+
* and don't see each other's in-progress work.
|
|
130
|
+
*
|
|
131
|
+
* **Fork for threads**: when a thread reply comes in (identity has
|
|
132
|
+
* `threadTs`), the snapshot is filtered to include only:
|
|
133
|
+
* 1. Top-level messages before the thread parent (channel context)
|
|
134
|
+
* 2. All messages in that thread (parent + replies)
|
|
135
|
+
* 3. Recent top-level messages after the last thread reply
|
|
136
|
+
*
|
|
137
|
+
* **Append after turn**: when the turn finishes, new messages (user +
|
|
138
|
+
* assistant + tool calls) are appended to the session log. No data is
|
|
139
|
+
* ever rewritten or reordered.
|
|
140
|
+
*
|
|
141
|
+
* **Stable prefix for LLM caching**: the messages array is built as
|
|
142
|
+
* [system prompt] + [stable session history] + [new user message]
|
|
143
|
+
* The session history grows monotonically (append-only), so earlier
|
|
144
|
+
* tokens stay cached upstream. Per-turn metadata is injected at the end
|
|
145
|
+
* of the context (by a host-side `beforeContext` hook), not the
|
|
146
|
+
* beginning, to preserve the cache prefix.
|
|
147
|
+
*
|
|
148
|
+
* ## Host integration
|
|
149
|
+
*
|
|
150
|
+
* The host supplies a {@link SessionResolver} (or writes
|
|
151
|
+
* `metadata.session`) so the extension knows which session a turn
|
|
152
|
+
* belongs to. The extension itself is host-agnostic — it doesn't know
|
|
153
|
+
* about Slack, Discord, or any other platform.
|
|
154
|
+
*/
|
|
155
|
+
export default function createDiskSessionExtension(config?: DiskSessionConfig): Extension & {
|
|
156
|
+
getRecords(sessionKey?: string): SessionRecord[];
|
|
157
|
+
getSessions(): string[];
|
|
158
|
+
clear(sessionKey?: string): void;
|
|
159
|
+
clearAll(): void;
|
|
160
|
+
appendControl(sessionKey: string, content: string, ts?: string, threadTs?: string): void;
|
|
161
|
+
appendMessage(sessionKey: string, content: string, opts?: {
|
|
162
|
+
ts?: string;
|
|
163
|
+
threadTs?: string;
|
|
164
|
+
meta?: Record<string, unknown>;
|
|
165
|
+
}): void;
|
|
166
|
+
};
|
|
167
|
+
//# sourceMappingURL=extension.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extension.d.ts","sourceRoot":"","sources":["../../../../src/extras/extensions/disk-session/extension.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAW,cAAc,EAAQ,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAEtF;;;;;;;;GAQG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC3D,OAAO,EAAE,cAAc,CAAC;IACxB,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qFAAqF;IACrF,EAAE,EAAE,MAAM,CAAC;IACX,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,KAAK,CAAC,EAAE;QACN,YAAY,EAAE,MAAM,CAAC;QACrB,gBAAgB,EAAE,MAAM,CAAC;QACzB,UAAU,EAAE,MAAM,CAAC;QACnB,8DAA8D;QAC9D,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,+CAA+C;QAC/C,eAAe,CAAC,EAAE,MAAM,CAAC;KAC1B,CAAC;IACF;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B;4NACwN;IACxN,GAAG,EAAE,MAAM,CAAC;IACZ,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAC;IAClB,iKAAiK;IACjK,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8FAA8F;IAC9F,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,eAAe,GAAG,SAAS,CAAC;CACzE;AAED,MAAM,WAAW,iBAAiB;IAChC,6DAA6D;IAC7D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC;;;;;;OAMG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,uEAAuE;IACvE,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,eAAe,CAAC;CAC5B;AAgBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,MAAM,CAAC,OAAO,UAAU,0BAA0B,CAChD,MAAM,CAAC,EAAE,iBAAiB,GACzB,SAAS,GAAG;IACb,UAAU,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,aAAa,EAAE,CAAC;IACjD,WAAW,IAAI,MAAM,EAAE,CAAC;IACxB,KAAK,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,IAAI,IAAI,CAAC;IACjB,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzF,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QACxD,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KAChC,GAAG,IAAI,CAAC;CACV,CAgfA"}
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import { readFileSync, appendFileSync, mkdirSync, readdirSync, writeFileSync, existsSync, renameSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Default resolver: reads `metadata.session` as a `SessionIdentity`,
|
|
5
|
+
* falls back to `metadata.sessionId` as a plain key, then to `'default'`.
|
|
6
|
+
*/
|
|
7
|
+
const defaultResolver = {
|
|
8
|
+
resolve(metadata) {
|
|
9
|
+
const session = metadata.session;
|
|
10
|
+
if (session && typeof session.key === 'string')
|
|
11
|
+
return session;
|
|
12
|
+
const sessionId = metadata.sessionId;
|
|
13
|
+
if (sessionId)
|
|
14
|
+
return { key: sessionId, messageTs: '' };
|
|
15
|
+
return { key: 'default', messageTs: '' };
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Disk-backed session extension.
|
|
20
|
+
*
|
|
21
|
+
* ## Architecture
|
|
22
|
+
*
|
|
23
|
+
* **One JSONL file per session** (`<sessionKey>.jsonl`). Append-only —
|
|
24
|
+
* every message the agent sees (human or bot, top-level or threaded) is
|
|
25
|
+
* appended. This log is the source of truth.
|
|
26
|
+
*
|
|
27
|
+
* **Snapshot at beforeTurn**: when a turn starts, the extension takes a
|
|
28
|
+
* read-only snapshot of the session log at that moment. The turn runs
|
|
29
|
+
* against the snapshot. Concurrent turns each take their own snapshot
|
|
30
|
+
* and don't see each other's in-progress work.
|
|
31
|
+
*
|
|
32
|
+
* **Fork for threads**: when a thread reply comes in (identity has
|
|
33
|
+
* `threadTs`), the snapshot is filtered to include only:
|
|
34
|
+
* 1. Top-level messages before the thread parent (channel context)
|
|
35
|
+
* 2. All messages in that thread (parent + replies)
|
|
36
|
+
* 3. Recent top-level messages after the last thread reply
|
|
37
|
+
*
|
|
38
|
+
* **Append after turn**: when the turn finishes, new messages (user +
|
|
39
|
+
* assistant + tool calls) are appended to the session log. No data is
|
|
40
|
+
* ever rewritten or reordered.
|
|
41
|
+
*
|
|
42
|
+
* **Stable prefix for LLM caching**: the messages array is built as
|
|
43
|
+
* [system prompt] + [stable session history] + [new user message]
|
|
44
|
+
* The session history grows monotonically (append-only), so earlier
|
|
45
|
+
* tokens stay cached upstream. Per-turn metadata is injected at the end
|
|
46
|
+
* of the context (by a host-side `beforeContext` hook), not the
|
|
47
|
+
* beginning, to preserve the cache prefix.
|
|
48
|
+
*
|
|
49
|
+
* ## Host integration
|
|
50
|
+
*
|
|
51
|
+
* The host supplies a {@link SessionResolver} (or writes
|
|
52
|
+
* `metadata.session`) so the extension knows which session a turn
|
|
53
|
+
* belongs to. The extension itself is host-agnostic — it doesn't know
|
|
54
|
+
* about Slack, Discord, or any other platform.
|
|
55
|
+
*/
|
|
56
|
+
export default function createDiskSessionExtension(config) {
|
|
57
|
+
const dir = config?.sessionDir ?? './sessions';
|
|
58
|
+
const maxRecords = config?.maxRecords ?? 1000;
|
|
59
|
+
const maxContextMessages = config?.maxContextMessages ?? 50;
|
|
60
|
+
const channelContextMessages = config?.channelContextMessages ?? 10;
|
|
61
|
+
const recentChannelMessages = config?.recentChannelMessages ?? 5;
|
|
62
|
+
const loadOnStartup = config?.loadOnStartup ?? true;
|
|
63
|
+
const resolver = config?.resolver ?? defaultResolver;
|
|
64
|
+
mkdirSync(dir, { recursive: true });
|
|
65
|
+
// In-memory cache: sessionKey → SessionRecord[]
|
|
66
|
+
// This is the live log. Reads take a snapshot (copy); writes append.
|
|
67
|
+
const store = new Map();
|
|
68
|
+
// ── Load existing sessions from disk ──────────────────────────
|
|
69
|
+
if (loadOnStartup) {
|
|
70
|
+
try {
|
|
71
|
+
const files = readdirSync(dir).filter((f) => f.endsWith('.jsonl'));
|
|
72
|
+
let totalRecords = 0;
|
|
73
|
+
for (const file of files) {
|
|
74
|
+
try {
|
|
75
|
+
const raw = readFileSync(join(dir, file), 'utf-8');
|
|
76
|
+
const lines = raw.split('\n').filter((l) => l.trim());
|
|
77
|
+
const records = lines.map((l) => JSON.parse(l));
|
|
78
|
+
const sessionKey = file.replace(/\.jsonl$/, '');
|
|
79
|
+
store.set(sessionKey, records.slice(-maxRecords));
|
|
80
|
+
totalRecords += records.length;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Skip corrupt files.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (store.size > 0) {
|
|
87
|
+
console.log(`[disk-session] loaded ${store.size} session(s), ${totalRecords} record(s) from ${dir}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Directory doesn't exist — start empty.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function filePath(sessionKey) {
|
|
95
|
+
const safe = sessionKey.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
96
|
+
return join(dir, `${safe}.jsonl`);
|
|
97
|
+
}
|
|
98
|
+
function appendToFile(sessionKey, records) {
|
|
99
|
+
if (records.length === 0)
|
|
100
|
+
return;
|
|
101
|
+
const lines = records.map((r) => JSON.stringify(r)).join('\n') + '\n';
|
|
102
|
+
try {
|
|
103
|
+
appendFileSync(filePath(sessionKey), lines);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
console.error(`[disk-session] failed to append to ${sessionKey}:`, err);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// ── Convert between Message and SessionRecord ──────────────────
|
|
110
|
+
function toMessage(r) {
|
|
111
|
+
return {
|
|
112
|
+
// `toMessage` is only called on records that passed
|
|
113
|
+
// `isConversationMessage`, which excludes 'control' (and 'tool').
|
|
114
|
+
role: r.role,
|
|
115
|
+
content: r.content,
|
|
116
|
+
...(r.toolCalls ? { toolCalls: r.toolCalls } : {}),
|
|
117
|
+
...(r.toolCallId ? { toolCallId: r.toolCallId } : {}),
|
|
118
|
+
...(r.name ? { name: r.name } : {}),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function toRecord(msg, ts, threadTs) {
|
|
122
|
+
return {
|
|
123
|
+
role: msg.role,
|
|
124
|
+
content: msg.content,
|
|
125
|
+
...(msg.toolCalls ? { toolCalls: msg.toolCalls } : {}),
|
|
126
|
+
...(msg.toolCallId ? { toolCallId: msg.toolCallId } : {}),
|
|
127
|
+
...(msg.name ? { name: msg.name } : {}),
|
|
128
|
+
ts,
|
|
129
|
+
...(threadTs ? { threadTs } : {}),
|
|
130
|
+
recordedAt: new Date().toISOString(),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// Filter for context windows: only keep user messages, system
|
|
134
|
+
// messages (background channel context), and assistant messages with
|
|
135
|
+
// text content (not tool-call intermediates or control records).
|
|
136
|
+
// Tool messages, control records, and assistant messages that only
|
|
137
|
+
// contain toolCalls are turn-internal mechanics — including them
|
|
138
|
+
// without the full tool-call flow produces invalid message sequences
|
|
139
|
+
// for the LLM. Control records (e.g. /halt) are persisted for audit
|
|
140
|
+
// but never shown to the model.
|
|
141
|
+
const isConversationMessage = (r) => {
|
|
142
|
+
if (r.role === 'tool')
|
|
143
|
+
return false;
|
|
144
|
+
if (r.role === 'control')
|
|
145
|
+
return false;
|
|
146
|
+
// Drop any assistant message that has tool_calls — even if it also
|
|
147
|
+
// has text content. Keeping it without the matching tool response
|
|
148
|
+
// messages produces an invalid sequence for the LLM.
|
|
149
|
+
if (r.role === 'assistant' && r.toolCalls?.length)
|
|
150
|
+
return false;
|
|
151
|
+
return true; // user, system, assistant (without tool calls)
|
|
152
|
+
};
|
|
153
|
+
// ── Build context (the "fork") from a snapshot ────────────────
|
|
154
|
+
// The snapshot is a read-only copy of the session records at the
|
|
155
|
+
// moment the turn started. This function builds the messages array
|
|
156
|
+
// that the agent will see.
|
|
157
|
+
//
|
|
158
|
+
// For top-level messages and DMs: last N records (simple slice).
|
|
159
|
+
// For thread replies: channel context before parent + thread history.
|
|
160
|
+
function buildContext(snapshot, threadTs, isDirect) {
|
|
161
|
+
if (snapshot.length === 0)
|
|
162
|
+
return [];
|
|
163
|
+
// DM or top-level message: last N messages, but filter out
|
|
164
|
+
// tool-call intermediates that might get split by the slice.
|
|
165
|
+
if (isDirect || !threadTs) {
|
|
166
|
+
return snapshot
|
|
167
|
+
.filter(isConversationMessage)
|
|
168
|
+
.slice(-maxContextMessages)
|
|
169
|
+
.map(toMessage);
|
|
170
|
+
}
|
|
171
|
+
// Thread: fork from channel context.
|
|
172
|
+
const parentIdx = snapshot.findIndex((r) => r.ts === threadTs);
|
|
173
|
+
if (parentIdx === -1) {
|
|
174
|
+
// Parent not in snapshot (evicted from cache or very old).
|
|
175
|
+
// Fall back to last N messages.
|
|
176
|
+
return snapshot
|
|
177
|
+
.filter(isConversationMessage)
|
|
178
|
+
.slice(-maxContextMessages)
|
|
179
|
+
.map(toMessage);
|
|
180
|
+
}
|
|
181
|
+
// Top-level messages before the parent (channel context at fork point).
|
|
182
|
+
const topLevelBefore = snapshot
|
|
183
|
+
.slice(0, parentIdx)
|
|
184
|
+
.filter((r) => !r.threadTs && isConversationMessage(r))
|
|
185
|
+
.slice(-channelContextMessages);
|
|
186
|
+
// All messages in this thread (including the parent).
|
|
187
|
+
// Filter out tool-call intermediates — they produce invalid message
|
|
188
|
+
// sequences if they get split or truncated by the context window.
|
|
189
|
+
const threadMessages = snapshot
|
|
190
|
+
.filter((r) => (r.ts === threadTs || r.threadTs === threadTs) && isConversationMessage(r));
|
|
191
|
+
// Recent top-level messages after the last thread reply.
|
|
192
|
+
// This gives the agent awareness of what's happened in the channel
|
|
193
|
+
// since the thread was last active — without pulling in the entire
|
|
194
|
+
// channel history. Find the last thread message's index in the
|
|
195
|
+
// snapshot by ts, then take top-level messages after it.
|
|
196
|
+
const lastThreadTs = threadMessages[threadMessages.length - 1]?.ts ?? threadTs;
|
|
197
|
+
let lastThreadIdx = parentIdx;
|
|
198
|
+
for (let i = snapshot.length - 1; i >= 0; i--) {
|
|
199
|
+
if (snapshot[i].ts === lastThreadTs) {
|
|
200
|
+
lastThreadIdx = i;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const recentTopLevel = recentChannelMessages > 0
|
|
205
|
+
? snapshot
|
|
206
|
+
.slice(lastThreadIdx + 1)
|
|
207
|
+
.filter((r) => !r.threadTs && isConversationMessage(r))
|
|
208
|
+
.slice(-recentChannelMessages)
|
|
209
|
+
: [];
|
|
210
|
+
return [...topLevelBefore, ...threadMessages, ...recentTopLevel].map(toMessage);
|
|
211
|
+
}
|
|
212
|
+
// ── Resolve session identity from turn metadata ───────────────
|
|
213
|
+
function identityFromCtx(ctx) {
|
|
214
|
+
return resolver.resolve(ctx.turn.request.metadata ?? {});
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
name: 'disk-session',
|
|
218
|
+
priority: -100,
|
|
219
|
+
install(agent) {
|
|
220
|
+
// ── beforeTurn: take snapshot, build context, prepend ─────
|
|
221
|
+
// The snapshot is a copy of the session records at this moment.
|
|
222
|
+
// Concurrent turns each get their own snapshot — they don't
|
|
223
|
+
// see each other's in-progress work.
|
|
224
|
+
agent.hook('beforeTurn', 'disk-session', async (ctx) => {
|
|
225
|
+
const identity = identityFromCtx(ctx);
|
|
226
|
+
if (!identity)
|
|
227
|
+
return;
|
|
228
|
+
const { key, messageTs, threadTs, isDirect } = identity;
|
|
229
|
+
const isDm = isDirect ?? false;
|
|
230
|
+
// Snapshot: copy the current records (read-only).
|
|
231
|
+
const liveRecords = store.get(key) ?? [];
|
|
232
|
+
const snapshot = [...liveRecords];
|
|
233
|
+
// Build the forked context from the snapshot.
|
|
234
|
+
const history = buildContext(snapshot, threadTs, isDm);
|
|
235
|
+
// ── Persist the user message immediately ────────────────
|
|
236
|
+
// Write the incoming user message to disk NOW, before the
|
|
237
|
+
// agent runs. If the process crashes mid-turn, the user's
|
|
238
|
+
// message is already saved. The assistant response is
|
|
239
|
+
// appended later in afterTurn.
|
|
240
|
+
//
|
|
241
|
+
// The system prompt is captured here (on the user record) so
|
|
242
|
+
// each turn's JSONL entry shows exactly what instructions the
|
|
243
|
+
// model was operating under. It's available from beforeTurn
|
|
244
|
+
// onward via ctx.turn.systemPrompt (resolved by the core from
|
|
245
|
+
// the per-turn override ?? agent config).
|
|
246
|
+
//
|
|
247
|
+
// Enrichment bag: any extension that ran before us in
|
|
248
|
+
// beforeTurn may have written into `sessionMeta`. We persist
|
|
249
|
+
// it opaquely — disk-session does not inspect the contents.
|
|
250
|
+
const sessionMeta = ctx.turn.metadata.sessionMeta;
|
|
251
|
+
const systemPrompt = ctx.turn.systemPrompt;
|
|
252
|
+
const userRecord = {
|
|
253
|
+
...toRecord({ role: 'user', content: ctx.turn.request.message }, messageTs, threadTs),
|
|
254
|
+
...(systemPrompt ? { systemPrompt } : {}),
|
|
255
|
+
...(sessionMeta ? { meta: sessionMeta } : {}),
|
|
256
|
+
};
|
|
257
|
+
const records = store.get(key) ?? [];
|
|
258
|
+
records.push(userRecord);
|
|
259
|
+
if (records.length > maxRecords) {
|
|
260
|
+
records.splice(0, records.length - maxRecords);
|
|
261
|
+
}
|
|
262
|
+
store.set(key, records);
|
|
263
|
+
appendToFile(key, [userRecord]);
|
|
264
|
+
// Track how many history messages we prepended so afterTurn
|
|
265
|
+
// knows where new messages start. +1 for the user message
|
|
266
|
+
// we just added to the log (it's in the snapshot but not in
|
|
267
|
+
// the history we built — the history was built from the
|
|
268
|
+
// snapshot BEFORE adding the user record).
|
|
269
|
+
ctx.turn.metadata['_diskSessionHistoryLen'] = history.length;
|
|
270
|
+
if (history.length > 0) {
|
|
271
|
+
ctx.turn.messages = [...history, ...ctx.turn.messages];
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
// ── afterLLM: accumulate token usage across iterations ─────
|
|
275
|
+
// Each LLM call returns usage (prompt/completion tokens). We
|
|
276
|
+
// accumulate them across all iterations in the turn so the final
|
|
277
|
+
// assistant record has the total cost.
|
|
278
|
+
agent.hook('afterLLM', 'disk-session', async (ctx) => {
|
|
279
|
+
const usage = ctx.modelResponse?.usage;
|
|
280
|
+
if (!usage)
|
|
281
|
+
return;
|
|
282
|
+
const prev = ctx.turn.metadata['_diskSessionUsage'] ?? { promptTokens: 0, completionTokens: 0, iterations: 0 };
|
|
283
|
+
ctx.turn.metadata['_diskSessionUsage'] = {
|
|
284
|
+
promptTokens: prev.promptTokens + usage.promptTokens,
|
|
285
|
+
completionTokens: prev.completionTokens + usage.completionTokens,
|
|
286
|
+
iterations: prev.iterations + 1,
|
|
287
|
+
...(usage.cachedPromptTokens && {
|
|
288
|
+
cachedPromptTokens: (prev.cachedPromptTokens ?? 0) + usage.cachedPromptTokens,
|
|
289
|
+
}),
|
|
290
|
+
...(usage.reasoningTokens && {
|
|
291
|
+
reasoningTokens: (prev.reasoningTokens ?? 0) + usage.reasoningTokens,
|
|
292
|
+
}),
|
|
293
|
+
};
|
|
294
|
+
});
|
|
295
|
+
// ── afterTurn: append assistant response + tool calls ─────
|
|
296
|
+
// The user message was already persisted in beforeTurn.
|
|
297
|
+
// Here we only append the NEW non-user, non-system messages
|
|
298
|
+
// (assistant responses, tool calls, tool results).
|
|
299
|
+
agent.hook('afterTurn', 'disk-session', async (ctx) => {
|
|
300
|
+
const identity = identityFromCtx(ctx);
|
|
301
|
+
if (!identity)
|
|
302
|
+
return;
|
|
303
|
+
const { key, messageTs, threadTs } = identity;
|
|
304
|
+
const historyLen = ctx.turn.metadata['_diskSessionHistoryLen'] ?? 0;
|
|
305
|
+
delete ctx.turn.metadata['_diskSessionHistoryLen'];
|
|
306
|
+
// ── Compaction: rotate the session file ──────────────────
|
|
307
|
+
// When metadata.compacting is set, this turn was a compaction
|
|
308
|
+
// request (triggered by /compact). The agent's response is the
|
|
309
|
+
// summary. Instead of appending to the old session, we:
|
|
310
|
+
// 1. Rename the old JSONL file with a timestamp suffix
|
|
311
|
+
// 2. Seed a new session with the summary as the first record
|
|
312
|
+
// The compaction request/response are already in the old file
|
|
313
|
+
// (written by beforeTurn + the normal append below would have
|
|
314
|
+
// run, but we return early before that).
|
|
315
|
+
const isCompacting = ctx.turn.request.metadata?.compacting === true;
|
|
316
|
+
if (isCompacting) {
|
|
317
|
+
const summary = ctx.turn.response?.message ?? '';
|
|
318
|
+
// Archive the old file with a timestamp suffix.
|
|
319
|
+
const oldPath = filePath(key);
|
|
320
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
321
|
+
const archivedName = `${key.replace(/[^a-zA-Z0-9_-]/g, '_')}_${timestamp}.jsonl`;
|
|
322
|
+
const archivedPath = join(dir, archivedName);
|
|
323
|
+
if (existsSync(oldPath)) {
|
|
324
|
+
renameSync(oldPath, archivedPath);
|
|
325
|
+
}
|
|
326
|
+
// Seed the new session with the summary.
|
|
327
|
+
const summaryRecord = {
|
|
328
|
+
role: 'assistant',
|
|
329
|
+
content: `[Session summary — prior conversation compacted ${new Date().toISOString()}]\n\n${summary}`,
|
|
330
|
+
ts: messageTs || String(Date.now() / 1000),
|
|
331
|
+
recordedAt: new Date().toISOString(),
|
|
332
|
+
};
|
|
333
|
+
store.set(key, [summaryRecord]);
|
|
334
|
+
appendToFile(key, [summaryRecord]);
|
|
335
|
+
// Store the archived filename so the host can report it.
|
|
336
|
+
ctx.turn.metadata['_compactArchivedFile'] = archivedName;
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
// Messages: [history...] [system?] [user msg] [assistant] [tool calls...]
|
|
340
|
+
// The beforeContext hook may have inserted a system message.
|
|
341
|
+
// Skip it when counting new messages.
|
|
342
|
+
const systemMsgCount = ctx.turn.messages[historyLen]?.role === 'system' ? 1 : 0;
|
|
343
|
+
const newStart = historyLen + systemMsgCount;
|
|
344
|
+
const newMessages = ctx.turn.messages.slice(newStart);
|
|
345
|
+
// Only persist non-user, non-system messages here.
|
|
346
|
+
// The user message was already written in beforeTurn.
|
|
347
|
+
const filteredMessages = newMessages.filter((m) => m.role !== 'system' && m.role !== 'user');
|
|
348
|
+
if (filteredMessages.length === 0)
|
|
349
|
+
return;
|
|
350
|
+
// Pull accumulated usage for this turn (set by afterLLM hook).
|
|
351
|
+
const usage = ctx.turn.metadata['_diskSessionUsage'];
|
|
352
|
+
delete ctx.turn.metadata['_diskSessionUsage'];
|
|
353
|
+
// Enrichment bag (same convention as beforeTurn): any
|
|
354
|
+
// extension may have written into `sessionMeta` during the
|
|
355
|
+
// turn. Persist it opaquely on each new record.
|
|
356
|
+
const sessionMeta = ctx.turn.metadata.sessionMeta;
|
|
357
|
+
// Convert to records with correlation metadata.
|
|
358
|
+
const newRecords = filteredMessages.map((msg) => {
|
|
359
|
+
const record = {
|
|
360
|
+
...toRecord(msg, messageTs, threadTs),
|
|
361
|
+
...(sessionMeta ? { meta: sessionMeta } : {}),
|
|
362
|
+
};
|
|
363
|
+
// Attach usage to the LAST assistant record (the final response).
|
|
364
|
+
// Intermediate assistant records (tool-call requests) don't get usage.
|
|
365
|
+
return record;
|
|
366
|
+
});
|
|
367
|
+
// Attach accumulated usage to the last assistant record.
|
|
368
|
+
if (usage) {
|
|
369
|
+
let lastAssistantIdx = -1;
|
|
370
|
+
for (let i = newRecords.length - 1; i >= 0; i--) {
|
|
371
|
+
if (newRecords[i].role === 'assistant') {
|
|
372
|
+
lastAssistantIdx = i;
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (lastAssistantIdx !== -1) {
|
|
377
|
+
newRecords[lastAssistantIdx].usage = usage;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// Append to in-memory log.
|
|
381
|
+
const records = store.get(key) ?? [];
|
|
382
|
+
records.push(...newRecords);
|
|
383
|
+
// Trim in-memory cache (file keeps full history).
|
|
384
|
+
if (records.length > maxRecords) {
|
|
385
|
+
records.splice(0, records.length - maxRecords);
|
|
386
|
+
}
|
|
387
|
+
store.set(key, records);
|
|
388
|
+
// Append to JSONL file (append-only, never rewritten).
|
|
389
|
+
appendToFile(key, newRecords);
|
|
390
|
+
});
|
|
391
|
+
},
|
|
392
|
+
/** Get all records for a session. */
|
|
393
|
+
getRecords(sessionKey = 'default') {
|
|
394
|
+
return store.get(sessionKey) ?? [];
|
|
395
|
+
},
|
|
396
|
+
/**
|
|
397
|
+
* Append a control record (e.g. /halt) to the session log.
|
|
398
|
+
* Control records are persisted to disk but never included in the
|
|
399
|
+
* LLM context — they're for audit/logging only.
|
|
400
|
+
*/
|
|
401
|
+
appendControl(sessionKey, content, ts, threadTs) {
|
|
402
|
+
const record = {
|
|
403
|
+
role: 'control',
|
|
404
|
+
content,
|
|
405
|
+
ts: ts ?? String(Date.now() / 1000),
|
|
406
|
+
...(threadTs ? { threadTs } : {}),
|
|
407
|
+
recordedAt: new Date().toISOString(),
|
|
408
|
+
};
|
|
409
|
+
const records = store.get(sessionKey);
|
|
410
|
+
if (records) {
|
|
411
|
+
records.push(record);
|
|
412
|
+
if (records.length > maxRecords) {
|
|
413
|
+
store.set(sessionKey, records.slice(-maxRecords));
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
store.set(sessionKey, [record]);
|
|
418
|
+
}
|
|
419
|
+
appendToFile(sessionKey, [record]);
|
|
420
|
+
},
|
|
421
|
+
/**
|
|
422
|
+
* Append a background message to the session log. Stored as a
|
|
423
|
+
* `system` record so the agent sees it as context ("here's what
|
|
424
|
+
* the channel discussed") rather than a message to respond to.
|
|
425
|
+
*
|
|
426
|
+
* Unlike `appendControl`, system records ARE included in the LLM
|
|
427
|
+
* context window — they appear in the conversation history when
|
|
428
|
+
* the agent is triggered, giving it awareness of recent channel
|
|
429
|
+
* activity without needing to be mentioned on every message.
|
|
430
|
+
*
|
|
431
|
+
* The host should include sender info in the content (e.g.
|
|
432
|
+
* `[U123]: hey anyone seen the invoice?`) so the agent can
|
|
433
|
+
* distinguish who said what. Additional metadata (files, blocks,
|
|
434
|
+
* etc.) goes in `opts.meta` and is persisted to the JSONL for
|
|
435
|
+
* audit/debugging but not shown to the LLM.
|
|
436
|
+
*/
|
|
437
|
+
appendMessage(sessionKey, content, opts) {
|
|
438
|
+
const record = {
|
|
439
|
+
role: 'system',
|
|
440
|
+
content,
|
|
441
|
+
ts: opts?.ts ?? String(Date.now() / 1000),
|
|
442
|
+
...(opts?.threadTs ? { threadTs: opts.threadTs } : {}),
|
|
443
|
+
recordedAt: new Date().toISOString(),
|
|
444
|
+
...(opts?.meta ? { meta: opts.meta } : {}),
|
|
445
|
+
};
|
|
446
|
+
const records = store.get(sessionKey);
|
|
447
|
+
if (records) {
|
|
448
|
+
records.push(record);
|
|
449
|
+
if (records.length > maxRecords) {
|
|
450
|
+
store.set(sessionKey, records.slice(-maxRecords));
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
store.set(sessionKey, [record]);
|
|
455
|
+
}
|
|
456
|
+
appendToFile(sessionKey, [record]);
|
|
457
|
+
},
|
|
458
|
+
/** List all session keys. */
|
|
459
|
+
getSessions() {
|
|
460
|
+
return Array.from(store.keys());
|
|
461
|
+
},
|
|
462
|
+
/** Clear a single session (memory + disk). */
|
|
463
|
+
clear(sessionKey = 'default') {
|
|
464
|
+
store.delete(sessionKey);
|
|
465
|
+
const path = filePath(sessionKey);
|
|
466
|
+
if (existsSync(path)) {
|
|
467
|
+
try {
|
|
468
|
+
writeFileSync(path, '');
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
// ignore
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
/** Clear all sessions. */
|
|
476
|
+
clearAll() {
|
|
477
|
+
for (const key of store.keys()) {
|
|
478
|
+
const path = filePath(key);
|
|
479
|
+
if (existsSync(path)) {
|
|
480
|
+
try {
|
|
481
|
+
writeFileSync(path, '');
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
// ignore
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
store.clear();
|
|
489
|
+
},
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
//# sourceMappingURL=extension.js.map
|