@xandout/libra-harness 0.1.12 → 0.1.14

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.
Files changed (53) hide show
  1. package/dist/agent.d.ts.map +1 -1
  2. package/dist/agent.js +48 -11
  3. package/dist/agent.js.map +1 -1
  4. package/dist/extras/extensions/code-tools/extension.d.ts +83 -0
  5. package/dist/extras/extensions/code-tools/extension.d.ts.map +1 -0
  6. package/dist/extras/extensions/code-tools/extension.js +93 -0
  7. package/dist/extras/extensions/code-tools/extension.js.map +1 -0
  8. package/dist/extras/extensions/code-tools/extension.json +7 -0
  9. package/dist/extras/extensions/code-tools/extension.test.d.ts +2 -0
  10. package/dist/extras/extensions/code-tools/extension.test.d.ts.map +1 -0
  11. package/dist/extras/extensions/code-tools/extension.test.js +656 -0
  12. package/dist/extras/extensions/code-tools/extension.test.js.map +1 -0
  13. package/dist/extras/extensions/code-tools/index.d.ts +3 -0
  14. package/dist/extras/extensions/code-tools/index.d.ts.map +1 -0
  15. package/dist/extras/extensions/code-tools/index.js +2 -0
  16. package/dist/extras/extensions/code-tools/index.js.map +1 -0
  17. package/dist/extras/extensions/code-tools/tools/code-search.d.ts +12 -0
  18. package/dist/extras/extensions/code-tools/tools/code-search.d.ts.map +1 -0
  19. package/dist/extras/extensions/code-tools/tools/code-search.js +86 -0
  20. package/dist/extras/extensions/code-tools/tools/code-search.js.map +1 -0
  21. package/dist/extras/extensions/code-tools/tools/find.d.ts +3 -0
  22. package/dist/extras/extensions/code-tools/tools/find.d.ts.map +1 -0
  23. package/dist/extras/extensions/code-tools/tools/find.js +160 -0
  24. package/dist/extras/extensions/code-tools/tools/find.js.map +1 -0
  25. package/dist/extras/extensions/code-tools/tools/grep.d.ts +3 -0
  26. package/dist/extras/extensions/code-tools/tools/grep.d.ts.map +1 -0
  27. package/dist/extras/extensions/code-tools/tools/grep.js +306 -0
  28. package/dist/extras/extensions/code-tools/tools/grep.js.map +1 -0
  29. package/dist/extras/extensions/code-tools/tools/read.d.ts +3 -0
  30. package/dist/extras/extensions/code-tools/tools/read.d.ts.map +1 -0
  31. package/dist/extras/extensions/code-tools/tools/read.js +97 -0
  32. package/dist/extras/extensions/code-tools/tools/read.js.map +1 -0
  33. package/dist/extras/extensions/code-tools/tools/shared.d.ts +14 -0
  34. package/dist/extras/extensions/code-tools/tools/shared.d.ts.map +1 -0
  35. package/dist/extras/extensions/code-tools/tools/shared.js +53 -0
  36. package/dist/extras/extensions/code-tools/tools/shared.js.map +1 -0
  37. package/dist/extras/extensions/code-tools/tools/shell.d.ts +49 -0
  38. package/dist/extras/extensions/code-tools/tools/shell.d.ts.map +1 -0
  39. package/dist/extras/extensions/code-tools/tools/shell.js +623 -0
  40. package/dist/extras/extensions/code-tools/tools/shell.js.map +1 -0
  41. package/dist/extras/extensions/code-tools/tools/todo.d.ts +24 -0
  42. package/dist/extras/extensions/code-tools/tools/todo.d.ts.map +1 -0
  43. package/dist/extras/extensions/code-tools/tools/todo.js +131 -0
  44. package/dist/extras/extensions/code-tools/tools/todo.js.map +1 -0
  45. package/dist/extras/extensions/code-tools/tools/write.d.ts +4 -0
  46. package/dist/extras/extensions/code-tools/tools/write.d.ts.map +1 -0
  47. package/dist/extras/extensions/code-tools/tools/write.js +179 -0
  48. package/dist/extras/extensions/code-tools/tools/write.js.map +1 -0
  49. package/dist/extras/extensions/disk-session/extension.d.ts +5 -0
  50. package/dist/extras/extensions/disk-session/extension.d.ts.map +1 -1
  51. package/dist/extras/extensions/disk-session/extension.js +148 -20
  52. package/dist/extras/extensions/disk-session/extension.js.map +1 -1
  53. package/package.json +5 -1
@@ -0,0 +1,24 @@
1
+ export type TodoStatus = 'pending' | 'in_progress' | 'completed';
2
+ export interface Todo {
3
+ content: string;
4
+ status: TodoStatus;
5
+ }
6
+ export declare class TodoStore {
7
+ private todos;
8
+ private filePath?;
9
+ constructor(filePath?: string);
10
+ /** Load todos from disk. */
11
+ private load;
12
+ /** Save todos to disk. */
13
+ private save;
14
+ /** Replace the entire todo list. */
15
+ setAll(todos: Todo[]): void;
16
+ /** Get a snapshot of the current todos. */
17
+ getAll(): Todo[];
18
+ clear(): void;
19
+ }
20
+ export declare const todoWriteTool: (cfg: {
21
+ toolPrefix: string;
22
+ store: TodoStore;
23
+ }) => any;
24
+ //# sourceMappingURL=todo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"todo.d.ts","sourceRoot":"","sources":["../../../../../src/extras/extensions/code-tools/tools/todo.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,aAAa,GAAG,WAAW,CAAC;AAEjE,MAAM,WAAW,IAAI;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,UAAU,CAAC;CACpB;AAED,qBAAa,SAAS;IACpB,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,QAAQ,CAAC,CAAS;IAE1B,YAAY,QAAQ,CAAC,EAAE,MAAM,EAK5B;IAED,4BAA4B;IAC5B,OAAO,CAAC,IAAI;IAmBZ,0BAA0B;IAC1B,OAAO,CAAC,IAAI;IAUZ,oCAAoC;IACpC,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,IAAI,CAG1B;IAED,2CAA2C;IAC3C,MAAM,IAAI,IAAI,EAAE,CAEf;IAED,KAAK,IAAI,IAAI,CAGZ;CACF;AAGD,eAAO,MAAM,aAAa,EAAE,CAAC,GAAG,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,SAAS,CAAA;CAAE,KAAK,GA4E7E,CAAC"}
@@ -0,0 +1,131 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import { makeToolName } from './shared.js';
4
+ export class TodoStore {
5
+ todos = [];
6
+ filePath;
7
+ constructor(filePath) {
8
+ this.filePath = filePath;
9
+ if (filePath) {
10
+ this.load();
11
+ }
12
+ }
13
+ /** Load todos from disk. */
14
+ load() {
15
+ if (!this.filePath)
16
+ return;
17
+ try {
18
+ if (existsSync(this.filePath)) {
19
+ const data = JSON.parse(readFileSync(this.filePath, 'utf-8'));
20
+ if (Array.isArray(data)) {
21
+ this.todos = data.map((t) => ({
22
+ content: String(t.content ?? ''),
23
+ status: (t.status === 'in_progress' || t.status === 'completed')
24
+ ? t.status
25
+ : 'pending',
26
+ }));
27
+ }
28
+ }
29
+ }
30
+ catch {
31
+ // ignore corrupt file
32
+ }
33
+ }
34
+ /** Save todos to disk. */
35
+ save() {
36
+ if (!this.filePath)
37
+ return;
38
+ try {
39
+ mkdirSync(dirname(this.filePath), { recursive: true });
40
+ writeFileSync(this.filePath, JSON.stringify(this.todos, null, 2) + '\n', 'utf-8');
41
+ }
42
+ catch {
43
+ // ignore write errors
44
+ }
45
+ }
46
+ /** Replace the entire todo list. */
47
+ setAll(todos) {
48
+ this.todos = todos;
49
+ this.save();
50
+ }
51
+ /** Get a snapshot of the current todos. */
52
+ getAll() {
53
+ return [...this.todos];
54
+ }
55
+ clear() {
56
+ this.todos = [];
57
+ this.save();
58
+ }
59
+ }
60
+ // ── todo_write tool ──────────────────────────────────────────────────
61
+ export const todoWriteTool = (cfg) => ({
62
+ name: makeToolName(cfg.toolPrefix, 'todo_write'),
63
+ description: 'Create and manage a structured task list for tracking multi-step work. ' +
64
+ 'Use this for tasks with 3 or more distinct steps. Skip it for trivial single-step work. ' +
65
+ 'Each todo has a status: "pending", "in_progress", or "completed". ' +
66
+ 'Keep exactly ONE todo as "in_progress" at a time — mark it in_progress when starting, ' +
67
+ 'and mark it "completed" immediately when done. ' +
68
+ 'The entire list is replaced on each call (not appended). ' +
69
+ 'Add follow-up tasks discovered during work. Remove tasks that are no longer relevant. ' +
70
+ 'Todos persist across sessions, so you can pick up where you left off.',
71
+ parameters: {
72
+ type: 'object',
73
+ properties: {
74
+ todos: {
75
+ type: 'array',
76
+ description: 'The complete todo list. Replaces the previous list on each call.',
77
+ items: {
78
+ type: 'object',
79
+ properties: {
80
+ content: {
81
+ type: 'string',
82
+ description: 'The task description.',
83
+ },
84
+ status: {
85
+ type: 'string',
86
+ enum: ['pending', 'in_progress', 'completed'],
87
+ description: 'Current status of the todo.',
88
+ },
89
+ },
90
+ required: ['content', 'status'],
91
+ },
92
+ },
93
+ },
94
+ required: ['todos'],
95
+ },
96
+ async execute(args) {
97
+ const input = args.todos;
98
+ if (!Array.isArray(input)) {
99
+ return { toolCallId: '', content: 'Error: todos must be an array' };
100
+ }
101
+ const todos = input.map((t) => ({
102
+ content: String(t.content ?? ''),
103
+ status: (t.status === 'in_progress' || t.status === 'completed')
104
+ ? t.status
105
+ : 'pending',
106
+ }));
107
+ // Validate: at most one in_progress.
108
+ const inProgressCount = todos.filter((t) => t.status === 'in_progress').length;
109
+ if (inProgressCount > 1) {
110
+ return {
111
+ toolCallId: '',
112
+ content: 'Error: only one todo can be "in_progress" at a time. Mark the others "pending" or "completed".',
113
+ isError: true,
114
+ };
115
+ }
116
+ cfg.store.setAll(todos);
117
+ // Format the response so the model sees the current state.
118
+ if (todos.length === 0) {
119
+ return { toolCallId: '', content: 'Todo list cleared.' };
120
+ }
121
+ const lines = todos.map((t, i) => {
122
+ const marker = t.status === 'completed' ? '[x]' : t.status === 'in_progress' ? '[~]' : '[ ]';
123
+ return `${i + 1}. ${marker} ${t.content}`;
124
+ });
125
+ return {
126
+ toolCallId: '',
127
+ content: `Updated todo list:\n${lines.join('\n')}`,
128
+ };
129
+ },
130
+ });
131
+ //# sourceMappingURL=todo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"todo.js","sourceRoot":"","sources":["../../../../../src/extras/extensions/code-tools/tools/todo.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import { type ToolFactory } from './shared.js';
2
+ export declare const writeTool: ToolFactory;
3
+ export declare const editTool: ToolFactory;
4
+ //# sourceMappingURL=write.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write.d.ts","sourceRoot":"","sources":["../../../../../src/extras/extensions/code-tools/tools/write.ts"],"names":[],"mappings":"AAEA,OAAO,EAA4B,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAGzE,eAAO,MAAM,SAAS,EAAE,WA2DtB,CAAC;AAGH,eAAO,MAAM,QAAQ,EAAE,WA2HrB,CAAC"}
@@ -0,0 +1,179 @@
1
+ import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import { makeToolName, getReadSet } from './shared.js';
4
+ // ── write ────────────────────────────────────────────────────────────
5
+ export const writeTool = (cfg) => ({
6
+ name: makeToolName(cfg.toolPrefix, 'write'),
7
+ description: 'Write content to a file, creating it if it does not exist or overwriting it if it does. ' +
8
+ 'You MUST read an existing file before overwriting it — this is enforced. ' +
9
+ 'Use absolute paths. Creates parent directories as needed. ' +
10
+ 'The content parameter is the complete file contents (not a patch).',
11
+ parameters: {
12
+ type: 'object',
13
+ properties: {
14
+ file_path: {
15
+ type: 'string',
16
+ description: 'The absolute path to the file to write.',
17
+ },
18
+ content: {
19
+ type: 'string',
20
+ description: 'The complete content to write to the file.',
21
+ },
22
+ },
23
+ required: ['file_path', 'content'],
24
+ },
25
+ async execute(args, ctx) {
26
+ const filePath = String(args.file_path ?? '');
27
+ if (!filePath) {
28
+ return { toolCallId: '', content: 'Error: file_path is required' };
29
+ }
30
+ const content = String(args.content ?? '');
31
+ // Enforce read-before-overwrite for existing files.
32
+ if (existsSync(filePath)) {
33
+ const readSet = getReadSet(ctx.metadata);
34
+ if (!readSet.has(filePath)) {
35
+ return {
36
+ toolCallId: '',
37
+ content: `Error: You must read "${filePath}" before overwriting it. Use the read tool first.`,
38
+ isError: true,
39
+ };
40
+ }
41
+ }
42
+ try {
43
+ const dir = dirname(filePath);
44
+ if (!existsSync(dir)) {
45
+ mkdirSync(dir, { recursive: true });
46
+ }
47
+ writeFileSync(filePath, content, 'utf-8');
48
+ return {
49
+ toolCallId: '',
50
+ content: `Wrote ${content.length} chars to ${filePath}`,
51
+ };
52
+ }
53
+ catch (err) {
54
+ return {
55
+ toolCallId: '',
56
+ content: `Error writing file: ${err instanceof Error ? err.message : String(err)}`,
57
+ isError: true,
58
+ };
59
+ }
60
+ },
61
+ });
62
+ // ── edit ─────────────────────────────────────────────────────────────
63
+ export const editTool = (cfg) => ({
64
+ name: makeToolName(cfg.toolPrefix, 'edit'),
65
+ description: 'Replace a string in a file with a new string. The old_string must be unique in the file ' +
66
+ 'unless replace_all is true. You MUST read the file before editing it. ' +
67
+ 'Use absolute paths. Preserve indentation exactly — match the file\'s actual content. ' +
68
+ 'The new_string must differ from old_string.',
69
+ parameters: {
70
+ type: 'object',
71
+ properties: {
72
+ file_path: {
73
+ type: 'string',
74
+ description: 'The absolute path to the file to edit.',
75
+ },
76
+ old_string: {
77
+ type: 'string',
78
+ description: 'The exact string to find in the file. Must be unique unless replace_all is true.',
79
+ },
80
+ new_string: {
81
+ type: 'string',
82
+ description: 'The string to replace old_string with. Must differ from old_string.',
83
+ },
84
+ replace_all: {
85
+ type: 'boolean',
86
+ description: 'If true, replace all occurrences of old_string. Default: false.',
87
+ },
88
+ },
89
+ required: ['file_path', 'old_string', 'new_string'],
90
+ },
91
+ async execute(args, ctx) {
92
+ const filePath = String(args.file_path ?? '');
93
+ if (!filePath) {
94
+ return { toolCallId: '', content: 'Error: file_path is required' };
95
+ }
96
+ const oldString = String(args.old_string ?? '');
97
+ const newString = String(args.new_string ?? '');
98
+ const replaceAll = args.replace_all === true;
99
+ if (oldString === newString) {
100
+ return {
101
+ toolCallId: '',
102
+ content: 'Error: new_string must differ from old_string.',
103
+ isError: true,
104
+ };
105
+ }
106
+ if (!existsSync(filePath)) {
107
+ return {
108
+ toolCallId: '',
109
+ content: `File not found: ${filePath}`,
110
+ isError: true,
111
+ };
112
+ }
113
+ // Enforce read-before-edit.
114
+ const readSet = getReadSet(ctx.metadata);
115
+ if (!readSet.has(filePath)) {
116
+ return {
117
+ toolCallId: '',
118
+ content: `Error: You must read "${filePath}" before editing it. Use the read tool first.`,
119
+ isError: true,
120
+ };
121
+ }
122
+ let content;
123
+ try {
124
+ content = readFileSync(filePath, 'utf-8');
125
+ }
126
+ catch (err) {
127
+ return {
128
+ toolCallId: '',
129
+ content: `Error reading file: ${err instanceof Error ? err.message : String(err)}`,
130
+ isError: true,
131
+ };
132
+ }
133
+ // Count occurrences.
134
+ let count = 0;
135
+ let idx = 0;
136
+ while ((idx = content.indexOf(oldString, idx)) !== -1) {
137
+ count++;
138
+ idx += oldString.length;
139
+ }
140
+ if (count === 0) {
141
+ return {
142
+ toolCallId: '',
143
+ content: `Error: old_string not found in ${filePath}. Check for exact whitespace, indentation, and line endings.`,
144
+ isError: true,
145
+ };
146
+ }
147
+ if (count > 1 && !replaceAll) {
148
+ return {
149
+ toolCallId: '',
150
+ content: `Error: old_string appears ${count} times in ${filePath}. Provide more surrounding context to make it unique, or set replace_all to true.`,
151
+ isError: true,
152
+ };
153
+ }
154
+ // Perform the replacement.
155
+ let newContent;
156
+ if (replaceAll) {
157
+ newContent = content.split(oldString).join(newString);
158
+ }
159
+ else {
160
+ newContent = content.replace(oldString, newString);
161
+ }
162
+ try {
163
+ writeFileSync(filePath, newContent, 'utf-8');
164
+ const replaced = replaceAll ? count : 1;
165
+ return {
166
+ toolCallId: '',
167
+ content: `Replaced ${replaced} occurrence${replaced === 1 ? '' : 's'} in ${filePath}`,
168
+ };
169
+ }
170
+ catch (err) {
171
+ return {
172
+ toolCallId: '',
173
+ content: `Error writing file: ${err instanceof Error ? err.message : String(err)}`,
174
+ isError: true,
175
+ };
176
+ }
177
+ },
178
+ });
179
+ //# sourceMappingURL=write.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write.js","sourceRoot":"","sources":["../../../../../src/extras/extensions/code-tools/tools/write.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,YAAY,EAAE,UAAU,EAAoB,MAAM,aAAa,CAAC;AAEzE,wEAAwE;AACxE,MAAM,CAAC,MAAM,SAAS,GAAgB,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IAC9C,IAAI,EAAE,YAAY,CAAC,GAAG,CAAC,UAAU,EAAE,OAAO,CAAC;IAC3C,WAAW,EACT,0FAA0F;QAC1F,2EAA2E;QAC3E,4DAA4D;QAC5D,oEAAoE;IACtE,UAAU,EAAE;QACV,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE;YACV,SAAS,EAAE;gBACT,IAAI,EAAE,QAAQ;gBACd,WAAW,EAAE,yCAAyC;aACvD;YACD,OAAO,EAAE;gBACP,IAAI,EAAE,QAAQ;gBACd,WAAW,EAAE,4CAA4C;aAC1D;SACF;QACD,QAAQ,EAAE,CAAC,WAAW,EAAE,SAAS,CAAC;KACnC;IACD,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG;QACrB,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;QAC9C,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,OAAO,EAAE,UAAU,EAAE,EAAE,EAAE,OAAO,EAAE,8BAA8B,EAAE,CAAC;QACrE,CAAC;QAED,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC;QAE3C,oDAAoD;QACpD,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YACzB,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YACzC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC3B,OAAO;oBACL,UAAU,EAAE,EAAE;oBACd,OAAO,EAAE,yBAAyB,QAAQ,mDAAmD;oBAC7F,OAAO,EAAE,IAAI;iBACd,CAAC;YACJ,CAAC;QACH,CAAC;QAED,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;YAC9B,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;gBACrB,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YACtC,CAAC;YACD,aAAa,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;YAC1C,OAAO;gBACL,UAAU,EAAE,EAAE;gBACd,OAAO,EAAE,SAAS,OAAO,CAAC,MAAM,aAAa,QAAQ,EAAE;aACxD,CAAC;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO;gBACL,UAAU,EAAE,EAAE;gBACd,OAAO,EAAE,uBAAuB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE;gBAClF,OAAO,EAAE,IAAI;aACd,CAAC;QACJ,CAAC;IACH,CAAC;CACF,CAAC,CAAC;AAEH,wEAAwE;AACxE,MAAM,CAAC,MAAM,QAAQ,GAAgB,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IAC7C,IAAI,EAAE,YAAY,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC;IAC1C,WAAW,EACT,0FAA0F;QAC1F,wEAAwE;QACxE,uFAAuF;QACvF,6CAA6C;IAC/C,UAAU,EAAE;QACV,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE;YACV,SAAS,EAAE;gBACT,IAAI,EAAE,QAAQ;gBACd,WAAW,EAAE,wCAAwC;aACtD;YACD,UAAU,EAAE;gBACV,IAAI,EAAE,QAAQ;gBACd,WAAW,EAAE,kFAAkF;aAChG;YACD,UAAU,EAAE;gBACV,IAAI,EAAE,QAAQ;gBACd,WAAW,EAAE,qEAAqE;aACnF;YACD,WAAW,EAAE;gBACX,IAAI,EAAE,SAAS;gBACf,WAAW,EAAE,iEAAiE;aAC/E;SACF;QACD,QAAQ,EAAE,CAAC,WAAW,EAAE,YAAY,EAAE,YAAY,CAAC;KACpD;IACD,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG;QACrB,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;QAC9C,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,OAAO,EAAE,UAAU,EAAE,EAAE,EAAE,OAAO,EAAE,8BAA8B,EAAE,CAAC;QACrE,CAAC;QAED,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC;QAChD,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC;QAChD,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,KAAK,IAAI,CAAC;QAE7C,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC5B,OAAO;gBACL,UAAU,EAAE,EAAE;gBACd,OAAO,EAAE,gDAAgD;gBACzD,OAAO,EAAE,IAAI;aACd,CAAC;QACJ,CAAC;QAED,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC1B,OAAO;gBACL,UAAU,EAAE,EAAE;gBACd,OAAO,EAAE,mBAAmB,QAAQ,EAAE;gBACtC,OAAO,EAAE,IAAI;aACd,CAAC;QACJ,CAAC;QAED,4BAA4B;QAC5B,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACzC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC3B,OAAO;gBACL,UAAU,EAAE,EAAE;gBACd,OAAO,EAAE,yBAAyB,QAAQ,+CAA+C;gBACzF,OAAO,EAAE,IAAI;aACd,CAAC;QACJ,CAAC;QAED,IAAI,OAAe,CAAC;QACpB,IAAI,CAAC;YACH,OAAO,GAAG,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAC5C,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO;gBACL,UAAU,EAAE,EAAE;gBACd,OAAO,EAAE,uBAAuB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE;gBAClF,OAAO,EAAE,IAAI;aACd,CAAC;QACJ,CAAC;QAED,qBAAqB;QACrB,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,IAAI,GAAG,GAAG,CAAC,CAAC;QACZ,OAAO,CAAC,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YACtD,KAAK,EAAE,CAAC;YACR,GAAG,IAAI,SAAS,CAAC,MAAM,CAAC;QAC1B,CAAC;QAED,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;YAChB,OAAO;gBACL,UAAU,EAAE,EAAE;gBACd,OAAO,EAAE,kCAAkC,QAAQ,8DAA8D;gBACjH,OAAO,EAAE,IAAI;aACd,CAAC;QACJ,CAAC;QAED,IAAI,KAAK,GAAG,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;YAC7B,OAAO;gBACL,UAAU,EAAE,EAAE;gBACd,OAAO,EAAE,6BAA6B,KAAK,aAAa,QAAQ,mFAAmF;gBACnJ,OAAO,EAAE,IAAI;aACd,CAAC;QACJ,CAAC;QAED,2BAA2B;QAC3B,IAAI,UAAkB,CAAC;QACvB,IAAI,UAAU,EAAE,CAAC;YACf,UAAU,GAAG,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACxD,CAAC;aAAM,CAAC;YACN,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QACrD,CAAC;QAED,IAAI,CAAC;YACH,aAAa,CAAC,QAAQ,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC;YAC7C,MAAM,QAAQ,GAAG,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;YACxC,OAAO;gBACL,UAAU,EAAE,EAAE;gBACd,OAAO,EAAE,YAAY,QAAQ,cAAc,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,OAAO,QAAQ,EAAE;aACtF,CAAC;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO;gBACL,UAAU,EAAE,EAAE;gBACd,OAAO,EAAE,uBAAuB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE;gBAClF,OAAO,EAAE,IAAI;aACd,CAAC;QACJ,CAAC;IACH,CAAC;CACF,CAAC,CAAC"}
@@ -107,6 +107,11 @@ export interface DiskSessionConfig {
107
107
  recentChannelMessages?: number;
108
108
  /** Load existing JSONL files into memory on startup. Default: true. */
109
109
  loadOnStartup?: boolean;
110
+ /**
111
+ * Print a summary line when sessions are loaded from disk.
112
+ * Default: true.
113
+ */
114
+ verbose?: boolean;
110
115
  /**
111
116
  * Resolve a {@link SessionIdentity} from turn metadata. Default:
112
117
  * reads `metadata.session` as a `SessionIdentity`, falls back to
@@ -1 +1 @@
1
- {"version":3,"file":"extension.d.ts","sourceRoot":"","sources":["../../../../src/extras/extensions/disk-session/extension.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"extension.d.ts","sourceRoot":"","sources":["../../../../src/extras/extensions/disk-session/extension.ts"],"names":[],"mappings":"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"}
@@ -83,7 +83,7 @@ export default function createDiskSessionExtension(config) {
83
83
  // Skip corrupt files.
84
84
  }
85
85
  }
86
- if (store.size > 0) {
86
+ if (store.size > 0 && config?.verbose !== false) {
87
87
  console.log(`[disk-session] loaded ${store.size} session(s), ${totalRecords} record(s) from ${dir}`);
88
88
  }
89
89
  }
@@ -150,6 +150,17 @@ export default function createDiskSessionExtension(config) {
150
150
  return false;
151
151
  return true; // user, system, assistant (without tool calls)
152
152
  };
153
+ // Drop trailing user messages that don't have a following assistant
154
+ // response. This happens when a turn was interrupted (crash, halt,
155
+ // kill -9). Including an unanswered user request from a prior session
156
+ // confuses the agent — it sees a request it never responded to.
157
+ function dropIncompleteTrailingTurn(records) {
158
+ const result = [...records];
159
+ while (result.length > 0 && result[result.length - 1].role === 'user') {
160
+ result.pop();
161
+ }
162
+ return result;
163
+ }
153
164
  // ── Build context (the "fork") from a snapshot ────────────────
154
165
  // The snapshot is a read-only copy of the session records at the
155
166
  // moment the turn started. This function builds the messages array
@@ -163,8 +174,13 @@ export default function createDiskSessionExtension(config) {
163
174
  // DM or top-level message: last N messages, but filter out
164
175
  // tool-call intermediates that might get split by the slice.
165
176
  if (isDirect || !threadTs) {
166
- return snapshot
167
- .filter(isConversationMessage)
177
+ const filtered = snapshot.filter(isConversationMessage);
178
+ // Drop incomplete turns: a user message at the end without a
179
+ // following assistant response means the turn was interrupted
180
+ // (crash, halt, etc). Including it would confuse the agent with
181
+ // an unanswered request from a previous session.
182
+ const complete = dropIncompleteTrailingTurn(filtered);
183
+ return complete
168
184
  .slice(-maxContextMessages)
169
185
  .map(toMessage);
170
186
  }
@@ -268,26 +284,91 @@ export default function createDiskSessionExtension(config) {
268
284
  ctx.turn.messages = [...history, ...ctx.turn.messages];
269
285
  }
270
286
  });
271
- // ── afterLLM: accumulate token usage across iterations ─────
287
+ // ── afterLLM: accumulate token usage + append assistant msg ──
272
288
  // Each LLM call returns usage (prompt/completion tokens). We
273
289
  // accumulate them across all iterations in the turn so the final
274
290
  // assistant record has the total cost.
291
+ //
292
+ // We also append the assistant message (including tool calls) to
293
+ // the session log immediately, so the JSONL file is updated
294
+ // iteration by iteration rather than only at afterTurn. Tool
295
+ // results are appended in the afterTool hook below.
275
296
  agent.hook('afterLLM', 'disk-session', async (ctx) => {
297
+ const identity = identityFromCtx(ctx);
298
+ if (!identity)
299
+ return;
300
+ const { key, messageTs, threadTs } = identity;
276
301
  const usage = ctx.modelResponse?.usage;
277
- if (!usage)
302
+ if (usage) {
303
+ const prev = ctx.turn.metadata['_diskSessionUsage'] ?? { promptTokens: 0, completionTokens: 0, iterations: 0 };
304
+ ctx.turn.metadata['_diskSessionUsage'] = {
305
+ promptTokens: prev.promptTokens + usage.promptTokens,
306
+ completionTokens: prev.completionTokens + usage.completionTokens,
307
+ iterations: prev.iterations + 1,
308
+ ...(usage.cachedPromptTokens && {
309
+ cachedPromptTokens: (prev.cachedPromptTokens ?? 0) + usage.cachedPromptTokens,
310
+ }),
311
+ ...(usage.reasoningTokens && {
312
+ reasoningTokens: (prev.reasoningTokens ?? 0) + usage.reasoningTokens,
313
+ }),
314
+ };
315
+ }
316
+ // ── Incremental append: write the assistant message now ──
317
+ // The agent core pushes the assistant message to turn.messages
318
+ // AFTER afterLLM fires, so we can't read it from there yet.
319
+ // Instead, we build the record from modelResponse.message.
320
+ const modelResponse = ctx.modelResponse;
321
+ if (modelResponse?.message) {
322
+ const sessionMeta = ctx.turn.metadata.sessionMeta;
323
+ const record = {
324
+ ...toRecord(modelResponse.message, messageTs, threadTs),
325
+ ...(sessionMeta ? { meta: sessionMeta } : {}),
326
+ };
327
+ const records = store.get(key) ?? [];
328
+ records.push(record);
329
+ if (records.length > maxRecords) {
330
+ records.splice(0, records.length - maxRecords);
331
+ }
332
+ store.set(key, records);
333
+ appendToFile(key, [record]);
334
+ // Track how many records we've already written so afterTurn
335
+ // doesn't duplicate them.
336
+ const written = ctx.turn.metadata['_diskSessionWritten'] ?? 0;
337
+ ctx.turn.metadata['_diskSessionWritten'] = written + 1;
338
+ }
339
+ });
340
+ // ── afterTool: append tool results incrementally ──────────────
341
+ // Each tool result is written to disk as soon as it completes,
342
+ // so the JSONL file reflects progress in real time.
343
+ agent.hook('afterTool', 'disk-session', async (ctx) => {
344
+ const identity = identityFromCtx(ctx);
345
+ if (!identity)
346
+ return;
347
+ const { key, messageTs, threadTs } = identity;
348
+ const toolResult = ctx.toolResult;
349
+ const toolCall = ctx.toolCall;
350
+ if (!toolResult || !toolCall)
278
351
  return;
279
- const prev = ctx.turn.metadata['_diskSessionUsage'] ?? { promptTokens: 0, completionTokens: 0, iterations: 0 };
280
- ctx.turn.metadata['_diskSessionUsage'] = {
281
- promptTokens: prev.promptTokens + usage.promptTokens,
282
- completionTokens: prev.completionTokens + usage.completionTokens,
283
- iterations: prev.iterations + 1,
284
- ...(usage.cachedPromptTokens && {
285
- cachedPromptTokens: (prev.cachedPromptTokens ?? 0) + usage.cachedPromptTokens,
286
- }),
287
- ...(usage.reasoningTokens && {
288
- reasoningTokens: (prev.reasoningTokens ?? 0) + usage.reasoningTokens,
289
- }),
352
+ const sessionMeta = ctx.turn.metadata.sessionMeta;
353
+ const record = {
354
+ role: 'tool',
355
+ content: toolResult.content,
356
+ toolCallId: toolResult.toolCallId,
357
+ name: toolCall.name,
358
+ ts: messageTs,
359
+ ...(threadTs ? { threadTs } : {}),
360
+ recordedAt: new Date().toISOString(),
361
+ ...(sessionMeta ? { meta: sessionMeta } : {}),
290
362
  };
363
+ const records = store.get(key) ?? [];
364
+ records.push(record);
365
+ if (records.length > maxRecords) {
366
+ records.splice(0, records.length - maxRecords);
367
+ }
368
+ store.set(key, records);
369
+ appendToFile(key, [record]);
370
+ const written = ctx.turn.metadata['_diskSessionWritten'] ?? 0;
371
+ ctx.turn.metadata['_diskSessionWritten'] = written + 1;
291
372
  });
292
373
  // ── afterTurn: append assistant response + tool calls ─────
293
374
  // The user message was already persisted in beforeTurn.
@@ -339,11 +420,58 @@ export default function createDiskSessionExtension(config) {
339
420
  const systemMsgCount = ctx.turn.messages[historyLen]?.role === 'system' ? 1 : 0;
340
421
  const newStart = historyLen + systemMsgCount;
341
422
  const newMessages = ctx.turn.messages.slice(newStart);
342
- // Only persist non-user, non-system messages here.
343
- // The user message was already written in beforeTurn.
344
- const filteredMessages = newMessages.filter((m) => m.role !== 'system' && m.role !== 'user');
345
- if (filteredMessages.length === 0)
423
+ // Persist non-user, non-system messages here. The initial user
424
+ // message was already written in beforeTurn. EXCEPTION: steering
425
+ // messages are injected mid-turn as user messages with a
426
+ // [steering] prefix — those are new and must be persisted.
427
+ let filteredMessages = newMessages.filter((m) => {
428
+ if (m.role === 'system')
429
+ return false;
430
+ if (m.role === 'user') {
431
+ const text = typeof m.content === 'string'
432
+ ? m.content
433
+ : Array.isArray(m.content)
434
+ ? m.content.filter((p) => p.type === 'text').map((p) => p.text).join('')
435
+ : '';
436
+ return text.startsWith('[steering]');
437
+ }
438
+ return true;
439
+ });
440
+ // ── Skip records already written incrementally ───────────
441
+ // afterLLM and afterTool hooks append assistant messages and
442
+ // tool results as they happen. Skip those here to avoid
443
+ // duplicates. Only the final assistant response (with no tool
444
+ // calls) may not have been written yet — it's the one that
445
+ // ends the turn without triggering afterTool.
446
+ const alreadyWritten = ctx.turn.metadata['_diskSessionWritten'] ?? 0;
447
+ delete ctx.turn.metadata['_diskSessionWritten'];
448
+ if (alreadyWritten > 0) {
449
+ filteredMessages = filteredMessages.slice(alreadyWritten);
450
+ }
451
+ if (filteredMessages.length === 0) {
452
+ // All records were already written incrementally by afterLLM
453
+ // and afterTool. Still need to backfill the system prompt on
454
+ // the user record and clean up usage metadata.
455
+ delete ctx.turn.metadata['_diskSessionUsage'];
456
+ // ── Backfill system prompt on the last user record ───────
457
+ const finalSystemPrompt = ctx.turn.systemPrompt;
458
+ if (finalSystemPrompt) {
459
+ const records = store.get(key) ?? [];
460
+ for (let i = records.length - 1; i >= 0; i--) {
461
+ if (records[i].role === 'user') {
462
+ records[i].systemPrompt = finalSystemPrompt;
463
+ break;
464
+ }
465
+ }
466
+ appendToFile(key, [{
467
+ role: 'control',
468
+ content: '',
469
+ recordedAt: new Date().toISOString(),
470
+ systemPrompt: finalSystemPrompt,
471
+ }]);
472
+ }
346
473
  return;
474
+ }
347
475
  // Pull accumulated usage for this turn (set by afterLLM hook).
348
476
  const usage = ctx.turn.metadata['_diskSessionUsage'];
349
477
  delete ctx.turn.metadata['_diskSessionUsage'];