@strayl/agent 0.1.3 → 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.
- package/package.json +5 -1
- package/skills/api-creation/SKILL.md +631 -0
- package/skills/authentication/SKILL.md +294 -0
- package/skills/frontend-design/SKILL.md +108 -0
- package/skills/landing-creation/SKILL.md +125 -0
- package/skills/reference/SKILL.md +149 -0
- package/skills/web-application-creation/SKILL.md +231 -0
- package/src/agent.ts +0 -465
- package/src/checkpoints/manager.ts +0 -112
- package/src/context/manager.ts +0 -185
- package/src/context/summarizer.ts +0 -104
- package/src/context/trim.ts +0 -55
- package/src/emitter.ts +0 -14
- package/src/hitl/manager.ts +0 -77
- package/src/hitl/transport.ts +0 -13
- package/src/index.ts +0 -116
- package/src/llm/client.ts +0 -276
- package/src/llm/gemini-native.ts +0 -307
- package/src/llm/models.ts +0 -64
- package/src/middleware/compose.ts +0 -24
- package/src/middleware/credential-scrubbing.ts +0 -31
- package/src/middleware/forbidden-packages.ts +0 -107
- package/src/middleware/plan-mode.ts +0 -143
- package/src/middleware/prompt-caching.ts +0 -21
- package/src/middleware/tool-compression.ts +0 -25
- package/src/middleware/tool-filter.ts +0 -13
- package/src/prompts/implementation-mode.md +0 -16
- package/src/prompts/plan-mode.md +0 -51
- package/src/prompts/system.ts +0 -173
- package/src/skills/loader.ts +0 -53
- package/src/stdin-listener.ts +0 -61
- package/src/subagents/definitions.ts +0 -72
- package/src/subagents/manager.ts +0 -161
- package/src/todos/manager.ts +0 -61
- package/src/tools/builtin/delete.ts +0 -29
- package/src/tools/builtin/edit.ts +0 -74
- package/src/tools/builtin/exec.ts +0 -216
- package/src/tools/builtin/glob.ts +0 -104
- package/src/tools/builtin/grep.ts +0 -115
- package/src/tools/builtin/ls.ts +0 -54
- package/src/tools/builtin/move.ts +0 -31
- package/src/tools/builtin/read.ts +0 -69
- package/src/tools/builtin/write.ts +0 -42
- package/src/tools/executor.ts +0 -51
- package/src/tools/external/database.ts +0 -285
- package/src/tools/external/enter-plan-mode.ts +0 -34
- package/src/tools/external/generate-image.ts +0 -110
- package/src/tools/external/hitl-tools.ts +0 -118
- package/src/tools/external/preview.ts +0 -28
- package/src/tools/external/proxy-fetch.ts +0 -51
- package/src/tools/external/task.ts +0 -38
- package/src/tools/external/wait.ts +0 -20
- package/src/tools/external/web-fetch.ts +0 -57
- package/src/tools/external/web-search.ts +0 -61
- package/src/tools/registry.ts +0 -36
- package/src/tools/zod-to-json-schema.ts +0 -86
- 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
|
-
}
|