gekto 0.0.10 → 0.0.12
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/agentPool.js +6 -10
- package/dist/agents/agentWebSocket.js +69 -18
- package/dist/agents/gektoPersistent.js +6 -10
- package/dist/agents/gektoTools.js +19 -8
- package/dist/entityStore.js +1 -0
- package/dist/state.js +8 -0
- package/dist/widget/gekto-widget.iife.js +114 -114
- package/package.json +1 -1
package/dist/agents/agentPool.js
CHANGED
|
@@ -5,6 +5,7 @@ import { randomUUID } from 'crypto';
|
|
|
5
5
|
import { tmpdir } from 'os';
|
|
6
6
|
import { HeadlessAgent } from './HeadlessAgent.js';
|
|
7
7
|
import { getState, mutate, broadcastFileChange, broadcastAgent } from '../state.js';
|
|
8
|
+
import { BASH_SAFETY_RULES } from './bashSafetyRules.js';
|
|
8
9
|
// Per-lizard sessions
|
|
9
10
|
const sessions = new Map();
|
|
10
11
|
// Summarize tool input for display
|
|
@@ -21,20 +22,15 @@ function summarizeInput(input) {
|
|
|
21
22
|
}
|
|
22
23
|
const DEFAULT_SYSTEM_PROMPT = `You are a helpful coding assistant. Be concise and direct in your responses.
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
2. DO NOT try to build, compile, or bundle the project
|
|
27
|
-
3. DO NOT try to start, restart, or run any servers or dev environments
|
|
28
|
-
4. DO NOT run tests, linters, or any CLI tools
|
|
29
|
-
5. DO NOT install packages or run npm/yarn/pnpm commands
|
|
25
|
+
You can use Bash for running tests, installing packages, building, and other shell operations.
|
|
26
|
+
${BASH_SAFETY_RULES}
|
|
30
27
|
|
|
31
|
-
Your job is
|
|
28
|
+
Your job is to:
|
|
32
29
|
- Read and understand code using Read, Glob, Grep tools
|
|
33
30
|
- Write and edit code using Write and Edit tools
|
|
31
|
+
- Use Bash for running tests, installing packages, git operations, and build commands
|
|
34
32
|
- Make the requested code changes
|
|
35
33
|
|
|
36
|
-
After making changes, simply report what you did. The user will handle building, testing, and running the code themselves.
|
|
37
|
-
|
|
38
34
|
STATUS MARKER - At the END of EVERY response, you MUST include exactly one of these markers:
|
|
39
35
|
- [STATUS:DONE] - Use when the task is complete and you have no questions for the user
|
|
40
36
|
- [STATUS:PENDING] - Use when you need user input, confirmation, clarification, or approval to proceed
|
|
@@ -44,7 +40,7 @@ Examples:
|
|
|
44
40
|
- When asking a question: "Which approach would you prefer? [STATUS:PENDING]"
|
|
45
41
|
- After answering a simple question: "The file is located at src/utils.ts [STATUS:DONE]"`;
|
|
46
42
|
// Tools that agents are not allowed to use
|
|
47
|
-
const DISALLOWED_TOOLS = ['
|
|
43
|
+
const DISALLOWED_TOOLS = ['Task'];
|
|
48
44
|
function getOrCreateSession(lizardId, ws) {
|
|
49
45
|
let session = sessions.get(lizardId);
|
|
50
46
|
if (!session) {
|
|
@@ -2,7 +2,7 @@ import { WebSocketServer } from 'ws';
|
|
|
2
2
|
import { sendMessage, resumeSession, resetSession, getWorkingDir, getActiveSessions, killSession, killAllSessions, attachWebSocket, revertFiles, saveImagesToTempFiles } from './agentPool.js';
|
|
3
3
|
import { processWithTools, generateTasksFromAbstract } from './gektoTools.js';
|
|
4
4
|
import { initGekto, getGektoState, abortGekto, setStateCallback, resetGektoSession, restoreGektoSession, getGektoSessionId } from './gektoPersistent.js';
|
|
5
|
-
import { getState, mutate, mutateBatch, addClient, removeClient, sendSnapshot, getClients, broadcastActivePlans, broadcastSinglePlan, broadcastTask, broadcastAgent, broadcastVisuals, broadcastVisualDelete, broadcastForPath } from '../state.js';
|
|
5
|
+
import { getState, mutate, mutateBatch, addClient, removeClient, sendSnapshot, getClients, broadcastActivePlans, broadcastActivePlanId, broadcastSinglePlan, broadcastTask, broadcastAgent, broadcastVisuals, broadcastVisualDelete, broadcastForPath } from '../state.js';
|
|
6
6
|
import { persistEntity } from '../entityStore.js';
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import nodePath from 'path';
|
|
@@ -190,7 +190,14 @@ export function setupAgentWebSocket(server, path = '/__gekto/agent') {
|
|
|
190
190
|
// Clear tasks and plans
|
|
191
191
|
mutate('tasks', {});
|
|
192
192
|
mutate('activePlans', {});
|
|
193
|
+
mutate('activePlanId', null);
|
|
193
194
|
broadcastActivePlans();
|
|
195
|
+
broadcastActivePlanId();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
case 'set_active_plan': {
|
|
199
|
+
mutate('activePlanId', msg.planId ?? null);
|
|
200
|
+
broadcastActivePlanId();
|
|
194
201
|
return;
|
|
195
202
|
}
|
|
196
203
|
case 'create_plan': {
|
|
@@ -208,6 +215,31 @@ export function setupAgentWebSocket(server, path = '/__gekto/agent') {
|
|
|
208
215
|
if (planImages && planImages.length > 0) {
|
|
209
216
|
planImagePaths = saveImagesToTempFiles(planImages);
|
|
210
217
|
}
|
|
218
|
+
// Remember the plan's state before processing so we can restore it
|
|
219
|
+
// if Gekto replies with chat/delegate/error instead of a plan update.
|
|
220
|
+
// Only delete plans that were created as temporary 'planning' entries.
|
|
221
|
+
const planBeforeProcessing = getState().activePlans[msg.planId] ?? null;
|
|
222
|
+
const planExistedBefore = planBeforeProcessing !== null;
|
|
223
|
+
const previousStatus = planBeforeProcessing?.status;
|
|
224
|
+
// Set plan status to 'planning' on the server side (authoritative)
|
|
225
|
+
// so we don't race with the client's save_state message
|
|
226
|
+
if (planBeforeProcessing) {
|
|
227
|
+
mutate(`activePlans.${msg.planId}.status`, 'planning');
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
// Create temporary plan entry for new plans
|
|
231
|
+
mutate(`activePlans.${msg.planId}`, {
|
|
232
|
+
id: msg.planId,
|
|
233
|
+
status: 'planning',
|
|
234
|
+
originalPrompt: msg.prompt ?? '',
|
|
235
|
+
taskIds: [],
|
|
236
|
+
createdAt: new Date().toISOString(),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
broadcastSinglePlan(msg.planId);
|
|
240
|
+
// Set as active plan
|
|
241
|
+
mutate('activePlanId', msg.planId);
|
|
242
|
+
broadcastActivePlanId();
|
|
211
243
|
try {
|
|
212
244
|
// Server-side accumulators and block counter
|
|
213
245
|
let accThinking = '';
|
|
@@ -308,6 +340,9 @@ export function setupAgentWebSocket(server, path = '/__gekto/agent') {
|
|
|
308
340
|
broadcastTask(task.id);
|
|
309
341
|
}
|
|
310
342
|
}
|
|
343
|
+
// Set the newly created/updated plan as active
|
|
344
|
+
mutate('activePlanId', planResult.plan.id);
|
|
345
|
+
broadcastActivePlanId();
|
|
311
346
|
ws.send(JSON.stringify({
|
|
312
347
|
type: 'plan_created',
|
|
313
348
|
planId: msg.planId,
|
|
@@ -336,21 +371,27 @@ export function setupAgentWebSocket(server, path = '/__gekto/agent') {
|
|
|
336
371
|
}));
|
|
337
372
|
}
|
|
338
373
|
else if (planResult.type === 'delegate' && planResult.delegateAgentId) {
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
374
|
+
// Restore plan state — only delete if it was a temporary entry
|
|
375
|
+
if (msg.planId && getState().activePlans[msg.planId]?.status === 'planning') {
|
|
376
|
+
if (planExistedBefore && previousStatus) {
|
|
377
|
+
mutate(`activePlans.${msg.planId}.status`, previousStatus);
|
|
378
|
+
broadcastSinglePlan(msg.planId);
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
mutate(`activePlans.${msg.planId}`, undefined);
|
|
382
|
+
broadcastSinglePlan(msg.planId);
|
|
383
|
+
}
|
|
344
384
|
}
|
|
345
385
|
// Send instruction to existing agent
|
|
346
386
|
const targetAgentId = planResult.delegateAgentId;
|
|
347
|
-
const
|
|
387
|
+
const delegateState = getState();
|
|
388
|
+
const targetAgent = delegateState.agents[targetAgentId];
|
|
348
389
|
if (targetAgent) {
|
|
349
390
|
// Update agent status to working
|
|
350
391
|
mutate(`agents.${targetAgentId}.status`, 'working');
|
|
351
392
|
broadcastAgent(targetAgentId);
|
|
352
393
|
// Notify client about delegation
|
|
353
|
-
const delegateTask = targetAgent.taskId ?
|
|
394
|
+
const delegateTask = targetAgent.taskId ? delegateState.tasks[targetAgent.taskId] : null;
|
|
354
395
|
ws.send(JSON.stringify({
|
|
355
396
|
type: 'gekto_delegate',
|
|
356
397
|
planId: msg.planId,
|
|
@@ -372,11 +413,16 @@ export function setupAgentWebSocket(server, path = '/__gekto/agent') {
|
|
|
372
413
|
}
|
|
373
414
|
}
|
|
374
415
|
else if (planResult.type === 'chat') {
|
|
375
|
-
//
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
416
|
+
// Restore plan state — only delete if it was a temporary entry
|
|
417
|
+
if (msg.planId && getState().activePlans[msg.planId]?.status === 'planning') {
|
|
418
|
+
if (planExistedBefore && previousStatus) {
|
|
419
|
+
mutate(`activePlans.${msg.planId}.status`, previousStatus);
|
|
420
|
+
broadcastSinglePlan(msg.planId);
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
mutate(`activePlans.${msg.planId}`, undefined);
|
|
424
|
+
broadcastSinglePlan(msg.planId);
|
|
425
|
+
}
|
|
380
426
|
}
|
|
381
427
|
}
|
|
382
428
|
// Persist Gekto session ID on current master so it survives restart
|
|
@@ -400,11 +446,16 @@ export function setupAgentWebSocket(server, path = '/__gekto/agent') {
|
|
|
400
446
|
planId: msg.planId,
|
|
401
447
|
message: `Error: ${err instanceof Error ? err.message : 'Processing failed'}`,
|
|
402
448
|
}));
|
|
403
|
-
//
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
449
|
+
// Restore plan state on error — only delete if it was a temporary entry
|
|
450
|
+
if (msg.planId && getState().activePlans[msg.planId]?.status === 'planning') {
|
|
451
|
+
if (planExistedBefore && previousStatus) {
|
|
452
|
+
mutate(`activePlans.${msg.planId}.status`, previousStatus);
|
|
453
|
+
broadcastSinglePlan(msg.planId);
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
mutate(`activePlans.${msg.planId}`, undefined);
|
|
457
|
+
broadcastSinglePlan(msg.planId);
|
|
458
|
+
}
|
|
408
459
|
}
|
|
409
460
|
ws.send(JSON.stringify({ type: 'gekto_done', planId: msg.planId }));
|
|
410
461
|
ws.send(JSON.stringify({ type: 'state', lizardId: 'master', state: 'ready' }));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
import { randomUUID } from 'crypto';
|
|
3
3
|
import { CLAUDE_PATH } from '../claudePath.js';
|
|
4
|
+
import { BASH_SAFETY_RULES } from './bashSafetyRules.js';
|
|
4
5
|
// === JSON Schema for structured output ===
|
|
5
6
|
export const GEKTO_OUTPUT_SCHEMA = JSON.stringify({
|
|
6
7
|
type: 'object',
|
|
@@ -36,7 +37,7 @@ How you act:
|
|
|
36
37
|
- If the user's request is ambiguous, use "clarify" with a focused question in "message".
|
|
37
38
|
- IMPORTANT: Before creating a new plan, ALWAYS check [CURRENT STATE] first. If an existing agent has context about the relevant files, use "delegate" instead. Only use "create_plan" when no existing agent can handle the request.
|
|
38
39
|
- If the user wants to build something and no existing agent is relevant, use "create_plan" with a short "title" (2-4 words), an abstract plan description, and a short "message" (1-2 sentences) for the chat — this is a brief confirmation shown to the user, NOT a copy of the abstract.
|
|
39
|
-
- If the user wants to modify
|
|
40
|
+
- If the user wants to modify the active plan (marked [ACTIVE] in CURRENT STATE), use "update_plan" with the updated abstract. "update_plan" ALWAYS applies to the active plan. You MUST also include a short "message" for the chat confirming what changed.
|
|
40
41
|
- If the user wants to remove agents, use "remove_agents" with a target.
|
|
41
42
|
- If there is an existing agent that has context about the relevant files or task, use "delegate" with "agentId" (the agent's ID from CURRENT STATE) and "message" (a clear instruction). The agent already has session context — delegating is faster than creating a new plan. Always include a short "message" for the chat confirming what you delegated.
|
|
42
43
|
- ALWAYS research the codebase first (Read, Glob, Grep) before creating plans. Understand the project structure, frameworks, and conventions.
|
|
@@ -44,6 +45,7 @@ How you act:
|
|
|
44
45
|
Conflict awareness:
|
|
45
46
|
- Before creating or updating a plan, check the [CURRENT STATE] context for running agents, active tasks, and files being modified.
|
|
46
47
|
- If the user's request would modify files that are currently being worked on by other agents, WARN the user about the conflict. Use "clarify" to explain which files/tasks overlap and suggest waiting or adjusting scope.
|
|
48
|
+
- The [ACTIVE] plan in CURRENT STATE is the one the user is currently viewing. When the user asks to change "the plan", they mean the active plan — use "update_plan".
|
|
47
49
|
- If there is already an active plan with similar goals, point it out and ask whether to update the existing plan or start a new one.
|
|
48
50
|
- Never schedule tasks that write to the same files as currently running agents — this causes merge conflicts and lost work.
|
|
49
51
|
|
|
@@ -61,7 +63,8 @@ Abstract plan rules for create_plan / update_plan:
|
|
|
61
63
|
- Include a "buildPrompt" explaining how to wire everything together after individual tasks complete.
|
|
62
64
|
- Work items should be parallelizable — no item should depend on another item's output.
|
|
63
65
|
|
|
64
|
-
You can
|
|
66
|
+
You can use Read, Glob, Grep, and Bash tools. Task is disabled.
|
|
67
|
+
${BASH_SAFETY_RULES}
|
|
65
68
|
|
|
66
69
|
Your response MUST be valid JSON matching this schema. Output ONLY the JSON object, nothing else.
|
|
67
70
|
${GEKTO_OUTPUT_SCHEMA}`;
|
|
@@ -130,7 +133,7 @@ function spawnOpus() {
|
|
|
130
133
|
'--model', 'claude-opus-4-5-20251101',
|
|
131
134
|
'--system-prompt', GEKTO_SYSTEM_PROMPT,
|
|
132
135
|
'--dangerously-skip-permissions',
|
|
133
|
-
'--disallowed-tools', '
|
|
136
|
+
'--disallowed-tools', 'Task',
|
|
134
137
|
'--session-id', gektoSessionId,
|
|
135
138
|
];
|
|
136
139
|
console.log(`[GektoPersistent] Spawning with session ${gektoSessionId}`);
|
|
@@ -315,13 +318,6 @@ async function sendToOpus(prompt, callbacks, retries = 3) {
|
|
|
315
318
|
}
|
|
316
319
|
return;
|
|
317
320
|
}
|
|
318
|
-
// Timeout after 5 min for complex tasks
|
|
319
|
-
setTimeout(() => {
|
|
320
|
-
if (opusPendingResolve) {
|
|
321
|
-
opusPendingResolve('Task timed out. Please try breaking it into smaller steps.');
|
|
322
|
-
opusPendingResolve = null;
|
|
323
|
-
}
|
|
324
|
-
}, 300000);
|
|
325
321
|
});
|
|
326
322
|
}
|
|
327
323
|
// === Planning API (reuses warm persistent process) ===
|
|
@@ -114,13 +114,15 @@ export async function processWithTools(prompt, planId, _workingDir, activeAgents
|
|
|
114
114
|
});
|
|
115
115
|
contextParts.push(`Active agents:\n${agentLines.join('\n')}`);
|
|
116
116
|
}
|
|
117
|
-
// Active plans and their tasks
|
|
117
|
+
// Active plans and their tasks — mark which one is the active plan
|
|
118
118
|
const planEntries = Object.values(serverState.activePlans);
|
|
119
119
|
if (planEntries.length > 0) {
|
|
120
|
+
const activePlanId = serverState.activePlanId;
|
|
120
121
|
const planLines = planEntries.map(plan => {
|
|
122
|
+
const isActive = plan.id === activePlanId;
|
|
121
123
|
const tasks = plan.taskIds.map(id => serverState.tasks[id]).filter(Boolean);
|
|
122
124
|
const taskSummary = tasks.map(t => ` - "${t.name}" (${t.status})${t.files?.length ? ` files=[${t.files.join(', ')}]` : ''}`).join('\n');
|
|
123
|
-
return ` Plan "${plan.title || plan.id}" (${plan.status}):\n${taskSummary || ' (no tasks yet)'}`;
|
|
125
|
+
return ` ${isActive ? '[ACTIVE] ' : ''}Plan "${plan.title || plan.id}" (${plan.status}):\n${taskSummary || ' (no tasks yet)'}`;
|
|
124
126
|
});
|
|
125
127
|
contextParts.push(`Active plans:\n${planLines.join('\n')}`);
|
|
126
128
|
}
|
|
@@ -139,12 +141,15 @@ export async function processWithTools(prompt, planId, _workingDir, activeAgents
|
|
|
139
141
|
contextPrompt += `\n\n[CURRENT STATE:\n${contextParts.join('\n\n')}]`;
|
|
140
142
|
}
|
|
141
143
|
// Add existing plan context for modifications
|
|
142
|
-
|
|
143
|
-
|
|
144
|
+
// Use server-side activePlanId as the authoritative source
|
|
145
|
+
const activePlan = serverState.activePlanId ? serverState.activePlans[serverState.activePlanId] : null;
|
|
146
|
+
const planAbstract = existingPlan?.abstract || activePlan?.abstract;
|
|
147
|
+
if (planAbstract) {
|
|
148
|
+
contextPrompt += `\n\n[ACTIVE PLAN ABSTRACT — "${activePlan?.title || 'Untitled'}" (${activePlan?.status || 'unknown'}):
|
|
144
149
|
|
|
145
|
-
${
|
|
150
|
+
${planAbstract}
|
|
146
151
|
|
|
147
|
-
|
|
152
|
+
If the user wants to change this plan, respond with "update_plan" and the FULL updated abstract (not just the changes). Keep the same structure and style.]`;
|
|
148
153
|
}
|
|
149
154
|
// Append image file paths to prompt so Gekto can reference them
|
|
150
155
|
if (imagePaths && imagePaths.length > 0) {
|
|
@@ -168,16 +173,22 @@ The user's message above is a modification request. Respond with "update_plan" a
|
|
|
168
173
|
switch (parsed.action) {
|
|
169
174
|
case 'create_plan':
|
|
170
175
|
case 'update_plan': {
|
|
176
|
+
// For update_plan, always target the server's active plan
|
|
177
|
+
const effectivePlanId = parsed.action === 'update_plan' && serverState.activePlanId
|
|
178
|
+
? serverState.activePlanId
|
|
179
|
+
: planId;
|
|
180
|
+
// Preserve createdAt when updating an existing plan
|
|
181
|
+
const existingCreatedAt = serverState.activePlans[effectivePlanId]?.createdAt;
|
|
171
182
|
// Create a draft plan with abstract — tasks are generated later
|
|
172
183
|
const plan = {
|
|
173
|
-
id:
|
|
184
|
+
id: effectivePlanId,
|
|
174
185
|
status: 'draft',
|
|
175
186
|
title: parsed.title,
|
|
176
187
|
originalPrompt: prompt,
|
|
177
188
|
abstract: parsed.abstract || parsed.message || '',
|
|
178
189
|
buildPrompt: parsed.buildPrompt,
|
|
179
190
|
taskIds: [],
|
|
180
|
-
createdAt: new Date().toISOString(),
|
|
191
|
+
createdAt: existingCreatedAt || new Date().toISOString(),
|
|
181
192
|
};
|
|
182
193
|
return {
|
|
183
194
|
type: 'build',
|
package/dist/entityStore.js
CHANGED
package/dist/state.js
CHANGED
|
@@ -16,6 +16,7 @@ function createMasterId() {
|
|
|
16
16
|
function createEmptyState() {
|
|
17
17
|
return {
|
|
18
18
|
activePlans: {},
|
|
19
|
+
activePlanId: null,
|
|
19
20
|
tasks: {},
|
|
20
21
|
agents: {},
|
|
21
22
|
visuals: {},
|
|
@@ -136,6 +137,10 @@ export function broadcast(action) {
|
|
|
136
137
|
export function broadcastActivePlans() {
|
|
137
138
|
broadcast({ type: 'active_plans_set', activePlans: state.activePlans });
|
|
138
139
|
}
|
|
140
|
+
/** Broadcast active plan ID to all clients. */
|
|
141
|
+
export function broadcastActivePlanId() {
|
|
142
|
+
broadcast({ type: 'active_plan_id_set', activePlanId: state.activePlanId });
|
|
143
|
+
}
|
|
139
144
|
/** Broadcast a single plan (or deletion) to all clients. */
|
|
140
145
|
export function broadcastSinglePlan(planId) {
|
|
141
146
|
broadcast({ type: 'plan_set', planId, plan: state.activePlans[planId] ?? null });
|
|
@@ -190,6 +195,9 @@ export function broadcastForPath(dotPath) {
|
|
|
190
195
|
const parts = dotPath.split('.');
|
|
191
196
|
const root = parts[0];
|
|
192
197
|
switch (root) {
|
|
198
|
+
case 'activePlanId':
|
|
199
|
+
broadcastActivePlanId();
|
|
200
|
+
break;
|
|
193
201
|
case 'activePlans':
|
|
194
202
|
if (parts[1]) {
|
|
195
203
|
broadcastSinglePlan(parts[1]);
|