awel 0.1.0
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/LICENSE +200 -0
- package/README.md +98 -0
- package/babel-plugin-awel-source.cjs +79 -0
- package/bin/awel.js +2 -0
- package/dist/cli/agent.d.ts +6 -0
- package/dist/cli/agent.js +266 -0
- package/dist/cli/babel-setup.d.ts +1 -0
- package/dist/cli/babel-setup.js +180 -0
- package/dist/cli/comment-popup.d.ts +2 -0
- package/dist/cli/comment-popup.js +206 -0
- package/dist/cli/config.d.ts +14 -0
- package/dist/cli/config.js +29 -0
- package/dist/cli/devserver.d.ts +17 -0
- package/dist/cli/devserver.js +43 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +34 -0
- package/dist/cli/inspector.d.ts +2 -0
- package/dist/cli/inspector.js +117 -0
- package/dist/cli/logger.d.ts +10 -0
- package/dist/cli/logger.js +40 -0
- package/dist/cli/plan-store.d.ts +14 -0
- package/dist/cli/plan-store.js +18 -0
- package/dist/cli/providers/registry.d.ts +17 -0
- package/dist/cli/providers/registry.js +112 -0
- package/dist/cli/providers/types.d.ts +17 -0
- package/dist/cli/providers/types.js +1 -0
- package/dist/cli/providers/vercel.d.ts +4 -0
- package/dist/cli/providers/vercel.js +483 -0
- package/dist/cli/proxy.d.ts +5 -0
- package/dist/cli/proxy.js +72 -0
- package/dist/cli/server.d.ts +7 -0
- package/dist/cli/server.js +104 -0
- package/dist/cli/session.d.ts +32 -0
- package/dist/cli/session.js +77 -0
- package/dist/cli/skills/react-best-practices.md +2934 -0
- package/dist/cli/skills/skills/react-best-practices.md +2934 -0
- package/dist/cli/sse.d.ts +17 -0
- package/dist/cli/sse.js +51 -0
- package/dist/cli/subprocess.d.ts +30 -0
- package/dist/cli/subprocess.js +163 -0
- package/dist/cli/tools/ask-user.d.ts +11 -0
- package/dist/cli/tools/ask-user.js +28 -0
- package/dist/cli/tools/bash.d.ts +4 -0
- package/dist/cli/tools/bash.js +30 -0
- package/dist/cli/tools/code-search.d.ts +4 -0
- package/dist/cli/tools/code-search.js +70 -0
- package/dist/cli/tools/edit.d.ts +6 -0
- package/dist/cli/tools/edit.js +37 -0
- package/dist/cli/tools/glob.d.ts +4 -0
- package/dist/cli/tools/glob.js +29 -0
- package/dist/cli/tools/grep.d.ts +5 -0
- package/dist/cli/tools/grep.js +146 -0
- package/dist/cli/tools/index.d.ts +86 -0
- package/dist/cli/tools/index.js +41 -0
- package/dist/cli/tools/ls.d.ts +3 -0
- package/dist/cli/tools/ls.js +31 -0
- package/dist/cli/tools/multi-edit.d.ts +8 -0
- package/dist/cli/tools/multi-edit.js +53 -0
- package/dist/cli/tools/propose-plan.d.ts +4 -0
- package/dist/cli/tools/propose-plan.js +21 -0
- package/dist/cli/tools/react-best-practices.d.ts +3 -0
- package/dist/cli/tools/react-best-practices.js +55 -0
- package/dist/cli/tools/read.d.ts +3 -0
- package/dist/cli/tools/read.js +24 -0
- package/dist/cli/tools/restart-dev-server.d.ts +3 -0
- package/dist/cli/tools/restart-dev-server.js +18 -0
- package/dist/cli/tools/todo.d.ts +8 -0
- package/dist/cli/tools/todo.js +59 -0
- package/dist/cli/tools/web-fetch.d.ts +5 -0
- package/dist/cli/tools/web-fetch.js +116 -0
- package/dist/cli/tools/web-search.d.ts +5 -0
- package/dist/cli/tools/web-search.js +74 -0
- package/dist/cli/tools/write.d.ts +4 -0
- package/dist/cli/tools/write.js +26 -0
- package/dist/cli/types.d.ts +16 -0
- package/dist/cli/types.js +2 -0
- package/dist/cli/undo.d.ts +49 -0
- package/dist/cli/undo.js +212 -0
- package/dist/cli/verbose.d.ts +7 -0
- package/dist/cli/verbose.js +60 -0
- package/dist/dashboard/assets/index-Bk--q3wu.js +313 -0
- package/dist/dashboard/assets/index-DkWV03So.css +1 -0
- package/dist/dashboard/index.html +16 -0
- package/dist/host/host.js +274 -0
- package/package.json +67 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
import { streamText, stepCountIs } from 'ai';
|
|
2
|
+
import { createOpenAI } from '@ai-sdk/openai';
|
|
3
|
+
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
4
|
+
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
5
|
+
import { claudeCode } from 'ai-sdk-provider-claude-code';
|
|
6
|
+
import { createQwen } from 'qwen-ai-provider';
|
|
7
|
+
import { createMinimax } from 'vercel-minimax-ai-provider';
|
|
8
|
+
import { pauseDevServer, resumeDevServer } from '../devserver.js';
|
|
9
|
+
import { addToHistory, writeSSEEvent } from '../sse.js';
|
|
10
|
+
import { awelTools } from '../tools/index.js';
|
|
11
|
+
import { storePlan } from '../plan-store.js';
|
|
12
|
+
import { startUndoSession, endUndoSession, pushSnapshot, getCurrentSessionStats } from '../undo.js';
|
|
13
|
+
import { resolve } from 'path';
|
|
14
|
+
import { logEvent } from '../verbose.js';
|
|
15
|
+
const SYSTEM_PROMPT = `You are Awel, an expert AI coding assistant. You help users build, modify, and understand their code projects.
|
|
16
|
+
|
|
17
|
+
You have access to these tools:
|
|
18
|
+
- Read: Read file contents
|
|
19
|
+
- Write: Create or overwrite files (creates parent directories automatically)
|
|
20
|
+
- Edit: Find-and-replace edits in files
|
|
21
|
+
- Bash: Execute shell commands
|
|
22
|
+
- Glob: Find files by glob pattern
|
|
23
|
+
- Ls: List directory contents
|
|
24
|
+
- ProposePlan: Propose a structured implementation plan before executing complex tasks
|
|
25
|
+
- AskUser: Ask the user clarifying questions with selectable options
|
|
26
|
+
- ReactBestPractices: Get React/Next.js performance best practices (40+ rules). Call with a section name or "all".
|
|
27
|
+
- Grep: Search file contents for a regex pattern (find function definitions, variable usage, string matches)
|
|
28
|
+
- MultiEdit: Apply multiple find-and-replace edits to a single file in one call
|
|
29
|
+
- WebSearch: Search the web for real-time information (documentation, error messages, APIs, libraries)
|
|
30
|
+
- WebFetch: Fetch content from a URL and return it as markdown, plain text, or raw HTML
|
|
31
|
+
- CodeSearch: Search the web for code examples, API docs, and SDK references
|
|
32
|
+
- TodoRead: Read the current task list to check progress
|
|
33
|
+
- TodoWrite: Create or update the task list to track multi-step work
|
|
34
|
+
- RestartDevServer: Restart the user's dev server if it has crashed, is unresponsive, or needs a restart after config changes
|
|
35
|
+
|
|
36
|
+
React Best Practices:
|
|
37
|
+
- When writing, reviewing, or refactoring React/Next.js code, use the ReactBestPractices tool to consult the performance guide. Request a specific section (e.g. "bundle", "rerender") when you know the area, or "all" for the full guide.
|
|
38
|
+
|
|
39
|
+
Guidelines:
|
|
40
|
+
- Always read a file before editing it
|
|
41
|
+
- Use relative paths when possible
|
|
42
|
+
- Be concise in explanations
|
|
43
|
+
- When making changes, explain what you did and why
|
|
44
|
+
- If a task requires multiple steps, work through them methodically
|
|
45
|
+
|
|
46
|
+
Plan Mode:
|
|
47
|
+
- When a user's request involves changes to 2 or more files, or any non-trivial multi-step work, you MUST use the ProposePlan tool FIRST before making any changes.
|
|
48
|
+
- Write a detailed markdown plan (up to 600-700 words for complex projects) covering: an overview of the approach, step-by-step implementation details, which files will be modified or created, and critical considerations or trade-offs.
|
|
49
|
+
- After calling ProposePlan, STOP and wait for the user to approve the plan or provide feedback.
|
|
50
|
+
- Do NOT begin executing file changes until the user has approved your plan.
|
|
51
|
+
- If the user provides feedback, revise your plan and call ProposePlan again with the updated plan.
|
|
52
|
+
|
|
53
|
+
Clarifying Questions:
|
|
54
|
+
- When a user's request is ambiguous or has multiple valid approaches, use the AskUser tool to ask clarifying questions before proceeding.
|
|
55
|
+
- Present 1-4 questions, each with 2-4 concrete options. Use multiSelect when choices are not mutually exclusive.
|
|
56
|
+
- Keep header labels short (max 12 chars). Put the recommended option first with "(Recommended)" in the label.
|
|
57
|
+
- CRITICAL: All fields in the AskUser tool must be plain text. Do NOT use markdown formatting (no **, no ##, no \`code\`, no bullet points, no lists). The UI renders these strings directly in a structured card — markdown will display as raw characters.
|
|
58
|
+
- After calling AskUser, STOP and wait for the user's answers before continuing.
|
|
59
|
+
|
|
60
|
+
Inspector Context:
|
|
61
|
+
- When a prompt includes [Inspector Context], the user selected a specific HTML tag using the visual inspector.
|
|
62
|
+
- The context has two sections: "Selected Tag" (the exact element clicked) and "Parent Component Context" (the surrounding component for reference).
|
|
63
|
+
- CRITICAL: Focus your changes on the specific selected tag, NOT the entire parent component. The rendered HTML attributes help you locate the exact JSX element in the source code.
|
|
64
|
+
- Use the parent component source code only as context to find and modify the specific tag.
|
|
65
|
+
- Prioritize addressing what the user sees: if props are undefined/null, investigate why.
|
|
66
|
+
- Reference the specific line numbers from the context when making edits.`;
|
|
67
|
+
/** Detects files Claude Code uses for plan output (.claude/plans/*.md, plan.md) */
|
|
68
|
+
function isPlanFile(filePath) {
|
|
69
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
70
|
+
if (/\.claude\/plans\/[^/]+\.md$/.test(normalized))
|
|
71
|
+
return true;
|
|
72
|
+
const basename = normalized.split('/').pop()?.toLowerCase() || '';
|
|
73
|
+
return /^\.?plan\.md$/.test(basename);
|
|
74
|
+
}
|
|
75
|
+
/** Extracts a title and body from plan markdown content */
|
|
76
|
+
function parsePlanContent(raw) {
|
|
77
|
+
const lines = raw.split('\n');
|
|
78
|
+
const headingIdx = lines.findIndex(l => /^#\s+/.test(l));
|
|
79
|
+
if (headingIdx !== -1) {
|
|
80
|
+
const title = lines[headingIdx].replace(/^#\s+/, '').trim();
|
|
81
|
+
const content = [...lines.slice(0, headingIdx), ...lines.slice(headingIdx + 1)].join('\n').trim();
|
|
82
|
+
return { title, content };
|
|
83
|
+
}
|
|
84
|
+
return { title: lines[0]?.trim() || 'Plan', content: lines.slice(1).join('\n').trim() };
|
|
85
|
+
}
|
|
86
|
+
/** Tool names that require pausing the stream for user interaction.
|
|
87
|
+
* Covers both custom tool names (non-Anthropic) and Claude Code SDK names. */
|
|
88
|
+
const ASK_USER_TOOLS = new Set(['AskUser', 'AskUserQuestion']);
|
|
89
|
+
const PLAN_TOOLS = new Set(['ProposePlan', 'EnterPlanMode', 'ExitPlanMode']);
|
|
90
|
+
const INTERACTIVE_TOOLS = new Set([...ASK_USER_TOOLS, ...PLAN_TOOLS]);
|
|
91
|
+
function createModel(modelId, providerType, cwd) {
|
|
92
|
+
if (providerType === 'claude-code') {
|
|
93
|
+
return claudeCode(modelId, {
|
|
94
|
+
allowedTools: ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Ls', 'ProposePlan', 'AskUser'],
|
|
95
|
+
cwd,
|
|
96
|
+
permissionMode: 'acceptEdits',
|
|
97
|
+
streamingInput: 'always',
|
|
98
|
+
maxTurns: 25,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
else if (providerType === 'anthropic') {
|
|
102
|
+
const anthropic = createAnthropic({});
|
|
103
|
+
return anthropic(modelId);
|
|
104
|
+
}
|
|
105
|
+
else if (providerType === 'openai') {
|
|
106
|
+
const openai = createOpenAI({});
|
|
107
|
+
return openai(modelId);
|
|
108
|
+
}
|
|
109
|
+
else if (providerType === 'google-ai') {
|
|
110
|
+
const google = createGoogleGenerativeAI({});
|
|
111
|
+
return google(modelId);
|
|
112
|
+
}
|
|
113
|
+
else if (providerType === 'qwen') {
|
|
114
|
+
const qwen = createQwen({});
|
|
115
|
+
// qwen-ai-provider returns LanguageModelV1; AI SDK v6 handles v1 models
|
|
116
|
+
// at runtime but the type signature expects v2/v3 — cast to satisfy tsc.
|
|
117
|
+
return qwen(modelId);
|
|
118
|
+
}
|
|
119
|
+
else if (providerType === 'minimax') {
|
|
120
|
+
const minimax = createMinimax({});
|
|
121
|
+
return minimax(modelId);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
// vercel-gateway: pass model ID string directly to streamText
|
|
125
|
+
// ai v6 routes through the gateway using AI_GATEWAY_API_KEY env var
|
|
126
|
+
return modelId;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export function createVercelProvider(modelId, providerType) {
|
|
130
|
+
return {
|
|
131
|
+
async streamResponse(stream, messages, config) {
|
|
132
|
+
const PROVIDER_LABELS = {
|
|
133
|
+
'claude-code': 'Claude Code',
|
|
134
|
+
anthropic: 'Anthropic',
|
|
135
|
+
openai: 'OpenAI',
|
|
136
|
+
'google-ai': 'Google AI',
|
|
137
|
+
'vercel-gateway': 'Vercel AI Gateway',
|
|
138
|
+
qwen: 'Qwen',
|
|
139
|
+
minimax: 'MiniMax',
|
|
140
|
+
};
|
|
141
|
+
const providerLabel = PROVIDER_LABELS[providerType];
|
|
142
|
+
await writeSSEEvent(stream, 'status', {
|
|
143
|
+
type: 'status',
|
|
144
|
+
message: `Connecting to ${providerLabel}...`
|
|
145
|
+
});
|
|
146
|
+
// Extract the last user message text for use in storePlan's originalPrompt
|
|
147
|
+
const lastUserMsg = [...messages].reverse().find(m => m.role === 'user');
|
|
148
|
+
const lastUserPrompt = typeof lastUserMsg?.content === 'string'
|
|
149
|
+
? lastUserMsg.content
|
|
150
|
+
: '';
|
|
151
|
+
// Self-contained providers have built-in tools, system prompt, and execution
|
|
152
|
+
// loop via the `cwd` config — they don't need Awel's tools or system prompt.
|
|
153
|
+
const isSelfContained = providerType === 'claude-code';
|
|
154
|
+
const model = createModel(modelId, providerType, config.projectCwd);
|
|
155
|
+
const tools = isSelfContained ? undefined : awelTools(config.projectCwd);
|
|
156
|
+
pauseDevServer(config.targetPort);
|
|
157
|
+
const startTime = Date.now();
|
|
158
|
+
let numTurns = 0;
|
|
159
|
+
// State for intercepting Claude Code native plan mode
|
|
160
|
+
let pendingPlanContent = null;
|
|
161
|
+
let accumulatedText = '';
|
|
162
|
+
let planEmitted = false;
|
|
163
|
+
let inPlanMode = false;
|
|
164
|
+
let waitingForUserInput = false;
|
|
165
|
+
let reasoningActive = false;
|
|
166
|
+
const suppressedToolCallIds = new Set();
|
|
167
|
+
const abortController = new AbortController();
|
|
168
|
+
// Propagate external cancellation (e.g. new chat request) to the internal controller
|
|
169
|
+
if (config.signal) {
|
|
170
|
+
if (config.signal.aborted) {
|
|
171
|
+
abortController.abort();
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
config.signal.addEventListener('abort', () => abortController.abort(), { once: true });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Start undo session to group all file changes from this agent run
|
|
178
|
+
startUndoSession();
|
|
179
|
+
let responseMessages = [];
|
|
180
|
+
try {
|
|
181
|
+
// Self-contained providers handle their own system prompt and tools internally.
|
|
182
|
+
// All other providers get our system prompt + tools.
|
|
183
|
+
const systemPrompt = isSelfContained
|
|
184
|
+
? undefined
|
|
185
|
+
: `${SYSTEM_PROMPT}\n\nThe user's project directory is: ${config.projectCwd}`;
|
|
186
|
+
const streamTextArgs = {
|
|
187
|
+
model,
|
|
188
|
+
...(systemPrompt && { system: systemPrompt }),
|
|
189
|
+
messages,
|
|
190
|
+
tools,
|
|
191
|
+
...(!isSelfContained && { stopWhen: stepCountIs(25) }),
|
|
192
|
+
abortSignal: abortController.signal,
|
|
193
|
+
};
|
|
194
|
+
logEvent('stream:start', `model=${modelId} provider=${providerType} messages=${messages.length}`);
|
|
195
|
+
const result = streamText(streamTextArgs);
|
|
196
|
+
try {
|
|
197
|
+
for await (const part of result.fullStream) {
|
|
198
|
+
switch (part.type) {
|
|
199
|
+
case 'text-delta': {
|
|
200
|
+
logEvent('text-delta', part.text);
|
|
201
|
+
accumulatedText += part.text;
|
|
202
|
+
if (inPlanMode)
|
|
203
|
+
break;
|
|
204
|
+
const textData = JSON.stringify({
|
|
205
|
+
type: 'text',
|
|
206
|
+
text: part.text,
|
|
207
|
+
model: modelId
|
|
208
|
+
});
|
|
209
|
+
addToHistory('text', textData);
|
|
210
|
+
await stream.writeSSE({ event: 'text', data: textData });
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
case 'tool-call': {
|
|
214
|
+
logEvent('tool-call', `${part.toolName} ${JSON.stringify(part.input).slice(0, 200)}`);
|
|
215
|
+
// Intercept ProposePlan — emit as a plan SSE event
|
|
216
|
+
if (part.toolName === 'ProposePlan') {
|
|
217
|
+
const input = part.input;
|
|
218
|
+
const planId = crypto.randomUUID();
|
|
219
|
+
storePlan({
|
|
220
|
+
planId,
|
|
221
|
+
plan: { title: input.title, content: input.content },
|
|
222
|
+
originalPrompt: lastUserPrompt,
|
|
223
|
+
modelId,
|
|
224
|
+
approved: false,
|
|
225
|
+
});
|
|
226
|
+
const planData = JSON.stringify({
|
|
227
|
+
type: 'plan',
|
|
228
|
+
planId,
|
|
229
|
+
planTitle: input.title,
|
|
230
|
+
planContent: input.content,
|
|
231
|
+
});
|
|
232
|
+
addToHistory('plan', planData);
|
|
233
|
+
await stream.writeSSE({ event: 'plan', data: planData });
|
|
234
|
+
waitingForUserInput = true;
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
// Intercept AskUser / AskUserQuestion — emit as a question SSE event
|
|
238
|
+
if (ASK_USER_TOOLS.has(part.toolName)) {
|
|
239
|
+
const input = part.input;
|
|
240
|
+
const questionId = crypto.randomUUID();
|
|
241
|
+
const questionData = JSON.stringify({
|
|
242
|
+
type: 'question',
|
|
243
|
+
questionId,
|
|
244
|
+
questions: input.questions,
|
|
245
|
+
});
|
|
246
|
+
addToHistory('question', questionData);
|
|
247
|
+
await stream.writeSSE({ event: 'question', data: questionData });
|
|
248
|
+
waitingForUserInput = true;
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
// Intercept Claude Code native plan mode tools
|
|
252
|
+
if (part.toolName === 'EnterPlanMode') {
|
|
253
|
+
inPlanMode = true;
|
|
254
|
+
accumulatedText = '';
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
if (part.toolName === 'ExitPlanMode') {
|
|
258
|
+
inPlanMode = false;
|
|
259
|
+
// Deduplicate — Claude Code may call ExitPlanMode multiple times
|
|
260
|
+
if (planEmitted) {
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
// Prefer Write-captured content, fall back to accumulated text-deltas
|
|
264
|
+
const planContent = pendingPlanContent || accumulatedText;
|
|
265
|
+
if (planContent) {
|
|
266
|
+
const parsed = parsePlanContent(planContent);
|
|
267
|
+
const planId = crypto.randomUUID();
|
|
268
|
+
storePlan({
|
|
269
|
+
planId,
|
|
270
|
+
plan: { title: parsed.title, content: parsed.content || planContent },
|
|
271
|
+
originalPrompt: lastUserPrompt,
|
|
272
|
+
modelId,
|
|
273
|
+
approved: false,
|
|
274
|
+
});
|
|
275
|
+
const planData = JSON.stringify({
|
|
276
|
+
type: 'plan',
|
|
277
|
+
planId,
|
|
278
|
+
planTitle: parsed.title,
|
|
279
|
+
planContent: parsed.content || planContent,
|
|
280
|
+
});
|
|
281
|
+
addToHistory('plan', planData);
|
|
282
|
+
await stream.writeSSE({ event: 'plan', data: planData });
|
|
283
|
+
planEmitted = true;
|
|
284
|
+
waitingForUserInput = true;
|
|
285
|
+
}
|
|
286
|
+
pendingPlanContent = null;
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
// Snapshot files before Write/Edit execution for undo support.
|
|
290
|
+
// This is essential for CLI providers (Claude Code, Gemini CLI) whose
|
|
291
|
+
// built-in tools bypass Awel's tool implementations.
|
|
292
|
+
if (part.toolName === 'Write' || part.toolName === 'Edit') {
|
|
293
|
+
const input = part.input;
|
|
294
|
+
const filePath = (input.file_path || input.filePath || '');
|
|
295
|
+
if (filePath) {
|
|
296
|
+
const fullPath = filePath.startsWith('/') ? filePath : resolve(config.projectCwd, filePath);
|
|
297
|
+
pushSnapshot(fullPath);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// Capture Write calls to plan files for ExitPlanMode interception
|
|
301
|
+
if (part.toolName === 'Write') {
|
|
302
|
+
const input = part.input;
|
|
303
|
+
const filePath = (input.file_path || input.filePath || '');
|
|
304
|
+
if (isPlanFile(filePath) && typeof input.content === 'string') {
|
|
305
|
+
pendingPlanContent = input.content;
|
|
306
|
+
suppressedToolCallIds.add(part.toolCallId);
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const toolData = JSON.stringify({
|
|
311
|
+
type: 'tool_use',
|
|
312
|
+
tool: part.toolName,
|
|
313
|
+
input: part.input,
|
|
314
|
+
id: part.toolCallId
|
|
315
|
+
});
|
|
316
|
+
addToHistory('tool_use', toolData);
|
|
317
|
+
await stream.writeSSE({ event: 'tool_use', data: toolData });
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
case 'tool-result': {
|
|
321
|
+
logEvent('tool-result', `${part.toolName} ${typeof part.output === 'string' ? part.output.slice(0, 120) : JSON.stringify(part.output).slice(0, 120)}`);
|
|
322
|
+
// Suppress tool results for intercepted tools
|
|
323
|
+
if (INTERACTIVE_TOOLS.has(part.toolName)
|
|
324
|
+
|| suppressedToolCallIds.delete(part.toolCallId))
|
|
325
|
+
break;
|
|
326
|
+
const resultData = JSON.stringify({
|
|
327
|
+
type: 'tool_result',
|
|
328
|
+
tool_use_id: part.toolCallId,
|
|
329
|
+
tool: part.toolName,
|
|
330
|
+
content: part.output,
|
|
331
|
+
is_error: false
|
|
332
|
+
});
|
|
333
|
+
addToHistory('tool_result', resultData);
|
|
334
|
+
await stream.writeSSE({ event: 'tool_result', data: resultData });
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
case 'finish-step': {
|
|
338
|
+
numTurns++;
|
|
339
|
+
logEvent('finish-step', `turn=${numTurns}`);
|
|
340
|
+
if (inPlanMode) {
|
|
341
|
+
accumulatedText = '';
|
|
342
|
+
}
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
case 'reasoning-start': {
|
|
346
|
+
logEvent('reasoning', 'start');
|
|
347
|
+
reasoningActive = true;
|
|
348
|
+
// Emit a status so the UI shows progress
|
|
349
|
+
const reasoningStatus = JSON.stringify({ type: 'status', message: 'Reasoning...' });
|
|
350
|
+
addToHistory('status', reasoningStatus);
|
|
351
|
+
await stream.writeSSE({ event: 'status', data: reasoningStatus });
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
case 'reasoning-delta': {
|
|
355
|
+
// Some providers surface reasoning text; capture if present
|
|
356
|
+
const rp = part;
|
|
357
|
+
if (rp.text) {
|
|
358
|
+
logEvent('reasoning-delta', rp.text.slice(0, 120));
|
|
359
|
+
}
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
case 'reasoning-end': {
|
|
363
|
+
logEvent('reasoning', 'end');
|
|
364
|
+
reasoningActive = false;
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
case 'tool-input-start':
|
|
368
|
+
case 'tool-input-delta':
|
|
369
|
+
case 'tool-input-end': {
|
|
370
|
+
// Intermediate tool-input streaming; the aggregated tool-call
|
|
371
|
+
// event is what we act on — silently ignore these.
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
case 'tool-error': {
|
|
375
|
+
const te = part;
|
|
376
|
+
const toolErrMsg = te.error instanceof Error
|
|
377
|
+
? te.error.message
|
|
378
|
+
: typeof te.error === 'string'
|
|
379
|
+
? te.error
|
|
380
|
+
: JSON.stringify(te.error);
|
|
381
|
+
logEvent('tool-error', `${te.toolName ?? 'unknown'} ${toolErrMsg}`);
|
|
382
|
+
const toolErrData = JSON.stringify({
|
|
383
|
+
type: 'tool_result',
|
|
384
|
+
tool_use_id: '',
|
|
385
|
+
content: toolErrMsg,
|
|
386
|
+
is_error: true
|
|
387
|
+
});
|
|
388
|
+
addToHistory('tool_result', toolErrData);
|
|
389
|
+
await stream.writeSSE({ event: 'tool_result', data: toolErrData });
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
case 'error': {
|
|
393
|
+
const errorMsg = part.error instanceof Error
|
|
394
|
+
? part.error.message
|
|
395
|
+
: typeof part.error === 'string'
|
|
396
|
+
? part.error
|
|
397
|
+
: JSON.stringify(part.error);
|
|
398
|
+
logEvent('error', errorMsg);
|
|
399
|
+
const errorData = JSON.stringify({
|
|
400
|
+
type: 'error',
|
|
401
|
+
message: errorMsg
|
|
402
|
+
});
|
|
403
|
+
addToHistory('error', errorData);
|
|
404
|
+
await stream.writeSSE({ event: 'error', data: errorData });
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
// Known stream lifecycle events — no action needed
|
|
408
|
+
case 'start':
|
|
409
|
+
case 'start-step':
|
|
410
|
+
case 'text-start':
|
|
411
|
+
case 'text-end':
|
|
412
|
+
case 'source':
|
|
413
|
+
case 'file':
|
|
414
|
+
case 'finish':
|
|
415
|
+
case 'raw':
|
|
416
|
+
break;
|
|
417
|
+
default:
|
|
418
|
+
logEvent('stream:unknown', `type=${part.type}`);
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
// Stop consuming the stream when waiting for user input
|
|
422
|
+
if (waitingForUserInput) {
|
|
423
|
+
logEvent('abort', 'waiting for user input');
|
|
424
|
+
abortController.abort();
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
catch (err) {
|
|
430
|
+
// Ignore abort errors from user-input pauses or external cancellation
|
|
431
|
+
const externallyAborted = config.signal?.aborted;
|
|
432
|
+
if (!waitingForUserInput && !externallyAborted)
|
|
433
|
+
throw err;
|
|
434
|
+
}
|
|
435
|
+
// Capture response messages for multi-turn accumulation.
|
|
436
|
+
// Skip only when externally aborted (new request cancelled this one).
|
|
437
|
+
// For waitingForUserInput, we still try to capture the partial response
|
|
438
|
+
// (e.g. the assistant's plan/question tool call) so the session history
|
|
439
|
+
// stays consistent and avoids orphan user messages that cause 400 errors.
|
|
440
|
+
const externallyAborted = config.signal?.aborted;
|
|
441
|
+
if (!externallyAborted) {
|
|
442
|
+
try {
|
|
443
|
+
const response = await result.response;
|
|
444
|
+
responseMessages = response.messages;
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
// If awaiting response fails (e.g. abort race), leave empty
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (externallyAborted) {
|
|
451
|
+
logEvent('abort', 'externally cancelled');
|
|
452
|
+
}
|
|
453
|
+
// When externally cancelled, skip result events — no client is listening
|
|
454
|
+
if (!externallyAborted) {
|
|
455
|
+
const durationMs = Date.now() - startTime;
|
|
456
|
+
const resultSubtype = waitingForUserInput ? 'waiting_for_input' : 'success';
|
|
457
|
+
const fileStats = getCurrentSessionStats(config.projectCwd);
|
|
458
|
+
const resultData = JSON.stringify({
|
|
459
|
+
type: 'result',
|
|
460
|
+
subtype: resultSubtype,
|
|
461
|
+
duration_ms: durationMs,
|
|
462
|
+
num_turns: numTurns,
|
|
463
|
+
result: waitingForUserInput ? 'waiting_for_input' : 'completed',
|
|
464
|
+
is_error: false,
|
|
465
|
+
...(fileStats && fileStats.length > 0 ? { file_stats: fileStats } : {}),
|
|
466
|
+
});
|
|
467
|
+
logEvent('stream:end', `duration=${durationMs}ms turns=${numTurns} result=${resultSubtype}`);
|
|
468
|
+
addToHistory('result', resultData);
|
|
469
|
+
await stream.writeSSE({ event: 'result', data: resultData });
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
finally {
|
|
473
|
+
// End undo session so all file changes are grouped together
|
|
474
|
+
endUndoSession();
|
|
475
|
+
resumeDevServer(config.targetPort);
|
|
476
|
+
}
|
|
477
|
+
if (!config.signal?.aborted) {
|
|
478
|
+
await writeSSEEvent(stream, 'done', { type: 'done', message: 'Agent completed' });
|
|
479
|
+
}
|
|
480
|
+
return responseMessages;
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates proxy middleware that forwards requests to the target app
|
|
3
|
+
* and injects the Awel host script into HTML responses.
|
|
4
|
+
*/
|
|
5
|
+
export declare function createProxyMiddleware(targetPort: number, projectCwd?: string): (c: any, _next: () => Promise<void>) => Promise<any>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates proxy middleware that forwards requests to the target app
|
|
3
|
+
* and injects the Awel host script into HTML responses.
|
|
4
|
+
*/
|
|
5
|
+
export function createProxyMiddleware(targetPort, projectCwd) {
|
|
6
|
+
return async (c, _next) => {
|
|
7
|
+
const url = new URL(c.req.url);
|
|
8
|
+
const targetUrl = `http://localhost:${targetPort}${url.pathname}${url.search}`;
|
|
9
|
+
try {
|
|
10
|
+
// Clone headers and remove Accept-Encoding to get uncompressed response
|
|
11
|
+
const headers = new Headers(c.req.raw.headers);
|
|
12
|
+
headers.delete('accept-encoding');
|
|
13
|
+
const response = await fetch(targetUrl, {
|
|
14
|
+
method: c.req.method,
|
|
15
|
+
headers,
|
|
16
|
+
body: c.req.method !== 'GET' && c.req.method !== 'HEAD'
|
|
17
|
+
? await c.req.raw.arrayBuffer()
|
|
18
|
+
: undefined,
|
|
19
|
+
});
|
|
20
|
+
const contentType = response.headers.get('content-type') || '';
|
|
21
|
+
// If it's HTML, inject our script
|
|
22
|
+
if (contentType.includes('text/html')) {
|
|
23
|
+
let html = await response.text();
|
|
24
|
+
// Inject project CWD (for source-map resolution) and the host script
|
|
25
|
+
const cwdScript = projectCwd
|
|
26
|
+
? `<script>window.__AWEL_PROJECT_CWD__=${JSON.stringify(projectCwd)}</script>`
|
|
27
|
+
: '';
|
|
28
|
+
// Constrain Next.js error overlay stacking context below Awel's UI.
|
|
29
|
+
// Covers both legacy and modern Next.js error overlay custom elements.
|
|
30
|
+
const awelOverlayStyle = `<style id="awel-overlay-fix">nextjs-portal, nextjs-portal-root, next-error-overlay, nextjs-dev-tools { position: relative !important; z-index: 999997 !important; }</style>`;
|
|
31
|
+
const scriptTag = `${awelOverlayStyle}${cwdScript}<script src="/_awel/host.js"></script>`;
|
|
32
|
+
// Inject into <head> so the script loads early — even on error pages
|
|
33
|
+
// where the body might be minimal or replaced by Next.js error recovery.
|
|
34
|
+
// Fall back to </body> then </html> if <head> isn't found.
|
|
35
|
+
if (html.includes('</head>')) {
|
|
36
|
+
html = html.replace('</head>', `${scriptTag}</head>`);
|
|
37
|
+
}
|
|
38
|
+
else if (html.includes('</body>')) {
|
|
39
|
+
html = html.replace('</body>', `${scriptTag}</body>`);
|
|
40
|
+
}
|
|
41
|
+
else if (html.includes('</html>')) {
|
|
42
|
+
html = html.replace('</html>', `${scriptTag}</html>`);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
html += scriptTag;
|
|
46
|
+
}
|
|
47
|
+
const responseHeaders = new Headers(response.headers);
|
|
48
|
+
responseHeaders.delete('content-encoding');
|
|
49
|
+
responseHeaders.delete('content-length');
|
|
50
|
+
responseHeaders.set('content-type', 'text/html; charset=utf-8');
|
|
51
|
+
return new Response(html, {
|
|
52
|
+
status: response.status,
|
|
53
|
+
headers: responseHeaders,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
// For non-HTML responses, pass through as-is but clean up encoding headers
|
|
57
|
+
const body = await response.arrayBuffer();
|
|
58
|
+
const responseHeaders = new Headers(response.headers);
|
|
59
|
+
// Remove content-encoding since we've already decoded the response
|
|
60
|
+
responseHeaders.delete('content-encoding');
|
|
61
|
+
responseHeaders.delete('content-length');
|
|
62
|
+
return new Response(body, {
|
|
63
|
+
status: response.status,
|
|
64
|
+
headers: responseHeaders,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
// Target app might not be ready yet
|
|
69
|
+
return c.text('Waiting for target app to start...', 503);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|