astrabot 0.1.0
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 +411 -0
- package/ai/ai.config.ts +27 -0
- package/ai/auto-retry.ts +117 -0
- package/ai/config-loader.ts +132 -0
- package/ai/index.ts +4 -0
- package/ai/retry-prompt.ts +30 -0
- package/bin/astra +2 -0
- package/core/retry/error-classifier.ts +208 -0
- package/core/retry/index.ts +29 -0
- package/core/retry/retry-config.ts +142 -0
- package/core/retry/retry-engine.ts +215 -0
- package/game/index.html +573 -0
- package/game/neon-breaker.html +1037 -0
- package/index.ts +140 -0
- package/modes/agent/action-tracker.ts +47 -0
- package/modes/agent/agent-tools.ts +338 -0
- package/modes/agent/approval.ts +184 -0
- package/modes/agent/diff-view.ts +34 -0
- package/modes/agent/orchestrator.ts +234 -0
- package/modes/agent/tool-executor.ts +993 -0
- package/modes/agent/types.ts +68 -0
- package/modes/ask/orchestrator.ts +230 -0
- package/modes/auto.ts +88 -0
- package/modes/cli.ts +43 -0
- package/modes/multi/agent-pool-manager.ts +337 -0
- package/modes/multi/examples.ts +441 -0
- package/modes/multi/message-broker.ts +179 -0
- package/modes/multi/multi-agent-orchestrator.ts +891 -0
- package/modes/multi/orchestrator.ts +414 -0
- package/modes/multi/types.ts +245 -0
- package/modes/multi/workflow-builder.ts +569 -0
- package/modes/plan/orchestrator.ts +198 -0
- package/modes/plan/planner.ts +121 -0
- package/modes/plan/selection.ts +43 -0
- package/modes/plan/types.ts +13 -0
- package/modes/plan/web-tools.ts +132 -0
- package/modes/setup.ts +210 -0
- package/package.json +62 -0
- package/session/index.ts +45 -0
- package/session/session-context.ts +188 -0
- package/session/session-manager.ts +374 -0
- package/session/session-tools.ts +109 -0
- package/session/store.ts +278 -0
- package/tsconfig.json +30 -0
- package/tui/spinner.ts +182 -0
- package/tui/terminal-md.ts +17 -0
- package/tui/wakeup.ts +231 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { generateText, stepCountIs } from "ai";
|
|
3
|
+
import { getAgentModel } from "../ai";
|
|
4
|
+
import type { ActionTracker } from "../modes/agent/action-tracker";
|
|
5
|
+
import type { SessionMode, SessionEntry, TranscriptMessage } from "./store";
|
|
6
|
+
import {
|
|
7
|
+
listSessions,
|
|
8
|
+
getSession,
|
|
9
|
+
getMostRecentSession,
|
|
10
|
+
createSession,
|
|
11
|
+
updateSession,
|
|
12
|
+
deleteSession,
|
|
13
|
+
appendTranscript,
|
|
14
|
+
} from "./store";
|
|
15
|
+
import { captureSessionContext, buildContextSummary } from "./session-context";
|
|
16
|
+
|
|
17
|
+
const C = {
|
|
18
|
+
primary: chalk.hex("#a78bfa"),
|
|
19
|
+
dim: chalk.hex("#6b7280"),
|
|
20
|
+
success: chalk.hex("#34d399"),
|
|
21
|
+
warn: chalk.hex("#fbbf24"),
|
|
22
|
+
error: chalk.hex("#f87171"),
|
|
23
|
+
time: chalk.hex("#fbbf24"),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// ── Public API ─────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export interface BeginSessionResult {
|
|
29
|
+
entry: SessionEntry;
|
|
30
|
+
/** Full context block to inject into the system prompt, or null for brand-new sessions. */
|
|
31
|
+
contextSummary: string | null;
|
|
32
|
+
/** True if this is continuing an existing session (user didn't have to ask). */
|
|
33
|
+
autoResumed: boolean;
|
|
34
|
+
/** The session that was resumed, if any. */
|
|
35
|
+
resumedFrom?: SessionEntry;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Start a new session or resume an existing one.
|
|
40
|
+
*
|
|
41
|
+
* Resume logic (in priority order):
|
|
42
|
+
* 1. Explicit `resumeSessionId` supplied → resume that session.
|
|
43
|
+
* 2. `autoResume: true` + an interrupted session exists in this workspace → resume it.
|
|
44
|
+
* 3. `autoResume: true` + the most recent session is "related" to the new goal → resume it.
|
|
45
|
+
* 4. Otherwise → create a fresh session.
|
|
46
|
+
*/
|
|
47
|
+
export function beginSession(opts: {
|
|
48
|
+
workspacePath: string;
|
|
49
|
+
mode: SessionMode;
|
|
50
|
+
goal: string;
|
|
51
|
+
resumeSessionId?: string;
|
|
52
|
+
/** If true, silently resume when a clear prior session exists. Default: true. */
|
|
53
|
+
autoResume?: boolean;
|
|
54
|
+
}): BeginSessionResult {
|
|
55
|
+
const autoResume = opts.autoResume ?? true;
|
|
56
|
+
|
|
57
|
+
// ── 1. Explicit resume
|
|
58
|
+
if (opts.resumeSessionId) {
|
|
59
|
+
return resumeSession(opts.resumeSessionId, opts);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (autoResume) {
|
|
63
|
+
// ── 2. Interrupted session in same workspace
|
|
64
|
+
const interrupted = listSessions(opts.workspacePath, 10).find(
|
|
65
|
+
(s) => s.status === "interrupted"
|
|
66
|
+
);
|
|
67
|
+
if (interrupted) {
|
|
68
|
+
return resumeSession(interrupted.id, opts, true);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── 3. Recent session that looks related
|
|
72
|
+
const recent = getMostRecentSession(opts.workspacePath);
|
|
73
|
+
if (recent && isRelated(recent, opts.goal)) {
|
|
74
|
+
return resumeSession(recent.id, opts, true);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── 4. Brand new session
|
|
79
|
+
const entry = createSession({
|
|
80
|
+
workspacePath: opts.workspacePath,
|
|
81
|
+
mode: opts.mode,
|
|
82
|
+
goal: opts.goal,
|
|
83
|
+
});
|
|
84
|
+
return { entry, contextSummary: null, autoResumed: false };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Record a user message in the active session transcript.
|
|
89
|
+
* Call this each time the user sends a prompt so the transcript stays current.
|
|
90
|
+
*/
|
|
91
|
+
export function recordUserMessage(sessionId: string, content: string): void {
|
|
92
|
+
appendTranscript(sessionId, [
|
|
93
|
+
{ role: "user", content, timestamp: new Date().toISOString() },
|
|
94
|
+
]);
|
|
95
|
+
// Also track goal evolution
|
|
96
|
+
const session = getSession(sessionId);
|
|
97
|
+
if (session && !session.allGoals.includes(content)) {
|
|
98
|
+
updateSession(sessionId, { lastGoal: content });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Record an agent response in the active session transcript.
|
|
104
|
+
* Call this each time the agent produces output.
|
|
105
|
+
*/
|
|
106
|
+
export function recordAgentMessage(sessionId: string, content: string): void {
|
|
107
|
+
appendTranscript(sessionId, [
|
|
108
|
+
{ role: "agent", content, timestamp: new Date().toISOString() },
|
|
109
|
+
]);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Call this after the agent finishes its work.
|
|
114
|
+
* Extracts a summary, captures pending tasks, and persists everything.
|
|
115
|
+
*/
|
|
116
|
+
export async function endSession(
|
|
117
|
+
sessionId: string,
|
|
118
|
+
tracker: ActionTracker,
|
|
119
|
+
agentResponse: string,
|
|
120
|
+
pendingTasks: string[] = []
|
|
121
|
+
): Promise<void> {
|
|
122
|
+
const actions = tracker.getActions();
|
|
123
|
+
const touchedFiles = [
|
|
124
|
+
...new Set(
|
|
125
|
+
actions
|
|
126
|
+
.map((a) => a.path)
|
|
127
|
+
.filter(
|
|
128
|
+
(p) => p !== "web" && p !== "shell" && p !== "skills" && p !== "plan"
|
|
129
|
+
)
|
|
130
|
+
),
|
|
131
|
+
];
|
|
132
|
+
const appliedActions = actions.filter((a) => a.status === "approved").length;
|
|
133
|
+
const rejectedActions = actions.filter((a) => a.status === "rejected").length;
|
|
134
|
+
|
|
135
|
+
const summary = await summariseSession(actions, agentResponse);
|
|
136
|
+
|
|
137
|
+
updateSession(
|
|
138
|
+
sessionId,
|
|
139
|
+
{
|
|
140
|
+
summary,
|
|
141
|
+
touchedFiles,
|
|
142
|
+
appliedActions,
|
|
143
|
+
rejectedActions,
|
|
144
|
+
pendingTasks,
|
|
145
|
+
lastAgentResponse: agentResponse.slice(0, 2_000),
|
|
146
|
+
status: "completed",
|
|
147
|
+
},
|
|
148
|
+
actions
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* End a multi-agent session from multiple trackers.
|
|
154
|
+
*/
|
|
155
|
+
export async function endMultiSession(
|
|
156
|
+
sessionId: string,
|
|
157
|
+
trackers: Map<string, { tracker: ActionTracker; response: string }>,
|
|
158
|
+
pendingTasks: string[] = []
|
|
159
|
+
): Promise<void> {
|
|
160
|
+
let allTouchedFiles: string[] = [];
|
|
161
|
+
let totalApplied = 0;
|
|
162
|
+
let totalRejected = 0;
|
|
163
|
+
const responses: string[] = [];
|
|
164
|
+
|
|
165
|
+
for (const [, { tracker, response }] of trackers) {
|
|
166
|
+
const actions = tracker.getActions();
|
|
167
|
+
const files = actions
|
|
168
|
+
.map((a) => a.path)
|
|
169
|
+
.filter(
|
|
170
|
+
(p) => p !== "web" && p !== "shell" && p !== "skills" && p !== "plan"
|
|
171
|
+
);
|
|
172
|
+
allTouchedFiles.push(...files);
|
|
173
|
+
totalApplied += actions.filter((a) => a.status === "approved").length;
|
|
174
|
+
totalRejected += actions.filter((a) => a.status === "rejected").length;
|
|
175
|
+
if (response) responses.push(response);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const touchedFiles = [...new Set(allTouchedFiles)];
|
|
179
|
+
const allActions = [...trackers.values()].flatMap((t) =>
|
|
180
|
+
t.tracker.getActions()
|
|
181
|
+
);
|
|
182
|
+
const combinedResponse = responses.join("\n\n");
|
|
183
|
+
const summary = await summariseSession(allActions, combinedResponse);
|
|
184
|
+
|
|
185
|
+
updateSession(
|
|
186
|
+
sessionId,
|
|
187
|
+
{
|
|
188
|
+
summary,
|
|
189
|
+
touchedFiles,
|
|
190
|
+
appliedActions: totalApplied,
|
|
191
|
+
rejectedActions: totalRejected,
|
|
192
|
+
pendingTasks,
|
|
193
|
+
lastAgentResponse: combinedResponse.slice(0, 2_000),
|
|
194
|
+
status: "completed",
|
|
195
|
+
},
|
|
196
|
+
allActions
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Mark the current session as interrupted (Ctrl+C, process killed, etc.)
|
|
202
|
+
* The transcript and state so far are preserved for auto-resume.
|
|
203
|
+
*/
|
|
204
|
+
export function markSessionInterrupted(sessionId: string): void {
|
|
205
|
+
updateSession(sessionId, { status: "interrupted" });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Get the most recent session for a workspace that can be resumed.
|
|
210
|
+
*/
|
|
211
|
+
export function getResumableSession(
|
|
212
|
+
workspacePath: string
|
|
213
|
+
): SessionEntry | undefined {
|
|
214
|
+
return getMostRecentSession(workspacePath);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* List recent sessions for a workspace.
|
|
219
|
+
*/
|
|
220
|
+
export function getSessionHistory(
|
|
221
|
+
workspacePath?: string,
|
|
222
|
+
limit = 10
|
|
223
|
+
): SessionEntry[] {
|
|
224
|
+
return listSessions(workspacePath, limit);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Delete a specific session.
|
|
229
|
+
*/
|
|
230
|
+
export function removeSession(id: string): boolean {
|
|
231
|
+
return deleteSession(id);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── Internal helpers ───────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
function resumeSession(
|
|
237
|
+
previousId: string,
|
|
238
|
+
opts: { workspacePath: string; mode: SessionMode; goal: string },
|
|
239
|
+
autoResumed = false
|
|
240
|
+
): BeginSessionResult {
|
|
241
|
+
const prev = getSession(previousId);
|
|
242
|
+
if (!prev) {
|
|
243
|
+
// Fallback: create fresh
|
|
244
|
+
const entry = createSession({
|
|
245
|
+
workspacePath: opts.workspacePath,
|
|
246
|
+
mode: opts.mode,
|
|
247
|
+
goal: opts.goal,
|
|
248
|
+
});
|
|
249
|
+
return { entry, contextSummary: null, autoResumed: false };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Mark old session as completed before chaining
|
|
253
|
+
updateSession(prev.id, { status: "completed" });
|
|
254
|
+
|
|
255
|
+
const contextSummary = buildContextSummary(prev, { transcriptTurns: 12 });
|
|
256
|
+
|
|
257
|
+
const entry = createSession({
|
|
258
|
+
workspacePath: opts.workspacePath,
|
|
259
|
+
mode: opts.mode,
|
|
260
|
+
goal: opts.goal,
|
|
261
|
+
previousSessionId: prev.id,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Carry forward pending tasks and touched files so the new session inherits them
|
|
265
|
+
if (prev.pendingTasks?.length || prev.touchedFiles?.length) {
|
|
266
|
+
updateSession(entry.id, {
|
|
267
|
+
pendingTasks: prev.pendingTasks ?? [],
|
|
268
|
+
touchedFiles: prev.touchedFiles ?? [],
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
entry,
|
|
274
|
+
contextSummary,
|
|
275
|
+
autoResumed,
|
|
276
|
+
resumedFrom: prev,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Heuristic: is a new goal "related" to a previous session?
|
|
282
|
+
* Uses keyword overlap and recency (sessions older than 4h are less likely to be relevant).
|
|
283
|
+
*/
|
|
284
|
+
function isRelated(session: SessionEntry, newGoal: string): boolean {
|
|
285
|
+
// Don't auto-resume sessions older than 4 hours unless interrupted
|
|
286
|
+
const ageMs = Date.now() - new Date(session.updatedAt).getTime();
|
|
287
|
+
if (ageMs > 4 * 60 * 60 * 1_000 && session.status !== "interrupted") return false;
|
|
288
|
+
if (session.status === "completed" && ageMs > 30 * 60 * 1_000) return false;
|
|
289
|
+
|
|
290
|
+
const tokens = (s: string) =>
|
|
291
|
+
s
|
|
292
|
+
.toLowerCase()
|
|
293
|
+
.split(/\W+/)
|
|
294
|
+
.filter((w) => w.length > 3);
|
|
295
|
+
|
|
296
|
+
const prevTokens = new Set([
|
|
297
|
+
...tokens(session.lastGoal),
|
|
298
|
+
...tokens(session.summary ?? ""),
|
|
299
|
+
...(session.allGoals ?? []).flatMap(tokens),
|
|
300
|
+
]);
|
|
301
|
+
|
|
302
|
+
const newTokens = tokens(newGoal);
|
|
303
|
+
if (newTokens.length === 0) return false;
|
|
304
|
+
|
|
305
|
+
const overlap = newTokens.filter((t) => prevTokens.has(t)).length;
|
|
306
|
+
const ratio = overlap / newTokens.length;
|
|
307
|
+
|
|
308
|
+
return ratio >= 0.3; // ≥30% keyword overlap
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ── LLM Summarisation ─────────────────────────────────────────────────────
|
|
312
|
+
|
|
313
|
+
async function summariseSession(
|
|
314
|
+
actions: readonly { type: string; path: string; status: string }[],
|
|
315
|
+
agentResponse: string
|
|
316
|
+
): Promise<string> {
|
|
317
|
+
const actionsSummary = actions
|
|
318
|
+
.slice(0, 40)
|
|
319
|
+
.map((a) => `- ${a.type} ${a.path} [${a.status}]`)
|
|
320
|
+
.join("\n");
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
const result = await generateText({
|
|
324
|
+
model: getAgentModel(),
|
|
325
|
+
stopWhen: stepCountIs(1),
|
|
326
|
+
prompt: [
|
|
327
|
+
"Summarise this coding session in 2-3 concise sentences.",
|
|
328
|
+
"Focus on: what was the goal, what key files were changed, and the outcome.",
|
|
329
|
+
"If there are incomplete tasks, mention them.",
|
|
330
|
+
"",
|
|
331
|
+
"Actions:",
|
|
332
|
+
actionsSummary,
|
|
333
|
+
"",
|
|
334
|
+
"Agent's final response:",
|
|
335
|
+
agentResponse.slice(0, 2_000),
|
|
336
|
+
].join("\n"),
|
|
337
|
+
});
|
|
338
|
+
return result.text.trim();
|
|
339
|
+
} catch {
|
|
340
|
+
const created = actions.filter((a) => a.type === "file_create").length;
|
|
341
|
+
const modified = actions.filter((a) => a.type === "file_modify").length;
|
|
342
|
+
const deleted = actions.filter((a) => a.type === "file_delete").length;
|
|
343
|
+
const approved = actions.filter((a) => a.status === "approved").length;
|
|
344
|
+
return `Session completed. ${created} files created, ${modified} modified, ${deleted} deleted. ${approved} actions approved.`;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ── Formatting helpers ────────────────────────────────────────────────────
|
|
349
|
+
|
|
350
|
+
export function formatSessionLine(s: SessionEntry): string {
|
|
351
|
+
const age = humanAge(s.updatedAt);
|
|
352
|
+
const statusIcon =
|
|
353
|
+
s.status === "completed"
|
|
354
|
+
? C.success("✔")
|
|
355
|
+
: s.status === "interrupted"
|
|
356
|
+
? C.warn("⏸")
|
|
357
|
+
: C.dim("●");
|
|
358
|
+
const modeTag = C.dim(`[${s.mode}]`);
|
|
359
|
+
const pendingTag =
|
|
360
|
+
s.pendingTasks?.length ? C.warn(` (${s.pendingTasks.length} pending)`) : "";
|
|
361
|
+
const goal = s.lastGoal.slice(0, 55) + (s.lastGoal.length > 55 ? "…" : "");
|
|
362
|
+
return `${statusIcon} ${age.padEnd(8)} ${modeTag.padEnd(12)} ${goal}${pendingTag}`;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function humanAge(isoString: string): string {
|
|
366
|
+
const diff = Date.now() - new Date(isoString).getTime();
|
|
367
|
+
const minutes = Math.floor(diff / 60_000);
|
|
368
|
+
if (minutes < 1) return "just now";
|
|
369
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
370
|
+
const hours = Math.floor(minutes / 60);
|
|
371
|
+
if (hours < 24) return `${hours}h ago`;
|
|
372
|
+
const days = Math.floor(hours / 24);
|
|
373
|
+
return `${days}d ago`;
|
|
374
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { tool } from "ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { getSession, listSessions } from "./store";
|
|
4
|
+
import { getSessionHistory, formatSessionLine } from "./session-manager";
|
|
5
|
+
import { buildContextSummary } from "./session-context";
|
|
6
|
+
|
|
7
|
+
export function createSessionTools(workspacePath: string) {
|
|
8
|
+
return {
|
|
9
|
+
/**
|
|
10
|
+
* Lists recent sessions with their status and goal.
|
|
11
|
+
* The agent can use this to understand what work has been done recently.
|
|
12
|
+
*/
|
|
13
|
+
session_status: tool({
|
|
14
|
+
description:
|
|
15
|
+
"Check recent session history — shows mode, goal, outcome, and any pending tasks. " +
|
|
16
|
+
"Use this to understand what was previously worked on before starting new work.",
|
|
17
|
+
inputSchema: z.object({
|
|
18
|
+
limit: z
|
|
19
|
+
.number()
|
|
20
|
+
.int()
|
|
21
|
+
.min(1)
|
|
22
|
+
.max(20)
|
|
23
|
+
.default(5)
|
|
24
|
+
.describe("Number of recent sessions to show (default 5)"),
|
|
25
|
+
}),
|
|
26
|
+
execute: async ({ limit }) => {
|
|
27
|
+
const history = getSessionHistory(workspacePath, limit);
|
|
28
|
+
if (history.length === 0) return "(no previous sessions found)";
|
|
29
|
+
|
|
30
|
+
const lines = history.map((s) => {
|
|
31
|
+
const pending =
|
|
32
|
+
s.pendingTasks?.length
|
|
33
|
+
? `\n ⚠ Pending: ${s.pendingTasks.join("; ")}`
|
|
34
|
+
: "";
|
|
35
|
+
const summary = s.summary ? `\n Summary: ${s.summary}` : "";
|
|
36
|
+
return (
|
|
37
|
+
`• [${s.id}] [${s.mode}] ${s.lastGoal.slice(0, 80)}` +
|
|
38
|
+
`${s.lastGoal.length > 80 ? "…" : ""} (${s.status})` +
|
|
39
|
+
summary +
|
|
40
|
+
pending
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
return `Recent sessions:\n\n${lines.join("\n\n")}`;
|
|
44
|
+
},
|
|
45
|
+
}),
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Retrieves the full context of a previous session, including transcript
|
|
49
|
+
* and pending tasks, so the agent can seamlessly continue that work.
|
|
50
|
+
*/
|
|
51
|
+
session_resume_context: tool({
|
|
52
|
+
description:
|
|
53
|
+
"Get the full context of a previous session to continue its work. " +
|
|
54
|
+
"Returns the conversation transcript, pending tasks, touched files, and a summary. " +
|
|
55
|
+
"Use session_status first to find the right session ID.",
|
|
56
|
+
inputSchema: z.object({
|
|
57
|
+
session_id: z.string().describe("The session ID to resume (from session_status)"),
|
|
58
|
+
transcript_turns: z
|
|
59
|
+
.number()
|
|
60
|
+
.int()
|
|
61
|
+
.min(1)
|
|
62
|
+
.max(30)
|
|
63
|
+
.default(10)
|
|
64
|
+
.describe("How many recent conversation turns to include"),
|
|
65
|
+
}),
|
|
66
|
+
execute: async ({ session_id, transcript_turns }) => {
|
|
67
|
+
const session = getSession(session_id);
|
|
68
|
+
if (!session) return `Session not found: ${session_id}`;
|
|
69
|
+
return buildContextSummary(session, { transcriptTurns: transcript_turns });
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Search for sessions related to a topic or file.
|
|
75
|
+
* Useful for "what did we do with auth.ts last time?"
|
|
76
|
+
*/
|
|
77
|
+
session_search: tool({
|
|
78
|
+
description:
|
|
79
|
+
"Search previous sessions by keyword, file name, or goal. " +
|
|
80
|
+
"Useful for finding prior work on a specific topic before starting.",
|
|
81
|
+
inputSchema: z.object({
|
|
82
|
+
query: z.string().describe("Keyword, file name, or phrase to search for"),
|
|
83
|
+
limit: z.number().int().min(1).max(20).default(10),
|
|
84
|
+
}),
|
|
85
|
+
execute: async ({ query, limit }) => {
|
|
86
|
+
const all = listSessions(workspacePath, limit * 4); // over-fetch, then filter
|
|
87
|
+
const q = query.toLowerCase();
|
|
88
|
+
const matches = all.filter(
|
|
89
|
+
(s) =>
|
|
90
|
+
s.lastGoal.toLowerCase().includes(q) ||
|
|
91
|
+
(s.summary ?? "").toLowerCase().includes(q) ||
|
|
92
|
+
s.touchedFiles.some((f) => f.toLowerCase().includes(q)) ||
|
|
93
|
+
(s.allGoals ?? []).some((g) => g.toLowerCase().includes(q))
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
if (matches.length === 0) return `No sessions found matching "${query}".`;
|
|
97
|
+
|
|
98
|
+
const lines = matches.slice(0, limit).map(
|
|
99
|
+
(s) =>
|
|
100
|
+
`• [${s.id}] ${s.lastGoal.slice(0, 60)} (${s.status})` +
|
|
101
|
+
(s.touchedFiles.some((f) => f.toLowerCase().includes(q))
|
|
102
|
+
? `\n Files: ${s.touchedFiles.filter((f) => f.toLowerCase().includes(q)).join(", ")}`
|
|
103
|
+
: "")
|
|
104
|
+
);
|
|
105
|
+
return `Sessions matching "${query}":\n\n${lines.join("\n\n")}`;
|
|
106
|
+
},
|
|
107
|
+
}),
|
|
108
|
+
};
|
|
109
|
+
}
|