@strayl/agent 0.1.2 → 0.1.4

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 (58) hide show
  1. package/dist/agent.js +6 -7
  2. package/package.json +5 -1
  3. package/skills/api-creation/SKILL.md +631 -0
  4. package/skills/authentication/SKILL.md +294 -0
  5. package/skills/frontend-design/SKILL.md +108 -0
  6. package/skills/landing-creation/SKILL.md +125 -0
  7. package/skills/reference/SKILL.md +149 -0
  8. package/skills/web-application-creation/SKILL.md +231 -0
  9. package/src/agent.ts +0 -465
  10. package/src/checkpoints/manager.ts +0 -112
  11. package/src/context/manager.ts +0 -185
  12. package/src/context/summarizer.ts +0 -104
  13. package/src/context/trim.ts +0 -55
  14. package/src/emitter.ts +0 -14
  15. package/src/hitl/manager.ts +0 -77
  16. package/src/hitl/transport.ts +0 -13
  17. package/src/index.ts +0 -116
  18. package/src/llm/client.ts +0 -276
  19. package/src/llm/gemini-native.ts +0 -307
  20. package/src/llm/models.ts +0 -64
  21. package/src/middleware/compose.ts +0 -24
  22. package/src/middleware/credential-scrubbing.ts +0 -31
  23. package/src/middleware/forbidden-packages.ts +0 -107
  24. package/src/middleware/plan-mode.ts +0 -143
  25. package/src/middleware/prompt-caching.ts +0 -21
  26. package/src/middleware/tool-compression.ts +0 -25
  27. package/src/middleware/tool-filter.ts +0 -13
  28. package/src/prompts/implementation-mode.md +0 -16
  29. package/src/prompts/plan-mode.md +0 -51
  30. package/src/prompts/system.ts +0 -173
  31. package/src/skills/loader.ts +0 -53
  32. package/src/stdin-listener.ts +0 -61
  33. package/src/subagents/definitions.ts +0 -72
  34. package/src/subagents/manager.ts +0 -161
  35. package/src/todos/manager.ts +0 -61
  36. package/src/tools/builtin/delete.ts +0 -29
  37. package/src/tools/builtin/edit.ts +0 -74
  38. package/src/tools/builtin/exec.ts +0 -216
  39. package/src/tools/builtin/glob.ts +0 -104
  40. package/src/tools/builtin/grep.ts +0 -115
  41. package/src/tools/builtin/ls.ts +0 -54
  42. package/src/tools/builtin/move.ts +0 -31
  43. package/src/tools/builtin/read.ts +0 -69
  44. package/src/tools/builtin/write.ts +0 -42
  45. package/src/tools/executor.ts +0 -51
  46. package/src/tools/external/database.ts +0 -285
  47. package/src/tools/external/enter-plan-mode.ts +0 -34
  48. package/src/tools/external/generate-image.ts +0 -110
  49. package/src/tools/external/hitl-tools.ts +0 -118
  50. package/src/tools/external/preview.ts +0 -28
  51. package/src/tools/external/proxy-fetch.ts +0 -51
  52. package/src/tools/external/task.ts +0 -38
  53. package/src/tools/external/wait.ts +0 -20
  54. package/src/tools/external/web-fetch.ts +0 -57
  55. package/src/tools/external/web-search.ts +0 -61
  56. package/src/tools/registry.ts +0 -36
  57. package/src/tools/zod-to-json-schema.ts +0 -86
  58. package/src/types.ts +0 -151
@@ -0,0 +1,231 @@
1
+ ---
2
+ name: web-application-creation
3
+ description: Initialize a new web application from the Strayl template using npx create-strayl-app. Handles project scaffolding, dependency installation, and dev server startup. Use when the user wants a full-featured app with multiple routes, forms, auth, database, or any complex logic.
4
+ ---
5
+
6
+ # Web Application Creation
7
+
8
+ ## When to Use
9
+
10
+ - User asks to create, start, or scaffold a new web application
11
+ - User wants to build a new project from scratch
12
+ - The workspace repository is empty and needs a project initialized
13
+ - User says "create an app", "start a new project", "build me an app"
14
+ - User needs multiple routes, forms, auth, database, dashboards, CRUD
15
+
16
+ ## When NOT to Use (use `landing-creation` instead)
17
+
18
+ - User wants a single-page landing, marketing page, or promotional site
19
+ - User says "make me a website", "create a homepage", "build a landing page"
20
+ - User's request implies a simple static site without complex backend logic
21
+
22
+ ## Workflow
23
+
24
+ 1. **Ask the user for the app name** before running any commands. The name will be used as the project directory name.
25
+ 2. **Run the scaffolding command** in the repository root:
26
+ ```
27
+ npx -y create-strayl-app@latest --name {app-name}
28
+ ```
29
+ 3. **Install dependencies:**
30
+ ```
31
+ cd {app-name} && npm install
32
+ ```
33
+ 4. **Start the dev server** in background mode:
34
+ ```
35
+ cd {app-name} && npm run dev
36
+ ```
37
+ 5. **Check logs** to confirm the server started successfully (look for "ready" or "listening on port 3000").
38
+ 6. **Show preview** to the user with `showPreview`.
39
+ 7. **Continue building** based on user's request. Common next steps:
40
+ - Need user accounts? → Read `/skills/authentication/SKILL.md`
41
+ - Need a database? → Read `/skills/database-management/SKILL.md`
42
+ - Need API endpoints / server logic? → Read `/skills/api-creation/SKILL.md`
43
+ - Need UI/UX design, styling, or visual polish? → Read `/skills/frontend-design/SKILL.md`
44
+
45
+ ## Template Stack
46
+
47
+ | Layer | Technology | Version |
48
+ |-------|------------|---------|
49
+ | Framework | React + TanStack Start | React 19, Start 1.x |
50
+ | Bundler | Vite | 7.x |
51
+ | Language | TypeScript (strict) | 5.7+ |
52
+ | Routing | TanStack Router (file-based) | 1.x |
53
+ | Styling | Tailwind CSS + CSS custom properties | 4.x |
54
+ | UI Components | shadcn/ui (install as needed) | latest |
55
+ | Icons | Lucide React | 0.475+ |
56
+ | Theming | next-themes (light/dark + system) | 0.4.x |
57
+ | Font | Geist (fontsource) | — |
58
+ | Server | Nitro (SSR + server functions) | nightly |
59
+
60
+ ## Project Structure
61
+
62
+ ```
63
+ {app-name}/
64
+ ├── public/
65
+ │ └── favicon.ico
66
+ ├── src/
67
+ │ ├── components/
68
+ │ │ ├── theme-provider.tsx # next-themes wrapper
69
+ │ │ └── ui/ # Minimal starter components (button, input, label, textarea)
70
+ │ ├── hooks/
71
+ │ │ └── use-mobile.ts # useIsMobile() — viewport < 768px
72
+ │ ├── lib/
73
+ │ │ └── utils.ts # cn() — clsx + tailwind-merge
74
+ │ ├── routes/
75
+ │ │ ├── __root.tsx # Root layout (HTML shell, Geist font, ThemeProvider)
76
+ │ │ ├── index.tsx # Home page ("STRAYL" + theme toggle)
77
+ │ │ └── routeTree.gen.ts # Auto-generated route tree (DO NOT EDIT)
78
+ │ ├── router.tsx # Router config (empty context, scroll restoration)
79
+ │ └── styles.css # Tailwind 4 theme (colors, fonts, radius)
80
+ ├── components.json # shadcn/ui config (style: new-york)
81
+ ├── package.json
82
+ ├── tsconfig.json
83
+ └── vite.config.ts # Vite + Nitro + TanStack Start + Tailwind plugins
84
+ ```
85
+
86
+ ## Critical Files — Know Before Editing
87
+
88
+ ### `src/routes/__root.tsx` — App Shell
89
+
90
+ Uses `shellComponent` (NOT `component`) to define the HTML document structure:
91
+
92
+ ```typescript
93
+ import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router'
94
+ import '@fontsource-variable/geist'
95
+ import { ThemeProvider } from '../components/theme-provider'
96
+ import appCss from '../styles.css?url'
97
+
98
+ export const Route = createRootRoute({
99
+ head: () => ({
100
+ meta: [
101
+ { charSet: 'utf-8' },
102
+ { name: 'viewport', content: 'width=device-width, initial-scale=1' },
103
+ { title: 'Strayl' },
104
+ ],
105
+ links: [
106
+ { rel: 'stylesheet', href: appCss },
107
+ { rel: 'icon', href: '/favicon.ico' },
108
+ ],
109
+ }),
110
+ shellComponent: RootDocument,
111
+ })
112
+
113
+ function RootDocument({ children }: { children: React.ReactNode }) {
114
+ return (
115
+ <html lang="en" suppressHydrationWarning>
116
+ <head><HeadContent /></head>
117
+ <body>
118
+ <ThemeProvider>{children}</ThemeProvider>
119
+ <Scripts />
120
+ </body>
121
+ </html>
122
+ )
123
+ }
124
+ ```
125
+
126
+ **To add auth context**, add `beforeLoad` — it works alongside `shellComponent`:
127
+ ```typescript
128
+ export const Route = createRootRoute({
129
+ beforeLoad: async () => {
130
+ const user = await getUser();
131
+ return { user };
132
+ },
133
+ head: () => ({ ... }),
134
+ shellComponent: RootDocument,
135
+ })
136
+ ```
137
+
138
+ ### `src/router.tsx` — Router Instance
139
+
140
+ ```typescript
141
+ export const getRouter = () => {
142
+ const router = createRouter({
143
+ routeTree,
144
+ context: {}, // ← empty by default; auth adds user here
145
+ scrollRestoration: true,
146
+ defaultPreloadStaleTime: 0,
147
+ })
148
+ return router
149
+ }
150
+ ```
151
+
152
+ ### `vite.config.ts` — Plugin Order Matters
153
+
154
+ ```typescript
155
+ plugins: [
156
+ nitro(), // SSR / server functions
157
+ viteTsConfigPaths(), // @/* path aliases
158
+ tailwindcss(), // Tailwind CSS 4
159
+ tanstackStart(), // TanStack Start framework
160
+ viteReact(), // React Fast Refresh
161
+ ]
162
+ ```
163
+
164
+ **Do NOT rewrite this file.** Use `edit_file` to add plugins if needed.
165
+
166
+ ### `src/styles.css` — Tailwind CSS 4 Theme
167
+
168
+ Uses `@theme inline {}` directive (NOT `tailwind.config.js`):
169
+
170
+ ```css
171
+ @import "tailwindcss";
172
+ @import "tw-animate-css";
173
+
174
+ @custom-variant dark (&:is(.dark *));
175
+ @variant dark (&:where(.dark, .dark *));
176
+
177
+ @theme inline {
178
+ --color-background: var(--background);
179
+ --color-primary: var(--primary);
180
+ --font-sans: "Geist Variable", ui-sans-serif, system-ui, sans-serif;
181
+ /* ... radius, animations */
182
+ }
183
+
184
+ :root { /* light theme colors */ }
185
+ .dark { /* dark theme colors */ }
186
+ ```
187
+
188
+ **To add custom colors**, edit the `:root` and `.dark` blocks. To add font families, add `@theme inline {}` entries.
189
+
190
+ ## UI Components
191
+
192
+ The template ships with only **4 minimal starter components** in `src/components/ui/`: `button`, `input`, `label`, `textarea`. These are simple, unstyled wrappers — just enough to get started.
193
+
194
+ ### Adding More Components
195
+
196
+ Use **shadcn/ui** to add any components you need:
197
+
198
+ ```bash
199
+ npx shadcn@latest add card dialog sheet select badge separator scroll-area tabs accordion dropdown-menu
200
+ ```
201
+
202
+ You can add any shadcn/ui component this way. Always install components as needed rather than building custom ones from scratch.
203
+
204
+ ### Important
205
+
206
+ - **Do NOT hardcode a fixed set of UI components into every app.** Each app should have components appropriate for its purpose.
207
+ - **Design each app's UI uniquely** — use different layouts, color schemes, and component combinations. Avoid repeating the same card/dialog/sheet pattern everywhere.
208
+ - Use Tailwind CSS utility classes directly for custom layouts. Not everything needs a component wrapper.
209
+ - For complex components (data tables, charts, calendars, etc.), install them via shadcn: `npx shadcn@latest add table chart calendar`
210
+
211
+ ## Protected Files — Edit Only, Never Rewrite
212
+
213
+ - `package.json` — use `npm install <pkg>`, use `edit_file` for scripts
214
+ - `tsconfig.json` — edit specific fields only
215
+ - `vite.config.ts` — edit, don't rewrite (plugin order matters)
216
+ - `src/styles.css` — append or edit (Tailwind @theme config)
217
+ - `src/routes/__root.tsx` — edit carefully (fonts, providers, beforeLoad)
218
+ - `src/routeTree.gen.ts` — NEVER edit (auto-generated by TanStack Router)
219
+
220
+ ## Best Practices
221
+
222
+ - **Do NOT** use `npm create`, `npx create-vite`, or other scaffolding tools
223
+ - Always use the `--name` flag to specify the project name
224
+ - After creation, work inside the `{app-name}/` directory for all file edits
225
+ - Start editing from `src/routes/index.tsx` and create new routes as files in `src/routes/`
226
+ - Add UI components via `npx shadcn@latest add <component>` as needed — do NOT install alternative UI libraries (no MUI, Chakra, Ant Design, etc.)
227
+ - New routes auto-register via TanStack Router file-based routing (the `routeTree.gen.ts` regenerates on dev server restart)
228
+ - For protected routes, use the `_authed.tsx` layout pattern (see `authentication` skill)
229
+ - For server-side logic, use `createServerFn` (see `api-creation` skill)
230
+ - **Run `npm run build` after your first server function + route are written.** Build errors surface immediately when the codebase is small and easy to fix. Catching import/bundling errors early prevents hours of debugging later.
231
+ - **NEVER use `await import()` for server functions in components.** Always static imports + `useServerFn()` for mutations. See `api-creation` skill for correct patterns.
package/src/agent.ts DELETED
@@ -1,465 +0,0 @@
1
- import type { AgentConfig, ToolCall, ToolContext, Middleware, Message, AgentMode, ActivityType } from "./types.js";
2
- import { Emitter } from "./emitter.js";
3
- import { LLMClient } from "./llm/client.js";
4
- import { getModelLimits, resolveModel } from "./llm/models.js";
5
- import { ContextManager } from "./context/manager.js";
6
- import { ToolRegistry } from "./tools/registry.js";
7
- import { executeTool } from "./tools/executor.js";
8
- import { HITLManager } from "./hitl/manager.js";
9
- import { SubAgentManager } from "./subagents/manager.js";
10
- import { TodoManager } from "./todos/manager.js";
11
- import { buildSystemPrompt } from "./prompts/system.js";
12
- import { loadSkills } from "./skills/loader.js";
13
- import { StdinListener } from "./stdin-listener.js";
14
- import { CheckpointManager } from "./checkpoints/manager.js";
15
-
16
- // Builtin tools
17
- import { execTool, getLogsTool } from "./tools/builtin/exec.js";
18
- import { readFileTool } from "./tools/builtin/read.js";
19
- import { writeFileTool } from "./tools/builtin/write.js";
20
- import { editFileTool } from "./tools/builtin/edit.js";
21
- import { lsTool } from "./tools/builtin/ls.js";
22
- import { grepTool } from "./tools/builtin/grep.js";
23
- import { globTool } from "./tools/builtin/glob.js";
24
- import { deleteFileTool } from "./tools/builtin/delete.js";
25
- import { moveFileTool } from "./tools/builtin/move.js";
26
-
27
- // External tools
28
- import { webSearchTool } from "./tools/external/web-search.js";
29
- import { webFetchTool } from "./tools/external/web-fetch.js";
30
- import { previewTool } from "./tools/external/preview.js";
31
- import { waitTool } from "./tools/external/wait.js";
32
- import { generateImageTool } from "./tools/external/generate-image.js";
33
- import { createTaskTool } from "./tools/external/task.js";
34
- import { enterPlanModeTool } from "./tools/external/enter-plan-mode.js";
35
- import { askUserTool, writePlanTool, requestEnvVarTool } from "./tools/external/hitl-tools.js";
36
- import { createDatabaseTool, listDatabasesTool, runDatabaseQueryTool, prepareDatabaseMigrationTool, completeDatabaseMigrationTool } from "./tools/external/database.js";
37
-
38
- // Middleware
39
- import { forbiddenPackagesMiddleware } from "./middleware/forbidden-packages.js";
40
- import { toolCompressionMiddleware } from "./middleware/tool-compression.js";
41
- import { createToolFilterMiddleware } from "./middleware/tool-filter.js";
42
- import { createPromptCachingMiddleware } from "./middleware/prompt-caching.js";
43
- import { credentialScrubbingMiddleware } from "./middleware/credential-scrubbing.js";
44
- import { createPlanModeMiddleware, resetContextForImplementation } from "./middleware/plan-mode.js";
45
-
46
- export async function runAgent(config: AgentConfig): Promise<void> {
47
- const emitter = new Emitter();
48
- const stdin = new StdinListener();
49
- stdin.start();
50
- const modelName = resolveModel(config.model);
51
- const limits = getModelLimits(modelName);
52
-
53
- // Mode state — mutable, changes on enterPlanMode tool or confirm-plan stdin command
54
- let currentMode: AgentMode = config.mode ?? "normal";
55
-
56
- const client = new LLMClient({
57
- modelTier: config.model,
58
- env: config.env,
59
- sessionId: config.sessionId,
60
- });
61
-
62
- const context = new ContextManager({
63
- maxInputTokens: limits.maxInputTokens,
64
- summarizationThreshold: config.summarizationThreshold,
65
- preSummarizationRatio: config.preSummarizationRatio,
66
- previousSummary: config.previousSummary,
67
- });
68
-
69
- const registry = new ToolRegistry();
70
- const hitl = new HITLManager(config.hitlDir);
71
- await hitl.init();
72
- const subAgentManager = new SubAgentManager(emitter);
73
- const todoManager = new TodoManager(emitter);
74
- const checkpoints = new CheckpointManager(config.workDir, config.sessionId);
75
- await checkpoints.init();
76
-
77
- // Register all tools
78
- const builtinTools = [execTool, getLogsTool, readFileTool, writeFileTool, editFileTool, lsTool, grepTool, globTool, deleteFileTool, moveFileTool];
79
- const databaseTools = [createDatabaseTool, listDatabasesTool, runDatabaseQueryTool, prepareDatabaseMigrationTool, completeDatabaseMigrationTool];
80
- const externalTools = [webSearchTool, webFetchTool, previewTool, waitTool, generateImageTool, enterPlanModeTool, ...databaseTools];
81
- const hitlTools = [askUserTool, writePlanTool, requestEnvVarTool];
82
-
83
- for (const tool of [...builtinTools, ...externalTools, ...hitlTools]) {
84
- registry.register(tool);
85
- }
86
- registry.register(createTaskTool(subAgentManager));
87
- for (const tool of todoManager.createTools()) {
88
- registry.register(tool);
89
- }
90
-
91
- // Build middleware
92
- const middleware: Middleware[] = [
93
- toolCompressionMiddleware,
94
- forbiddenPackagesMiddleware,
95
- credentialScrubbingMiddleware,
96
- createToolFilterMiddleware(config.blockedTools ?? []),
97
- createPlanModeMiddleware(() => currentMode),
98
- createPromptCachingMiddleware(modelName),
99
- ];
100
-
101
- emitter.emit({ type: "session-start", model: modelName, session_id: config.sessionId });
102
-
103
- // Load skills and build system prompt
104
- const skills = await loadSkills(config.skillsDir ?? "./skills");
105
- const systemPrompt = buildSystemPrompt({
106
- workDir: config.workDir,
107
- skills,
108
- systemPromptExtra: config.systemPromptExtra,
109
- mode: currentMode,
110
- });
111
-
112
- let iteration = 0;
113
- const maxIterations = config.maxIterations ?? 200;
114
-
115
- // Restore from checkpoint or start fresh
116
- if (config.restoreCheckpoint) {
117
- const cp = config.restoreCheckpoint;
118
- context.restoreMessages(cp.messages);
119
- todoManager.restore(cp.todos);
120
- iteration = cp.iteration;
121
- emitter.emit({ type: "checkpoint-restored", id: cp.id, iteration: cp.iteration });
122
-
123
- // Inject new user message as continuation
124
- context.addUser(config.userMessage);
125
- } else {
126
- context.addSystem(systemPrompt);
127
- context.addUser(config.userMessage, config.images);
128
- }
129
-
130
- while (iteration < maxIterations) {
131
- iteration++;
132
-
133
- // Process stdin commands (inject, cancel, hitl-response, rollback)
134
- for (const cmd of stdin.drain()) {
135
- switch (cmd.type) {
136
- case "inject":
137
- context.addUser(cmd.text, cmd.images);
138
- emitter.emit({ type: "inject-received", text: cmd.text });
139
- break;
140
- case "hitl-response":
141
- // Forward to file-based HITL (stdin is an alternative transport)
142
- await hitl.writeResponse(cmd.id, { decision: cmd.decision, data: cmd.data });
143
- break;
144
- case "confirm-plan": {
145
- if (currentMode !== "plan") break;
146
- const prevMode = currentMode;
147
- currentMode = "implement";
148
-
149
- // Context reset: extract plan, clear planning messages, inject plan as system context
150
- const { newMessages, planContent } = resetContextForImplementation(context.messages());
151
- context.restoreMessages(newMessages);
152
-
153
- // Save confirmed plan to disk
154
- if (planContent) {
155
- const planDir = `${config.workDir}/.strayl/plans`;
156
- const fs = await import("node:fs/promises");
157
- const path = await import("node:path");
158
- await fs.mkdir(planDir, { recursive: true });
159
- const planFile = path.join(planDir, `${config.sessionId}-${Date.now()}.md`);
160
- await fs.writeFile(planFile, planContent);
161
- emitter.emit({ type: "plan-confirmed", plan: planContent });
162
- }
163
-
164
- emitter.emit({ type: "mode-changed", from: prevMode, to: currentMode });
165
-
166
- // Inject implementation mode prompt into context
167
- const { IMPLEMENTATION_MODE_PROMPT } = await import("./prompts/system.js");
168
- context.addUser(`[System] The plan has been confirmed. Switch to implementation mode.\n\n${IMPLEMENTATION_MODE_PROMPT}`);
169
- break;
170
- }
171
- case "rollback": {
172
- const cp = cmd.checkpoint_id
173
- ? checkpoints.get(cmd.checkpoint_id)
174
- : cmd.iteration != null
175
- ? checkpoints.getByIteration(cmd.iteration)
176
- : checkpoints.latest();
177
- if (cp) {
178
- context.restoreMessages(cp.messages);
179
- todoManager.restore(cp.todos);
180
- iteration = cp.iteration;
181
- emitter.emit({ type: "checkpoint-restored", id: cp.id, iteration: cp.iteration });
182
- } else {
183
- emitter.emit({ type: "error", message: "Checkpoint not found", recoverable: true });
184
- }
185
- break;
186
- }
187
- }
188
- }
189
-
190
- // Check cancellation (stdin or file-based)
191
- if (stdin.isCancelled() || await hitl.isCancelled()) {
192
- stdin.stop();
193
- emitter.emit({ type: "session-end", usage: context.totalUsage(), exit_reason: "cancelled" });
194
- return;
195
- }
196
-
197
- // Pre-summarization (non-blocking)
198
- context.maybeTriggerPreSummarization(client, emitter);
199
-
200
- // Apply pending summary if ready
201
- await context.applyPendingSummary(emitter);
202
-
203
- // Hard summarization (blocking)
204
- if (context.shouldSummarize()) {
205
- emitter.emit({
206
- type: "summarizing",
207
- token_count: context.estimateTokens(),
208
- threshold: config.summarizationThreshold ?? 140_000,
209
- });
210
- await context.summarize(client, emitter);
211
- }
212
-
213
- // Hard trim if still over limit
214
- if (context.estimateTokens() > context.maxInputTokens) {
215
- context.applyTrim();
216
- }
217
-
218
- // Prepare messages + tools
219
- let messages = context.messages();
220
- let tools = registry.toOpenAITools(new Set(config.blockedTools));
221
-
222
- // Apply middleware
223
- for (const mw of middleware) {
224
- if (mw.beforeModel) messages = mw.beforeModel(messages);
225
- if (mw.filterTools) tools = mw.filterTools(tools);
226
- }
227
-
228
- // Stream LLM response
229
- let assistantText = "";
230
- const completedToolCalls: ToolCall[] = [];
231
- const partialArgs = new Map<number, { id: string; name: string; args: string }>();
232
-
233
- let cancelledDuringStream = false;
234
- try {
235
- for await (const chunk of client.stream(messages, tools)) {
236
- // Check for cancellation during LLM streaming
237
- if (stdin.isCancelled()) {
238
- cancelledDuringStream = true;
239
- break;
240
- }
241
-
242
- switch (chunk.type) {
243
- case "text":
244
- assistantText += chunk.text;
245
- emitter.emit({ type: "text-delta", text: chunk.text });
246
- break;
247
-
248
- case "reasoning":
249
- emitter.emit({ type: "reasoning-delta", text: chunk.text });
250
- break;
251
-
252
- case "tool_call_delta": {
253
- const partial = partialArgs.get(chunk.index) ?? { id: "", name: "", args: "" };
254
- if (chunk.id) partial.id = chunk.id;
255
- if (chunk.name) partial.name = chunk.name;
256
- partial.args += chunk.arguments;
257
- partialArgs.set(chunk.index, partial);
258
- break;
259
- }
260
-
261
- case "tool_call_complete":
262
- completedToolCalls.push({
263
- id: chunk.id,
264
- type: "function",
265
- function: { name: chunk.name, arguments: chunk.arguments },
266
- });
267
- break;
268
-
269
- case "usage": {
270
- context.recordUsage(chunk);
271
- const used = context.estimateTokens();
272
- const max = context.maxInputTokens;
273
- const leftPercent = Math.max(0, Math.round((1 - used / max) * 100));
274
- emitter.emit({
275
- type: "usage-update",
276
- input_tokens: chunk.input_tokens,
277
- output_tokens: chunk.output_tokens,
278
- cost: chunk.cost,
279
- peak_input_tokens: context.peakInputTokens(),
280
- context_left_percent: leftPercent,
281
- });
282
- break;
283
- }
284
- }
285
- }
286
- } catch (e) {
287
- const msg = e instanceof Error ? e.message : String(e);
288
- emitter.emit({ type: "error", message: `LLM error: ${msg}`, recoverable: true });
289
-
290
- // Add error as assistant message so loop can continue
291
- context.addAssistant(`[Error communicating with model: ${msg}]`);
292
- continue;
293
- }
294
-
295
- // If cancelled during stream, exit immediately
296
- if (cancelledDuringStream) {
297
- if (assistantText) {
298
- context.addAssistant(assistantText);
299
- }
300
- stdin.stop();
301
- emitter.emit({ type: "session-end", usage: context.totalUsage(), exit_reason: "cancelled" });
302
- return;
303
- }
304
-
305
- // Add assistant message to context
306
- context.addAssistant(assistantText, completedToolCalls.length > 0 ? completedToolCalls : undefined);
307
-
308
- // Emit context status after each LLM call
309
- {
310
- const used = context.estimateTokens();
311
- const max = context.maxInputTokens;
312
- const leftPercent = Math.max(0, Math.round((1 - used / max) * 100));
313
- const usage = context.totalUsage();
314
- emitter.emit({
315
- type: "usage-update",
316
- input_tokens: usage.input_tokens,
317
- output_tokens: usage.output_tokens,
318
- cost: usage.cost,
319
- peak_input_tokens: context.peakInputTokens(),
320
- context_left_percent: leftPercent,
321
- });
322
- }
323
-
324
- // No tool calls = agent is done
325
- if (completedToolCalls.length === 0) break;
326
-
327
- // Execute tool calls
328
- for (const tc of completedToolCalls) {
329
- // Check cancellation between tool executions
330
- if (stdin.isCancelled()) {
331
- // Add cancelled tool results so context stays valid
332
- context.addToolResult(tc.id, tc.function.name, JSON.stringify({ error: "Cancelled by user." }));
333
- emitter.emit({ type: "tool-result", id: tc.id, name: tc.function.name, output: "Cancelled by user.", error: "Cancelled by user." });
334
- break;
335
- }
336
-
337
- let parsedArgs: unknown;
338
- try {
339
- parsedArgs = JSON.parse(tc.function.arguments);
340
- } catch {
341
- parsedArgs = {};
342
- }
343
-
344
- // Emit semantic activity for UI indicators
345
- const activity = toolToActivity(tc.function.name, parsedArgs);
346
- if (activity) {
347
- emitter.emit({ type: "activity", ...activity });
348
- }
349
-
350
- emitter.emit({ type: "tool-call-start", id: tc.id, name: tc.function.name, args: parsedArgs });
351
-
352
- // HITL interrupt check
353
- const toolDef = registry.get(tc.function.name);
354
- if (toolDef?.hitl) {
355
- emitter.emit({ type: "hitl-request", id: tc.id, safe_id: hitl.safeId(tc.id), tool: tc.function.name, args: parsedArgs });
356
- const response = await hitl.waitForResponse(tc.id);
357
-
358
- if (response.decision === "reject") {
359
- const rejectResult = JSON.stringify({ error: "User rejected this action." });
360
- emitter.emit({ type: "tool-result", id: tc.id, name: tc.function.name, output: rejectResult });
361
- context.addToolResult(tc.id, tc.function.name, rejectResult);
362
- continue;
363
- }
364
-
365
- if (response.decision === "edit" && response.data && typeof response.data === "object") {
366
- parsedArgs = { ...(parsedArgs as Record<string, unknown>), ...(response.data as Record<string, unknown>) };
367
- }
368
-
369
- emitter.emit({ type: "hitl-response", id: tc.id, decision: response.decision, data: response.data });
370
- }
371
-
372
- // Execute with middleware chain
373
- const toolCtx: ToolContext = {
374
- emitter,
375
- workDir: config.workDir,
376
- env: config.env,
377
- sessionId: config.sessionId,
378
- toolCallId: tc.id,
379
- };
380
-
381
- const modifiedTc: ToolCall = {
382
- ...tc,
383
- function: { ...tc.function, arguments: JSON.stringify(parsedArgs) },
384
- };
385
-
386
- const result = await executeTool(registry, modifiedTc, toolCtx, middleware);
387
-
388
- emitter.emit({ type: "tool-result", id: tc.id, name: tc.function.name, output: result });
389
- context.addToolResult(tc.id, tc.function.name, result);
390
-
391
- // Mode transition: enterPlanMode tool → switch to plan mode
392
- if (tc.function.name === "enterPlanMode" && currentMode === "normal") {
393
- const prevMode = currentMode;
394
- currentMode = "plan";
395
- emitter.emit({ type: "mode-changed", from: prevMode, to: currentMode });
396
- }
397
- }
398
-
399
- // Save checkpoint after each complete iteration (LLM response + all tool results)
400
- await checkpoints.save(iteration, context.messages(), todoManager.read(), context.totalUsage(), emitter);
401
- }
402
-
403
- if (iteration >= maxIterations) {
404
- emitter.emit({
405
- type: "error",
406
- message: `Agent exceeded maximum iterations (${maxIterations})`,
407
- recoverable: false,
408
- });
409
- }
410
-
411
- stdin.stop();
412
- emitter.emit({
413
- type: "session-end",
414
- usage: context.totalUsage(),
415
- exit_reason: iteration >= maxIterations ? "max_iterations" : "complete",
416
- });
417
- }
418
-
419
- /** Map tool name → semantic activity for UI indicators */
420
- function toolToActivity(
421
- name: string,
422
- args: unknown,
423
- ): { activity: ActivityType; message?: string } | null {
424
- const a = args as Record<string, unknown> | undefined;
425
- switch (name) {
426
- case "write_todos": {
427
- const todos = a?.todos as unknown[] | undefined;
428
- if (!todos || todos.length === 0) return { activity: "clearing-todos" };
429
- return { activity: "creating-todos" };
430
- }
431
- case "update_todo":
432
- return { activity: "updating-todos" };
433
- case "askUser":
434
- return { activity: "asking-user", message: a?.question as string };
435
- case "writePlan":
436
- return { activity: "planning", message: a?.title as string };
437
- case "enterPlanMode":
438
- return { activity: "planning", message: a?.reason as string };
439
- case "create_database":
440
- return { activity: "creating-database", message: a?.name as string };
441
- case "run_database_query":
442
- return { activity: "running-query" };
443
- case "prepare_database_migration":
444
- case "complete_database_migration":
445
- return { activity: "running-migration" };
446
- case "web_search":
447
- return { activity: "searching-web", message: a?.query as string };
448
- case "web_fetch":
449
- return { activity: "fetching-page", message: a?.url as string };
450
- case "generate_image":
451
- return { activity: "generating-image" };
452
- case "read_file":
453
- return { activity: "reading-file", message: a?.path as string };
454
- case "write_file":
455
- return { activity: "writing-file", message: a?.path as string };
456
- case "edit_file":
457
- return { activity: "editing-file", message: a?.path as string };
458
- case "exec":
459
- return { activity: "running-command", message: (a?.command as string)?.slice(0, 80) };
460
- case "task":
461
- return { activity: "delegating-task", message: a?.description as string };
462
- default:
463
- return null;
464
- }
465
- }