gekto 0.0.8 → 0.0.9
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/dist/agents/HeadlessAgent.js +54 -6
- package/dist/agents/agentPool.js +99 -8
- package/dist/agents/agentWebSocket.js +721 -110
- package/dist/agents/gektoPersistent.js +176 -32
- package/dist/agents/gektoTools.js +383 -218
- package/dist/agents/types.js +2 -0
- package/dist/entityStore.js +499 -0
- package/dist/proxy.js +3 -85
- package/dist/state.js +251 -0
- package/dist/widget/gekto-widget.iife.js +295 -296
- package/package.json +1 -1
- package/dist/widget/gekto-widget.css +0 -1
|
@@ -35,9 +35,15 @@ export class HeadlessAgent {
|
|
|
35
35
|
isRunning() {
|
|
36
36
|
return this.currentProc !== null && !this.currentProc.killed;
|
|
37
37
|
}
|
|
38
|
-
async send(message, callbacks) {
|
|
38
|
+
async send(message, callbacks, imagePaths) {
|
|
39
|
+
// Append image file paths to the message so Claude can read them
|
|
40
|
+
let finalMessage = message;
|
|
41
|
+
if (imagePaths && imagePaths.length > 0) {
|
|
42
|
+
const imageRefs = imagePaths.map(p => ` - ${p}`).join('\n');
|
|
43
|
+
finalMessage += `\n\n[The user attached ${imagePaths.length} image(s). Use the Read tool to view them:\n${imageRefs}]`;
|
|
44
|
+
}
|
|
39
45
|
const args = [
|
|
40
|
-
'-p',
|
|
46
|
+
'-p', finalMessage,
|
|
41
47
|
'--output-format', 'stream-json',
|
|
42
48
|
'--verbose',
|
|
43
49
|
'--model', 'claude-opus-4-5-20251101',
|
|
@@ -71,6 +77,8 @@ export class HeadlessAgent {
|
|
|
71
77
|
let buffer = '';
|
|
72
78
|
let lastResult = null;
|
|
73
79
|
let currentTool = null;
|
|
80
|
+
const toolUseIdToName = new Map();
|
|
81
|
+
const streamState = { receivedDeltas: false };
|
|
74
82
|
proc.stdout.on('data', (data) => {
|
|
75
83
|
buffer += data.toString();
|
|
76
84
|
const lines = buffer.split('\n');
|
|
@@ -80,7 +88,7 @@ export class HeadlessAgent {
|
|
|
80
88
|
continue;
|
|
81
89
|
try {
|
|
82
90
|
const event = JSON.parse(line);
|
|
83
|
-
this.processStreamEvent(event, callbacks, (tool) => {
|
|
91
|
+
this.processStreamEvent(event, callbacks, toolUseIdToName, streamState, (tool) => {
|
|
84
92
|
currentTool = tool;
|
|
85
93
|
}, () => {
|
|
86
94
|
currentTool = null;
|
|
@@ -125,7 +133,7 @@ export class HeadlessAgent {
|
|
|
125
133
|
proc.on('error', reject);
|
|
126
134
|
});
|
|
127
135
|
}
|
|
128
|
-
processStreamEvent(event, callbacks, setCurrentTool, clearCurrentTool) {
|
|
136
|
+
processStreamEvent(event, callbacks, toolUseIdToName, streamState, setCurrentTool, clearCurrentTool) {
|
|
129
137
|
if (event.type === 'assistant' && event.message) {
|
|
130
138
|
const message = event.message;
|
|
131
139
|
if (message.content) {
|
|
@@ -133,6 +141,10 @@ export class HeadlessAgent {
|
|
|
133
141
|
if (block.type === 'tool_use' && block.name) {
|
|
134
142
|
setCurrentTool?.(block.name);
|
|
135
143
|
callbacks?.onToolStart?.(block.name, block.input);
|
|
144
|
+
// Track tool name by ID for tool_result matching
|
|
145
|
+
if (block.id) {
|
|
146
|
+
toolUseIdToName?.set(block.id, block.name);
|
|
147
|
+
}
|
|
136
148
|
// Track file changes for Write/Edit tools
|
|
137
149
|
if ((block.name === 'Write' || block.name === 'Edit') && block.id && block.input?.file_path) {
|
|
138
150
|
const filePath = String(block.input.file_path);
|
|
@@ -144,19 +156,55 @@ export class HeadlessAgent {
|
|
|
144
156
|
});
|
|
145
157
|
}
|
|
146
158
|
}
|
|
147
|
-
//
|
|
148
|
-
if (block.type === 'text' && block.text) {
|
|
159
|
+
// Fallback: emit full text block only if no streaming deltas were received
|
|
160
|
+
if (block.type === 'text' && block.text && !streamState?.receivedDeltas) {
|
|
149
161
|
callbacks?.onText?.(block.text);
|
|
150
162
|
}
|
|
163
|
+
// Thinking block from assistant event (fallback when no thinking_delta)
|
|
164
|
+
if (block.type === 'thinking' && block.thinking && !streamState?.receivedDeltas) {
|
|
165
|
+
callbacks?.onThinking?.(block.thinking);
|
|
166
|
+
}
|
|
151
167
|
}
|
|
152
168
|
}
|
|
153
169
|
}
|
|
170
|
+
// Streaming deltas — character-by-character text and thinking
|
|
171
|
+
if (event.type === 'content_block_delta') {
|
|
172
|
+
const delta = event.delta;
|
|
173
|
+
if (delta?.type === 'text_delta' && delta.text) {
|
|
174
|
+
if (streamState)
|
|
175
|
+
streamState.receivedDeltas = true;
|
|
176
|
+
callbacks?.onText?.(delta.text);
|
|
177
|
+
}
|
|
178
|
+
else if (delta?.type === 'thinking_delta' && delta.thinking) {
|
|
179
|
+
if (streamState)
|
|
180
|
+
streamState.receivedDeltas = true;
|
|
181
|
+
callbacks?.onThinking?.(delta.thinking);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
154
184
|
if (event.type === 'user' && event.message) {
|
|
155
185
|
const message = event.message;
|
|
156
186
|
if (message.content) {
|
|
157
187
|
for (const block of message.content) {
|
|
158
188
|
if (block.type === 'tool_result') {
|
|
159
189
|
clearCurrentTool?.();
|
|
190
|
+
// Extract tool result content
|
|
191
|
+
if (block.tool_use_id && block.content != null) {
|
|
192
|
+
const toolName = toolUseIdToName?.get(block.tool_use_id) || 'unknown';
|
|
193
|
+
let resultText = '';
|
|
194
|
+
if (typeof block.content === 'string') {
|
|
195
|
+
resultText = block.content;
|
|
196
|
+
}
|
|
197
|
+
else if (Array.isArray(block.content)) {
|
|
198
|
+
// content is array of {type: 'text', text: '...'} blocks
|
|
199
|
+
resultText = block.content
|
|
200
|
+
.filter(c => c.type === 'text' && c.text)
|
|
201
|
+
.map(c => c.text)
|
|
202
|
+
.join('\n');
|
|
203
|
+
}
|
|
204
|
+
if (resultText) {
|
|
205
|
+
callbacks?.onToolResult?.(toolName, resultText, block.tool_use_id);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
160
208
|
// Complete file change tracking
|
|
161
209
|
if (block.tool_use_id && this.pendingFileChanges.has(block.tool_use_id)) {
|
|
162
210
|
const pending = this.pendingFileChanges.get(block.tool_use_id);
|
package/dist/agents/agentPool.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { writeFileSync, unlinkSync, existsSync, mkdirSync } from 'fs';
|
|
3
3
|
import { dirname } from 'path';
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
5
|
+
import { tmpdir } from 'os';
|
|
4
6
|
import { HeadlessAgent } from './HeadlessAgent.js';
|
|
7
|
+
import { getState, mutate, broadcastFileChange, broadcastAgent } from '../state.js';
|
|
5
8
|
// Per-lizard sessions
|
|
6
9
|
const sessions = new Map();
|
|
7
10
|
// Summarize tool input for display
|
|
@@ -100,14 +103,51 @@ export function getQueueLength(lizardId) {
|
|
|
100
103
|
function safeSend(session, data) {
|
|
101
104
|
const ws = session.currentWs;
|
|
102
105
|
if (ws && ws.readyState === ws.OPEN) {
|
|
106
|
+
const type = data.type;
|
|
107
|
+
const lizardId = data.lizardId;
|
|
108
|
+
// Log outgoing messages (skip noisy streaming deltas)
|
|
109
|
+
if (type !== 'text' && type !== 'thinking') {
|
|
110
|
+
console.log(`[WS→] ${type}${lizardId ? ` [${lizardId}]` : ''}${data.tool ? ` tool=${data.tool}` : ''}${data.state ? ` state=${data.state}` : ''}`);
|
|
111
|
+
}
|
|
103
112
|
ws.send(JSON.stringify(data));
|
|
104
113
|
}
|
|
105
114
|
}
|
|
106
|
-
|
|
115
|
+
// Save base64 data URL images to temp files and return file paths
|
|
116
|
+
export function saveImagesToTempFiles(images) {
|
|
117
|
+
const paths = [];
|
|
118
|
+
const dir = path.join(tmpdir(), 'gekto-images');
|
|
119
|
+
if (!existsSync(dir)) {
|
|
120
|
+
mkdirSync(dir, { recursive: true });
|
|
121
|
+
}
|
|
122
|
+
for (const dataUrl of images) {
|
|
123
|
+
const match = dataUrl.match(/^data:image\/(\w+);base64,(.+)$/);
|
|
124
|
+
if (!match)
|
|
125
|
+
continue;
|
|
126
|
+
const ext = match[1] === 'jpeg' ? 'jpg' : match[1];
|
|
127
|
+
const buffer = Buffer.from(match[2], 'base64');
|
|
128
|
+
const filePath = path.join(dir, `gekto-img-${randomUUID()}.${ext}`);
|
|
129
|
+
writeFileSync(filePath, buffer);
|
|
130
|
+
paths.push(filePath);
|
|
131
|
+
}
|
|
132
|
+
return paths;
|
|
133
|
+
}
|
|
134
|
+
export async function sendMessage(lizardId, message, ws, images) {
|
|
107
135
|
const session = getOrCreateSession(lizardId, ws);
|
|
136
|
+
// Save images to temp files and build the final message
|
|
137
|
+
let finalMessage = message;
|
|
138
|
+
let imagePaths;
|
|
139
|
+
if (images && images.length > 0) {
|
|
140
|
+
imagePaths = saveImagesToTempFiles(images);
|
|
141
|
+
}
|
|
142
|
+
// Accumulators for streaming deltas — reset on each tool start
|
|
143
|
+
let accumulatedText = '';
|
|
144
|
+
let accumulatedThinking = '';
|
|
108
145
|
// Create streaming callbacks that use session's current WebSocket
|
|
109
146
|
const callbacks = {
|
|
110
147
|
onToolStart: (tool, input) => {
|
|
148
|
+
// Reset accumulators when a new tool starts (text block ended)
|
|
149
|
+
accumulatedText = '';
|
|
150
|
+
accumulatedThinking = '';
|
|
111
151
|
safeSend(session, {
|
|
112
152
|
type: 'tool',
|
|
113
153
|
lizardId,
|
|
@@ -126,24 +166,75 @@ export async function sendMessage(lizardId, message, ws) {
|
|
|
126
166
|
});
|
|
127
167
|
},
|
|
128
168
|
onText: (text) => {
|
|
169
|
+
accumulatedText += text;
|
|
129
170
|
safeSend(session, {
|
|
130
171
|
type: 'text',
|
|
131
172
|
lizardId,
|
|
132
|
-
text,
|
|
173
|
+
text: accumulatedText,
|
|
174
|
+
});
|
|
175
|
+
},
|
|
176
|
+
onThinking: (text) => {
|
|
177
|
+
accumulatedThinking += text;
|
|
178
|
+
safeSend(session, {
|
|
179
|
+
type: 'thinking',
|
|
180
|
+
lizardId,
|
|
181
|
+
text: accumulatedThinking,
|
|
182
|
+
});
|
|
183
|
+
},
|
|
184
|
+
onToolResult: (tool, content, toolUseId) => {
|
|
185
|
+
safeSend(session, {
|
|
186
|
+
type: 'tool_result',
|
|
187
|
+
lizardId,
|
|
188
|
+
tool,
|
|
189
|
+
content: content.length > 2000 ? content.substring(0, 2000) + '…' : content,
|
|
190
|
+
toolUseId,
|
|
133
191
|
});
|
|
134
192
|
},
|
|
135
193
|
onFileChange: (change) => {
|
|
194
|
+
// Encode path for use as key
|
|
195
|
+
const encodedPath = change.filePath.replace(/\//g, '--');
|
|
196
|
+
// Enrich change with metadata
|
|
197
|
+
const enrichedChange = {
|
|
198
|
+
...change,
|
|
199
|
+
agentId: lizardId,
|
|
200
|
+
taskId: getState().agents[lizardId]?.taskId,
|
|
201
|
+
timestamp: new Date().toISOString(),
|
|
202
|
+
};
|
|
203
|
+
// Write to top-level fileChanges collection
|
|
204
|
+
mutate(`fileChanges.${encodedPath}`, enrichedChange);
|
|
205
|
+
broadcastFileChange(encodedPath);
|
|
206
|
+
// Also update agent's fileChangePaths for reference
|
|
207
|
+
const agent = getState().agents[lizardId];
|
|
208
|
+
if (agent) {
|
|
209
|
+
const paths = agent.fileChangePaths ?? [];
|
|
210
|
+
if (!paths.includes(change.filePath)) {
|
|
211
|
+
mutate(`agents.${lizardId}.fileChangePaths`, [...paths, change.filePath]);
|
|
212
|
+
}
|
|
213
|
+
// Keep backward-compat fileChanges on agent
|
|
214
|
+
const existing = agent.fileChanges ?? [];
|
|
215
|
+
const existingIndex = existing.findIndex(fc => fc.filePath === change.filePath);
|
|
216
|
+
let updated;
|
|
217
|
+
if (existingIndex >= 0) {
|
|
218
|
+
updated = [...existing];
|
|
219
|
+
updated[existingIndex] = { ...updated[existingIndex], after: change.after, tool: change.tool };
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
updated = [...existing, enrichedChange];
|
|
223
|
+
}
|
|
224
|
+
mutate(`agents.${lizardId}.fileChanges`, updated);
|
|
225
|
+
broadcastAgent(lizardId);
|
|
226
|
+
}
|
|
136
227
|
safeSend(session, {
|
|
137
228
|
type: 'file_change',
|
|
138
229
|
lizardId,
|
|
139
|
-
change,
|
|
230
|
+
change: enrichedChange,
|
|
140
231
|
});
|
|
141
232
|
},
|
|
142
233
|
};
|
|
143
234
|
// If already processing, queue the message
|
|
144
235
|
if (session.isProcessing) {
|
|
145
236
|
return new Promise((resolve, reject) => {
|
|
146
|
-
session.queue.push({ message, ws, callbacks, resolve, reject });
|
|
237
|
+
session.queue.push({ message: finalMessage, ws, callbacks, resolve, reject, imagePaths });
|
|
147
238
|
const position = session.queue.length;
|
|
148
239
|
ws.send(JSON.stringify({
|
|
149
240
|
type: 'queued',
|
|
@@ -153,14 +244,14 @@ export async function sendMessage(lizardId, message, ws) {
|
|
|
153
244
|
});
|
|
154
245
|
}
|
|
155
246
|
// Process immediately
|
|
156
|
-
return processMessage(lizardId, session,
|
|
247
|
+
return processMessage(lizardId, session, finalMessage, ws, callbacks, imagePaths);
|
|
157
248
|
}
|
|
158
249
|
async function processMessage(lizardId, session, message, _ws, // Kept for queue compatibility, but we use session.currentWs
|
|
159
|
-
callbacks) {
|
|
250
|
+
callbacks, imagePaths) {
|
|
160
251
|
session.isProcessing = true;
|
|
161
252
|
safeSend(session, { type: 'state', lizardId, state: 'working' });
|
|
162
253
|
try {
|
|
163
|
-
const response = await session.agent.send(message, callbacks);
|
|
254
|
+
const response = await session.agent.send(message, callbacks, imagePaths);
|
|
164
255
|
safeSend(session, {
|
|
165
256
|
type: 'response',
|
|
166
257
|
lizardId,
|
|
@@ -185,7 +276,7 @@ callbacks) {
|
|
|
185
276
|
// Process next queued message if any
|
|
186
277
|
if (session.queue.length > 0) {
|
|
187
278
|
const next = session.queue.shift();
|
|
188
|
-
processMessage(lizardId, session, next.message, next.ws, next.callbacks)
|
|
279
|
+
processMessage(lizardId, session, next.message, next.ws, next.callbacks, next.imagePaths)
|
|
189
280
|
.then(next.resolve)
|
|
190
281
|
.catch(next.reject);
|
|
191
282
|
}
|