@aion0/forge 0.4.16 → 0.5.1
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 +27 -2
- package/RELEASE_NOTES.md +21 -14
- package/app/api/agents/route.ts +17 -0
- package/app/api/delivery/[id]/route.ts +62 -0
- package/app/api/delivery/route.ts +40 -0
- package/app/api/mobile-chat/route.ts +13 -7
- package/app/api/monitor/route.ts +10 -6
- package/app/api/pipelines/[id]/route.ts +16 -3
- package/app/api/tasks/route.ts +2 -1
- package/app/api/workspace/[id]/agents/route.ts +35 -0
- package/app/api/workspace/[id]/memory/route.ts +23 -0
- package/app/api/workspace/[id]/smith/route.ts +22 -0
- package/app/api/workspace/[id]/stream/route.ts +28 -0
- package/app/api/workspace/route.ts +100 -0
- package/app/global-error.tsx +10 -4
- package/app/icon.ico +0 -0
- package/app/layout.tsx +2 -2
- package/app/login/LoginForm.tsx +96 -0
- package/app/login/page.tsx +7 -98
- package/app/page.tsx +2 -2
- package/bin/forge-server.mjs +13 -1
- package/check-forge-status.sh +9 -0
- package/components/ConversationEditor.tsx +411 -0
- package/components/ConversationGraphView.tsx +347 -0
- package/components/ConversationTerminalView.tsx +303 -0
- package/components/Dashboard.tsx +36 -39
- package/components/DashboardWrapper.tsx +9 -0
- package/components/DeliveryFlowEditor.tsx +491 -0
- package/components/DeliveryList.tsx +230 -0
- package/components/DeliveryWorkspace.tsx +589 -0
- package/components/DocTerminal.tsx +10 -2
- package/components/DocsViewer.tsx +10 -2
- package/components/HelpTerminal.tsx +11 -6
- package/components/InlinePipelineView.tsx +111 -0
- package/components/MobileView.tsx +20 -0
- package/components/MonitorPanel.tsx +9 -4
- package/components/NewTaskModal.tsx +32 -0
- package/components/PipelineEditor.tsx +49 -6
- package/components/PipelineView.tsx +482 -64
- package/components/ProjectDetail.tsx +314 -56
- package/components/ProjectManager.tsx +49 -4
- package/components/SessionView.tsx +27 -13
- package/components/SettingsModal.tsx +790 -124
- package/components/SkillsPanel.tsx +31 -8
- package/components/TaskBoard.tsx +3 -0
- package/components/WebTerminal.tsx +257 -43
- package/components/WorkspaceTree.tsx +221 -0
- package/components/WorkspaceView.tsx +2245 -0
- package/install.sh +2 -2
- package/lib/agents/claude-adapter.ts +104 -0
- package/lib/agents/generic-adapter.ts +64 -0
- package/lib/agents/index.ts +242 -0
- package/lib/agents/types.ts +70 -0
- package/lib/artifacts.ts +106 -0
- package/lib/delivery.ts +787 -0
- package/lib/forge-skills/forge-inbox.md +37 -0
- package/lib/forge-skills/forge-send.md +40 -0
- package/lib/forge-skills/forge-status.md +32 -0
- package/lib/forge-skills/forge-workspace-sync.md +37 -0
- package/lib/help-docs/00-overview.md +7 -1
- package/lib/help-docs/01-settings.md +159 -2
- package/lib/help-docs/05-pipelines.md +89 -0
- package/lib/help-docs/07-projects.md +35 -1
- package/lib/help-docs/11-workspace.md +254 -0
- package/lib/help-docs/CLAUDE.md +7 -2
- package/lib/init.ts +60 -10
- package/lib/pipeline.ts +537 -1
- package/lib/settings.ts +115 -22
- package/lib/skills.ts +249 -372
- package/lib/task-manager.ts +113 -33
- package/lib/telegram-bot.ts +33 -1
- package/lib/workspace/__tests__/state-machine.test.ts +388 -0
- package/lib/workspace/__tests__/workspace.test.ts +311 -0
- package/lib/workspace/agent-bus.ts +416 -0
- package/lib/workspace/agent-worker.ts +667 -0
- package/lib/workspace/backends/api-backend.ts +262 -0
- package/lib/workspace/backends/cli-backend.ts +479 -0
- package/lib/workspace/index.ts +82 -0
- package/lib/workspace/manager.ts +136 -0
- package/lib/workspace/orchestrator.ts +1914 -0
- package/lib/workspace/persistence.ts +310 -0
- package/lib/workspace/presets.ts +170 -0
- package/lib/workspace/skill-installer.ts +188 -0
- package/lib/workspace/smith-memory.ts +498 -0
- package/lib/workspace/types.ts +231 -0
- package/lib/workspace/watch-manager.ts +288 -0
- package/lib/workspace-standalone.ts +814 -0
- package/middleware.ts +1 -0
- package/next-env.d.ts +1 -1
- package/package.json +4 -1
- package/src/config/index.ts +12 -1
- package/src/core/db/database.ts +1 -0
- package/start.sh +7 -0
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smith Memory — persistent per-agent memory system.
|
|
3
|
+
*
|
|
4
|
+
* Inspired by claude-mem (https://github.com/thedotmack/claude-mem):
|
|
5
|
+
* - Per-step observation capture (not just end-of-run)
|
|
6
|
+
* - 6 observation types: decision, bugfix, feature, refactor, discovery, change
|
|
7
|
+
* - Session summary at end (request/investigated/learned/completed/next_steps)
|
|
8
|
+
* - Progressive disclosure: recent entries full detail, older ones title-only
|
|
9
|
+
* - Structured observations: title, facts, concepts, files_read, files_modified
|
|
10
|
+
*
|
|
11
|
+
* Storage: ~/.forge/workspaces/{workspace-id}/agents/{agent-id}/memory.json
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
15
|
+
import { writeFile, mkdir } from 'node:fs/promises';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { homedir } from 'node:os';
|
|
18
|
+
|
|
19
|
+
// ─── Types ───────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
export type ObservationType = 'decision' | 'bugfix' | 'feature' | 'refactor' | 'discovery' | 'change';
|
|
22
|
+
|
|
23
|
+
export interface Observation {
|
|
24
|
+
id: string; // unique per observation
|
|
25
|
+
timestamp: number;
|
|
26
|
+
type: ObservationType;
|
|
27
|
+
title: string; // one-line summary
|
|
28
|
+
subtitle?: string; // additional detail
|
|
29
|
+
facts?: string[]; // extracted structured facts
|
|
30
|
+
concepts?: string[]; // abstract tags/categories
|
|
31
|
+
filesRead?: string[];
|
|
32
|
+
filesModified?: string[];
|
|
33
|
+
stepLabel?: string; // which step produced this
|
|
34
|
+
detail?: string; // full detail (pruned in older entries)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SessionSummary {
|
|
38
|
+
timestamp: number;
|
|
39
|
+
request: string; // what was asked
|
|
40
|
+
investigated: string; // what was explored
|
|
41
|
+
learned: string; // key insights
|
|
42
|
+
completed: string; // what was finished
|
|
43
|
+
nextSteps: string; // remaining work
|
|
44
|
+
filesRead: string[];
|
|
45
|
+
filesModified: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface SmithMemory {
|
|
49
|
+
agentId: string;
|
|
50
|
+
agentLabel: string;
|
|
51
|
+
role: string;
|
|
52
|
+
observations: Observation[];
|
|
53
|
+
sessions: SessionSummary[];
|
|
54
|
+
lastUpdated: number;
|
|
55
|
+
version: number; // schema version for migration
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ─── Constants ───────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
const WORKSPACES_ROOT = join(homedir(), '.forge', 'workspaces');
|
|
61
|
+
const MAX_OBSERVATIONS = 100;
|
|
62
|
+
const MAX_SESSIONS = 20;
|
|
63
|
+
const FULL_DETAIL_COUNT = 15; // most recent N observations keep full detail
|
|
64
|
+
const SCHEMA_VERSION = 2;
|
|
65
|
+
|
|
66
|
+
const TYPE_ICONS: Record<ObservationType, string> = {
|
|
67
|
+
decision: '🎯',
|
|
68
|
+
bugfix: '🐛',
|
|
69
|
+
feature: '✨',
|
|
70
|
+
refactor: '♻️',
|
|
71
|
+
discovery: '🔍',
|
|
72
|
+
change: '📝',
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// ─── Paths ───────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
function memoryDir(workspaceId: string, agentId: string): string {
|
|
78
|
+
return join(WORKSPACES_ROOT, workspaceId, 'agents', agentId);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function memoryFile(workspaceId: string, agentId: string): string {
|
|
82
|
+
return join(memoryDir(workspaceId, agentId), 'memory.json');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ─── CRUD ────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
export function loadMemory(workspaceId: string, agentId: string): SmithMemory | null {
|
|
88
|
+
const file = memoryFile(workspaceId, agentId);
|
|
89
|
+
if (!existsSync(file)) return null;
|
|
90
|
+
try {
|
|
91
|
+
const raw = JSON.parse(readFileSync(file, 'utf-8'));
|
|
92
|
+
// Migrate from v1 if needed
|
|
93
|
+
if (!raw.version || raw.version < SCHEMA_VERSION) {
|
|
94
|
+
return migrateMemory(raw);
|
|
95
|
+
}
|
|
96
|
+
return raw;
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function saveMemory(workspaceId: string, agentId: string, memory: SmithMemory): Promise<void> {
|
|
103
|
+
const dir = memoryDir(workspaceId, agentId);
|
|
104
|
+
await mkdir(dir, { recursive: true });
|
|
105
|
+
await writeFile(memoryFile(workspaceId, agentId), JSON.stringify(memory, null, 2), 'utf-8');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function createMemory(agentId: string, agentLabel: string, role: string): SmithMemory {
|
|
109
|
+
return {
|
|
110
|
+
agentId,
|
|
111
|
+
agentLabel,
|
|
112
|
+
role,
|
|
113
|
+
observations: [],
|
|
114
|
+
sessions: [],
|
|
115
|
+
lastUpdated: Date.now(),
|
|
116
|
+
version: SCHEMA_VERSION,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Migrate from v1 (entries-based) to v2 (observations + sessions) */
|
|
121
|
+
function migrateMemory(raw: any): SmithMemory {
|
|
122
|
+
const observations: Observation[] = (raw.entries || []).map((e: any, i: number) => ({
|
|
123
|
+
id: `migrated-${i}`,
|
|
124
|
+
timestamp: e.timestamp || Date.now(),
|
|
125
|
+
type: mapLegacyType(e.type),
|
|
126
|
+
title: e.summary || '',
|
|
127
|
+
filesModified: e.files,
|
|
128
|
+
detail: e.details,
|
|
129
|
+
}));
|
|
130
|
+
return {
|
|
131
|
+
agentId: raw.agentId || '',
|
|
132
|
+
agentLabel: raw.agentLabel || '',
|
|
133
|
+
role: raw.role || '',
|
|
134
|
+
observations,
|
|
135
|
+
sessions: [],
|
|
136
|
+
lastUpdated: Date.now(),
|
|
137
|
+
version: SCHEMA_VERSION,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function mapLegacyType(t: string): ObservationType {
|
|
142
|
+
const map: Record<string, ObservationType> = {
|
|
143
|
+
task_completed: 'feature',
|
|
144
|
+
artifact_produced: 'change',
|
|
145
|
+
decision_made: 'decision',
|
|
146
|
+
issue_found: 'bugfix',
|
|
147
|
+
context_learned: 'discovery',
|
|
148
|
+
update: 'change',
|
|
149
|
+
};
|
|
150
|
+
return map[t] || 'change';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ─── Observation capture ─────────────────────────────────
|
|
154
|
+
|
|
155
|
+
/** Add a single observation after a step completes */
|
|
156
|
+
export async function addObservation(
|
|
157
|
+
workspaceId: string,
|
|
158
|
+
agentId: string,
|
|
159
|
+
agentLabel: string,
|
|
160
|
+
role: string,
|
|
161
|
+
obs: Omit<Observation, 'id' | 'timestamp'>,
|
|
162
|
+
): Promise<void> {
|
|
163
|
+
let memory = loadMemory(workspaceId, agentId) || createMemory(agentId, agentLabel, role);
|
|
164
|
+
memory.agentLabel = agentLabel;
|
|
165
|
+
memory.role = role;
|
|
166
|
+
|
|
167
|
+
memory.observations.push({
|
|
168
|
+
...obs,
|
|
169
|
+
id: `obs-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
170
|
+
timestamp: Date.now(),
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
pruneObservations(memory);
|
|
174
|
+
memory.lastUpdated = Date.now();
|
|
175
|
+
await saveMemory(workspaceId, agentId, memory);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Add session summary when agent finishes all steps */
|
|
179
|
+
export async function addSessionSummary(
|
|
180
|
+
workspaceId: string,
|
|
181
|
+
agentId: string,
|
|
182
|
+
summary: Omit<SessionSummary, 'timestamp'>,
|
|
183
|
+
): Promise<void> {
|
|
184
|
+
let memory = loadMemory(workspaceId, agentId);
|
|
185
|
+
if (!memory) return;
|
|
186
|
+
|
|
187
|
+
memory.sessions.push({ ...summary, timestamp: Date.now() });
|
|
188
|
+
|
|
189
|
+
if (memory.sessions.length > MAX_SESSIONS) {
|
|
190
|
+
memory.sessions = memory.sessions.slice(-MAX_SESSIONS);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
memory.lastUpdated = Date.now();
|
|
194
|
+
await saveMemory(workspaceId, agentId, memory);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ─── Progressive disclosure pruning ──────────────────────
|
|
198
|
+
|
|
199
|
+
function pruneObservations(memory: SmithMemory): void {
|
|
200
|
+
if (memory.observations.length <= MAX_OBSERVATIONS) return;
|
|
201
|
+
|
|
202
|
+
// Keep most recent MAX_OBSERVATIONS
|
|
203
|
+
memory.observations = memory.observations.slice(-MAX_OBSERVATIONS);
|
|
204
|
+
|
|
205
|
+
// Compact older entries: remove detail, keep title + type + files
|
|
206
|
+
const compactBoundary = memory.observations.length - FULL_DETAIL_COUNT;
|
|
207
|
+
for (let i = 0; i < compactBoundary; i++) {
|
|
208
|
+
const obs = memory.observations[i];
|
|
209
|
+
delete obs.detail;
|
|
210
|
+
delete obs.subtitle;
|
|
211
|
+
delete obs.facts;
|
|
212
|
+
// Keep title, type, concepts, files — enough for context
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ─── Format for injection into agent context ─────────────
|
|
217
|
+
|
|
218
|
+
export function formatMemoryForPrompt(memory: SmithMemory | null, maxTokenEstimate = 3000): string {
|
|
219
|
+
if (!memory) return '';
|
|
220
|
+
if (memory.observations.length === 0 && memory.sessions.length === 0) return '';
|
|
221
|
+
|
|
222
|
+
const lines: string[] = [];
|
|
223
|
+
lines.push(`## Smith Memory — ${memory.agentLabel}`);
|
|
224
|
+
lines.push(`Role: ${memory.role}`);
|
|
225
|
+
lines.push(`Memory entries: ${memory.observations.length} observations, ${memory.sessions.length} sessions\n`);
|
|
226
|
+
|
|
227
|
+
// Last session summary (most important for continuity)
|
|
228
|
+
const lastSession = memory.sessions[memory.sessions.length - 1];
|
|
229
|
+
if (lastSession) {
|
|
230
|
+
lines.push('### Last Session:');
|
|
231
|
+
if (lastSession.request) lines.push(`- **Request:** ${lastSession.request}`);
|
|
232
|
+
if (lastSession.completed) lines.push(`- **Completed:** ${lastSession.completed}`);
|
|
233
|
+
if (lastSession.learned) lines.push(`- **Learned:** ${lastSession.learned}`);
|
|
234
|
+
if (lastSession.nextSteps) lines.push(`- **Next steps:** ${lastSession.nextSteps}`);
|
|
235
|
+
if (lastSession.filesModified.length > 0) {
|
|
236
|
+
lines.push(`- **Files modified:** ${lastSession.filesModified.join(', ')}`);
|
|
237
|
+
}
|
|
238
|
+
lines.push('');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Recent observations (full detail for FULL_DETAIL_COUNT, title-only for older)
|
|
242
|
+
if (memory.observations.length > 0) {
|
|
243
|
+
lines.push('### Recent Work:');
|
|
244
|
+
|
|
245
|
+
const obs = memory.observations;
|
|
246
|
+
const compactBoundary = Math.max(0, obs.length - FULL_DETAIL_COUNT);
|
|
247
|
+
|
|
248
|
+
// Compact older entries (title only, grouped by type)
|
|
249
|
+
if (compactBoundary > 0) {
|
|
250
|
+
const older = obs.slice(0, compactBoundary);
|
|
251
|
+
const byType = new Map<ObservationType, string[]>();
|
|
252
|
+
for (const o of older) {
|
|
253
|
+
if (!byType.has(o.type)) byType.set(o.type, []);
|
|
254
|
+
byType.get(o.type)!.push(o.title);
|
|
255
|
+
}
|
|
256
|
+
for (const [type, titles] of byType) {
|
|
257
|
+
lines.push(`${TYPE_ICONS[type]} **${type}** (${titles.length} earlier):`);
|
|
258
|
+
// Show only last 3 titles per type to save tokens
|
|
259
|
+
for (const t of titles.slice(-3)) {
|
|
260
|
+
lines.push(` - ${t}`);
|
|
261
|
+
}
|
|
262
|
+
if (titles.length > 3) lines.push(` - ... and ${titles.length - 3} more`);
|
|
263
|
+
}
|
|
264
|
+
lines.push('');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Recent entries with full detail
|
|
268
|
+
const recent = obs.slice(compactBoundary);
|
|
269
|
+
for (const o of recent) {
|
|
270
|
+
const icon = TYPE_ICONS[o.type] || '📝';
|
|
271
|
+
const time = new Date(o.timestamp).toLocaleString();
|
|
272
|
+
let line = `${icon} **${o.title}**`;
|
|
273
|
+
if (o.stepLabel) line += ` (${o.stepLabel})`;
|
|
274
|
+
lines.push(line);
|
|
275
|
+
if (o.subtitle) lines.push(` ${o.subtitle}`);
|
|
276
|
+
if (o.facts && o.facts.length > 0) {
|
|
277
|
+
for (const f of o.facts) lines.push(` - ${f}`);
|
|
278
|
+
}
|
|
279
|
+
if (o.filesModified && o.filesModified.length > 0) {
|
|
280
|
+
lines.push(` Files: ${o.filesModified.join(', ')}`);
|
|
281
|
+
}
|
|
282
|
+
if (o.detail) lines.push(` ${o.detail}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
lines.push('\n---');
|
|
287
|
+
lines.push('**Instructions:** Use this memory to work incrementally. Do NOT redo completed work unless explicitly asked. Focus on what is new or changed. Update your understanding based on the latest observations.');
|
|
288
|
+
|
|
289
|
+
return lines.join('\n');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ─── Format for UI display ───────────────────────────────
|
|
293
|
+
|
|
294
|
+
export interface MemoryDisplayEntry {
|
|
295
|
+
id: string;
|
|
296
|
+
timestamp: number;
|
|
297
|
+
type: ObservationType | 'session';
|
|
298
|
+
icon: string;
|
|
299
|
+
title: string;
|
|
300
|
+
subtitle?: string;
|
|
301
|
+
facts?: string[];
|
|
302
|
+
files?: string[];
|
|
303
|
+
detail?: string;
|
|
304
|
+
isCompact: boolean;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function formatMemoryForDisplay(memory: SmithMemory | null): MemoryDisplayEntry[] {
|
|
308
|
+
if (!memory) return [];
|
|
309
|
+
|
|
310
|
+
const entries: MemoryDisplayEntry[] = [];
|
|
311
|
+
|
|
312
|
+
// Add session summaries
|
|
313
|
+
for (const s of memory.sessions) {
|
|
314
|
+
entries.push({
|
|
315
|
+
id: `session-${s.timestamp}`,
|
|
316
|
+
timestamp: s.timestamp,
|
|
317
|
+
type: 'session',
|
|
318
|
+
icon: '📋',
|
|
319
|
+
title: `Session: ${s.request || 'Work session'}`,
|
|
320
|
+
subtitle: s.completed,
|
|
321
|
+
facts: [
|
|
322
|
+
s.learned && `Learned: ${s.learned}`,
|
|
323
|
+
s.nextSteps && `Next: ${s.nextSteps}`,
|
|
324
|
+
].filter(Boolean) as string[],
|
|
325
|
+
files: [...(s.filesRead || []), ...(s.filesModified || [])],
|
|
326
|
+
isCompact: false,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Add observations
|
|
331
|
+
const compactBoundary = Math.max(0, memory.observations.length - FULL_DETAIL_COUNT);
|
|
332
|
+
for (let i = 0; i < memory.observations.length; i++) {
|
|
333
|
+
const o = memory.observations[i];
|
|
334
|
+
entries.push({
|
|
335
|
+
id: o.id,
|
|
336
|
+
timestamp: o.timestamp,
|
|
337
|
+
type: o.type,
|
|
338
|
+
icon: TYPE_ICONS[o.type] || '📝',
|
|
339
|
+
title: o.title,
|
|
340
|
+
subtitle: o.subtitle,
|
|
341
|
+
facts: o.facts,
|
|
342
|
+
files: [...(o.filesRead || []), ...(o.filesModified || [])],
|
|
343
|
+
detail: o.detail,
|
|
344
|
+
isCompact: i < compactBoundary,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Sort by timestamp descending (most recent first)
|
|
349
|
+
entries.sort((a, b) => b.timestamp - a.timestamp);
|
|
350
|
+
return entries;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ─── Parse step result into observations ─────────────────
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Parse a step's output into structured observations.
|
|
357
|
+
* Uses heuristic parsing (no LLM call needed).
|
|
358
|
+
*/
|
|
359
|
+
export function parseStepToObservations(
|
|
360
|
+
stepLabel: string,
|
|
361
|
+
stepResult: string,
|
|
362
|
+
artifacts: { path?: string; summary?: string }[],
|
|
363
|
+
): Observation[] {
|
|
364
|
+
const now = Date.now();
|
|
365
|
+
const observations: Observation[] = [];
|
|
366
|
+
const id = () => `obs-${now}-${Math.random().toString(36).slice(2, 6)}`;
|
|
367
|
+
|
|
368
|
+
// Extract file references from result
|
|
369
|
+
const filePatterns = stepResult.match(/(?:wrote|created|modified|updated|edited|added|deleted)\s+[`"]?([a-zA-Z0-9_\-./]+\.[a-zA-Z]+)[`"]?/gi) || [];
|
|
370
|
+
const filesModified = [
|
|
371
|
+
...artifacts.filter(a => a.path).map(a => a.path!),
|
|
372
|
+
...filePatterns.map(m => {
|
|
373
|
+
const match = m.match(/[`"]?([a-zA-Z0-9_\-./]+\.[a-zA-Z]+)[`"]?$/);
|
|
374
|
+
return match ? match[1] : '';
|
|
375
|
+
}).filter(Boolean),
|
|
376
|
+
];
|
|
377
|
+
const uniqueFiles = [...new Set(filesModified)];
|
|
378
|
+
|
|
379
|
+
// Detect observation type from content
|
|
380
|
+
const lower = stepResult.toLowerCase();
|
|
381
|
+
let type: ObservationType = 'change';
|
|
382
|
+
if (lower.includes('fix') || lower.includes('bug') || lower.includes('error') || lower.includes('issue')) {
|
|
383
|
+
type = 'bugfix';
|
|
384
|
+
} else if (lower.includes('implement') || lower.includes('feature') || lower.includes('add') || lower.includes('create')) {
|
|
385
|
+
type = 'feature';
|
|
386
|
+
} else if (lower.includes('refactor') || lower.includes('restructur') || lower.includes('cleanup') || lower.includes('reorganiz')) {
|
|
387
|
+
type = 'refactor';
|
|
388
|
+
} else if (lower.includes('decide') || lower.includes('decision') || lower.includes('chose') || lower.includes('architecture')) {
|
|
389
|
+
type = 'decision';
|
|
390
|
+
} else if (lower.includes('discover') || lower.includes('found') || lower.includes('learn') || lower.includes('analyz') || lower.includes('review')) {
|
|
391
|
+
type = 'discovery';
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Extract key sentences (first meaningful lines)
|
|
395
|
+
const sentences = stepResult
|
|
396
|
+
.split(/[.\n]/)
|
|
397
|
+
.map(s => s.trim())
|
|
398
|
+
.filter(s => s.length > 20 && s.length < 300)
|
|
399
|
+
.slice(0, 5);
|
|
400
|
+
|
|
401
|
+
const title = sentences[0]
|
|
402
|
+
? sentences[0].slice(0, 120)
|
|
403
|
+
: `${stepLabel} completed`;
|
|
404
|
+
|
|
405
|
+
observations.push({
|
|
406
|
+
id: id(),
|
|
407
|
+
timestamp: now,
|
|
408
|
+
type,
|
|
409
|
+
title,
|
|
410
|
+
subtitle: sentences[1]?.slice(0, 150),
|
|
411
|
+
stepLabel,
|
|
412
|
+
facts: sentences.slice(1, 4).map(s => s.slice(0, 150)),
|
|
413
|
+
filesModified: uniqueFiles.length > 0 ? uniqueFiles : undefined,
|
|
414
|
+
detail: stepResult.length > 500 ? stepResult.slice(0, 500) + '...' : stepResult,
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// Add separate artifact observations
|
|
418
|
+
for (const artifact of artifacts) {
|
|
419
|
+
if (artifact.path) {
|
|
420
|
+
observations.push({
|
|
421
|
+
id: id(),
|
|
422
|
+
timestamp: now,
|
|
423
|
+
type: 'change',
|
|
424
|
+
title: `Produced ${artifact.path}`,
|
|
425
|
+
subtitle: artifact.summary,
|
|
426
|
+
stepLabel,
|
|
427
|
+
filesModified: [artifact.path],
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return observations;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Build a session summary from all step results.
|
|
437
|
+
*/
|
|
438
|
+
export function buildSessionSummary(
|
|
439
|
+
stepLabels: string[],
|
|
440
|
+
stepResults: string[],
|
|
441
|
+
allArtifacts: { path?: string }[],
|
|
442
|
+
): Omit<SessionSummary, 'timestamp'> {
|
|
443
|
+
const allFiles = allArtifacts.filter(a => a.path).map(a => a.path!);
|
|
444
|
+
const uniqueFiles = [...new Set(allFiles)];
|
|
445
|
+
|
|
446
|
+
// Extract key info from results
|
|
447
|
+
const allText = stepResults.join('\n\n');
|
|
448
|
+
const sentences = allText
|
|
449
|
+
.split(/[.\n]/)
|
|
450
|
+
.map(s => s.trim())
|
|
451
|
+
.filter(s => s.length > 20 && s.length < 300);
|
|
452
|
+
|
|
453
|
+
// Simple heuristic extraction
|
|
454
|
+
const request = `Execute steps: ${stepLabels.join(' → ')}`;
|
|
455
|
+
const completed = sentences.slice(0, 3).join('. ') || `Completed ${stepLabels.length} steps`;
|
|
456
|
+
const learned = sentences.find(s =>
|
|
457
|
+
/learn|discover|found|realiz|understand|insight/i.test(s)
|
|
458
|
+
) || '';
|
|
459
|
+
const nextSteps = sentences.find(s =>
|
|
460
|
+
/next|todo|remaining|should|need to|follow.?up/i.test(s)
|
|
461
|
+
) || '';
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
request,
|
|
465
|
+
investigated: `Worked through ${stepLabels.length} steps`,
|
|
466
|
+
learned: learned.slice(0, 200),
|
|
467
|
+
completed: completed.slice(0, 300),
|
|
468
|
+
nextSteps: nextSteps.slice(0, 200),
|
|
469
|
+
filesRead: [],
|
|
470
|
+
filesModified: uniqueFiles,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ─── API endpoint helper ─────────────────────────────────
|
|
475
|
+
|
|
476
|
+
/** Get memory stats for display */
|
|
477
|
+
export function getMemoryStats(memory: SmithMemory | null): {
|
|
478
|
+
totalObservations: number;
|
|
479
|
+
totalSessions: number;
|
|
480
|
+
lastUpdated: number | null;
|
|
481
|
+
typeBreakdown: Record<string, number>;
|
|
482
|
+
} {
|
|
483
|
+
if (!memory) {
|
|
484
|
+
return { totalObservations: 0, totalSessions: 0, lastUpdated: null, typeBreakdown: {} };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const typeBreakdown: Record<string, number> = {};
|
|
488
|
+
for (const o of memory.observations) {
|
|
489
|
+
typeBreakdown[o.type] = (typeBreakdown[o.type] || 0) + 1;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return {
|
|
493
|
+
totalObservations: memory.observations.length,
|
|
494
|
+
totalSessions: memory.sessions.length,
|
|
495
|
+
lastUpdated: memory.lastUpdated,
|
|
496
|
+
typeBreakdown,
|
|
497
|
+
};
|
|
498
|
+
}
|