@shawnstack/quickforge 1.3.23 → 1.3.25
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/README.md +15 -15
- package/dist/assets/anthropic-B1_Yrokl.js +39 -0
- package/dist/assets/azure-openai-responses-UMiOBCBd.js +1 -0
- package/dist/assets/google-BLE_Gcd1.js +1 -0
- package/dist/assets/google-shared-Cqjw1plk.js +11 -0
- package/dist/assets/google-vertex-6_sIZLVc.js +1 -0
- package/dist/assets/{icons-WD3UkVNM.js → icons-Bs7OG8yi.js} +1 -1
- package/dist/assets/{index-CjTN0qaQ.js → index-C3bc5C3k.js} +576 -561
- package/dist/assets/index-C7oT9Rdw.css +3 -0
- package/dist/assets/{mistral-Ber29mja.js → mistral-DmZEmRkv.js} +1 -1
- package/dist/assets/openai-codex-responses-i_SmQGzQ.js +7 -0
- package/dist/assets/openai-completions-BmmZFDDY.js +5 -0
- package/dist/assets/openai-prompt-cache-CErE62Yt.js +1 -0
- package/dist/assets/openai-responses-C8tPdeE9.js +1 -0
- package/dist/assets/{openai-responses-shared-a_PAPxTO.js → openai-responses-shared-DchtjQNp.js} +1 -1
- package/dist/assets/openrouter-CcTv1G_v.js +1 -0
- package/dist/assets/react-vendor-Cu-7p9CI.js +61 -0
- package/dist/assets/sanitize-unicode-BhyPmlyt.js +1 -0
- package/dist/assets/transform-messages-Dhj_4OTw.js +1 -0
- package/dist/index.html +4 -4
- package/package.json +6 -3
- package/server/agent-manager.mjs +144 -151
- package/server/ai-http-logger.mjs +20 -5
- package/server/approval-store.mjs +63 -0
- package/server/custom-commands.mjs +8 -0
- package/server/index.mjs +1 -1
- package/server/message-converters.mjs +79 -0
- package/server/project-config.mjs +7 -9
- package/server/routes/agent-profiles.mjs +1 -1
- package/server/routes/agent.mjs +15 -1
- package/server/routes/filesystem.mjs +18 -2
- package/server/routes/project.mjs +33 -1
- package/server/routes/scheduled-tasks.mjs +1 -1
- package/server/routes/storage.mjs +66 -31
- package/server/routes/terminal.mjs +28 -3
- package/server/routes/workspace.mjs +43 -1
- package/server/session-utils.mjs +1 -1
- package/server/storage.mjs +78 -2
- package/server/terminal/terminal-manager.mjs +12 -0
- package/server/tool-wiring.mjs +87 -0
- package/server/utils/workspace.mjs +20 -1
- package/dist/assets/anthropic-CDKnv1FQ.js +0 -39
- package/dist/assets/azure-openai-responses-BnUwVl-8.js +0 -1
- package/dist/assets/google-DOEyCDZy.js +0 -1
- package/dist/assets/google-shared-CLc4ziON.js +0 -11
- package/dist/assets/google-vertex-BPPf3car.js +0 -1
- package/dist/assets/index-eeLjaV06.css +0 -3
- package/dist/assets/openai-codex-responses-D8gq8a3l.js +0 -7
- package/dist/assets/openai-completions-CATWPFBp.js +0 -5
- package/dist/assets/openai-responses-DxcB6Ksu.js +0 -1
- package/dist/assets/react-vendor-BcQaTQ90.js +0 -9
- package/dist/assets/transform-messages-CmnxG9RB.js +0 -1
- /package/dist/assets/{hash-Bt1aVMQ3.js → hash-kZ2KD_no.js} +0 -0
- /package/dist/assets/{openai-Cn7eGqwa.js → openai-Bf1npfRy.js} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function e(e){return e.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g,``)}export{e as t};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function e(e,t,n){return{temperature:t?.temperature,maxTokens:t?.maxTokens,signal:t?.signal,apiKey:n||t?.apiKey,transport:t?.transport,cacheRetention:t?.cacheRetention,sessionId:t?.sessionId,headers:t?.headers,onPayload:t?.onPayload,onResponse:t?.onResponse,timeoutMs:t?.timeoutMs,maxRetries:t?.maxRetries,maxRetryDelayMs:t?.maxRetryDelayMs,metadata:t?.metadata}}function t(e){return e===`xhigh`?`high`:e}function n(e,n,r,i){let a={minimal:1024,low:2048,medium:8192,high:16384,...i}[t(r)],o=e===void 0?n:Math.min(e+a,n);return o<=a&&(a=Math.max(0,o-1024)),{maxTokens:o,thinkingBudget:a}}var r=`(image omitted: model does not support images)`,i=`(tool image omitted: model does not support images)`;function a(e,t){let n=[],r=!1;for(let i of e){if(i.type===`image`){r||n.push({type:`text`,text:t}),r=!0;continue}n.push(i),r=i.text===t}return n}function o(e,t){return t.input.includes(`image`)?e:e.map(e=>e.role===`user`&&Array.isArray(e.content)?{...e,content:a(e.content,r)}:e.role===`toolResult`?{...e,content:a(e.content,i)}:e)}function s(e,t,n){let r=new Map,i=o(e,t).map(e=>{if(e.role===`user`)return e;if(e.role===`toolResult`){let t=r.get(e.toolCallId);return t&&t!==e.toolCallId?{...e,toolCallId:t}:e}if(e.role===`assistant`){let i=e,a=i.provider===t.provider&&i.api===t.api&&i.model===t.id,o=i.content.flatMap(e=>{if(e.type===`thinking`)return e.redacted?a?e:[]:a&&e.thinkingSignature?e:!e.thinking||e.thinking.trim()===``?[]:a?e:{type:`text`,text:e.thinking};if(e.type===`text`)return a?e:{type:`text`,text:e.text};if(e.type===`toolCall`){let o=e,s=o;if(!a&&o.thoughtSignature&&(s={...o},delete s.thoughtSignature),!a&&n){let e=n(o.id,t,i);e!==o.id&&(r.set(o.id,e),s={...s,id:e})}return s}return e});return{...i,content:o}}return e}),a=[],s=[],c=new Set,l=()=>{if(s.length>0){for(let e of s)c.has(e.id)||a.push({role:`toolResult`,toolCallId:e.id,toolName:e.name,content:[{type:`text`,text:`No result provided`}],isError:!0,timestamp:Date.now()});s=[],c=new Set}};for(let e=0;e<i.length;e++){let t=i[e];if(t.role===`assistant`){l();let e=t;if(e.stopReason===`error`||e.stopReason===`aborted`)continue;let n=e.content.filter(e=>e.type===`toolCall`);n.length>0&&(s=n,c=new Set),a.push(t)}else t.role===`toolResult`?(c.add(t.toolCallId),a.push(t)):(t.role===`user`&&l(),a.push(t))}return l(),a}export{n,e as r,s as t};
|
package/dist/index.html
CHANGED
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
<meta name="apple-mobile-web-app-title" content="QuickForge" />
|
|
12
12
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
13
13
|
<title>速构 QuickForge</title>
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-C3bc5C3k.js"></script>
|
|
15
15
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-CkqCuyE9.js">
|
|
16
16
|
<link rel="modulepreload" crossorigin href="/assets/lit-vendor-Dr3cpBGF.js">
|
|
17
17
|
<link rel="modulepreload" crossorigin href="/assets/css-utils-rkE68RDy.js">
|
|
18
|
-
<link rel="modulepreload" crossorigin href="/assets/icons-
|
|
19
|
-
<link rel="modulepreload" crossorigin href="/assets/react-vendor-
|
|
20
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
18
|
+
<link rel="modulepreload" crossorigin href="/assets/icons-Bs7OG8yi.js">
|
|
19
|
+
<link rel="modulepreload" crossorigin href="/assets/react-vendor-Cu-7p9CI.js">
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/assets/index-C7oT9Rdw.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
23
23
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shawnstack/quickforge",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.25",
|
|
4
4
|
"description": "AI chat application with YOLO-mode local workspace tools. React + Vite + Tailwind CSS frontend, local Node.js storage server.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -42,8 +42,11 @@
|
|
|
42
42
|
"package.json"
|
|
43
43
|
],
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@
|
|
46
|
-
"@
|
|
45
|
+
"@dnd-kit/core": "^6.3.1",
|
|
46
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
47
|
+
"@dnd-kit/utilities": "^3.2.2",
|
|
48
|
+
"@earendil-works/pi-agent-core": "^0.75.3",
|
|
49
|
+
"@earendil-works/pi-ai": "^0.75.3",
|
|
47
50
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
48
51
|
"ws": "^8.20.1"
|
|
49
52
|
},
|
package/server/agent-manager.mjs
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events'
|
|
2
2
|
import { randomUUID } from 'node:crypto'
|
|
3
|
-
import { Agent } from '@
|
|
3
|
+
import { Agent } from '@earendil-works/pi-agent-core'
|
|
4
4
|
import { streamSimpleWithAiHttpLogging } from './ai-http-logger.mjs'
|
|
5
|
-
import {
|
|
5
|
+
import { loadSkillToolContext, abortRunningCommand } from './tools/index.mjs'
|
|
6
6
|
import { createSkillTools, workspaceTools } from './tools/definitions.mjs'
|
|
7
|
-
import {
|
|
7
|
+
import { createMcpToolDefinitions, isMcpToolName } from './mcp/registry.mjs'
|
|
8
8
|
import {
|
|
9
9
|
composeSubagentSystemPrompt,
|
|
10
10
|
formatSubagentTask,
|
|
11
11
|
} from './subagents.mjs'
|
|
12
12
|
import { agentProfileSnapshot, getAgentProfile } from './agent-profiles.mjs'
|
|
13
13
|
import { projectContextFromId, readProjectConfig } from './project-config.mjs'
|
|
14
|
-
import { readStore, atomicUpdate, readSessionValue, writeSessionValue, deleteSessionValue } from './storage.mjs'
|
|
14
|
+
import { readStore, atomicUpdate, atomicSessionMetadataUpdate, readSessionValue, writeSessionValue, deleteSessionValue } from './storage.mjs'
|
|
15
15
|
import { logger } from './utils/logger.mjs'
|
|
16
16
|
import { buildSystemPrompt, generateAiTitle, generateTitle } from './session-utils.mjs'
|
|
17
17
|
import { restoreReasoningContentInPayload } from './reasoning-cache.mjs'
|
|
@@ -30,70 +30,22 @@ import {
|
|
|
30
30
|
parseInternalCommandInvocation,
|
|
31
31
|
resolveCustomCommandInvocation,
|
|
32
32
|
} from './custom-commands.mjs'
|
|
33
|
+
import { omitDetailsForLlm, serverConvertToLlm, messageText, lastAssistantText } from './message-converters.mjs'
|
|
34
|
+
import { isPlainObject, mergeQuickForgeTiming, wrapToolDefinition, wrapMcpToolDefinition, sessionSkillsContext } from './tool-wiring.mjs'
|
|
35
|
+
import {
|
|
36
|
+
APPROVAL_TIMEOUT_MS,
|
|
37
|
+
commandRestrictedTools,
|
|
38
|
+
safeReadTools,
|
|
39
|
+
pendingApprovals,
|
|
40
|
+
pendingAutoCompactApprovals,
|
|
41
|
+
commandToolPermissionError,
|
|
42
|
+
createCommandToolPermissions,
|
|
43
|
+
} from './approval-store.mjs'
|
|
33
44
|
|
|
34
45
|
// ---------------------------------------------------------------------------
|
|
35
46
|
// Tool definitions (server-side, no REST roundtrip)
|
|
36
47
|
// ---------------------------------------------------------------------------
|
|
37
48
|
|
|
38
|
-
function isPlainObject(value) {
|
|
39
|
-
return Boolean(value && typeof value === 'object' && !Array.isArray(value))
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function mergeQuickForgeTiming(details, timing) {
|
|
43
|
-
if (!isPlainObject(details)) return { quickforgeTiming: timing }
|
|
44
|
-
return { ...details, quickforgeTiming: timing }
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function wrapToolDefinition(definition, context, toolPermissions) {
|
|
48
|
-
const handler = toolHandlers[definition.name]
|
|
49
|
-
if (!handler) throw new Error(`Missing handler for tool: ${definition.name}`)
|
|
50
|
-
return {
|
|
51
|
-
...definition,
|
|
52
|
-
execute: async (_toolCallId, params, signal, onUpdate) => {
|
|
53
|
-
if (toolPermissions) {
|
|
54
|
-
const permissionError = toolPermissions(definition.name)
|
|
55
|
-
if (permissionError) throw new Error(permissionError)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const startedAt = Date.now()
|
|
59
|
-
const startedAtPerf = performance.now()
|
|
60
|
-
const result = await handler(params || {}, context, { signal, onUpdate, toolCallId: _toolCallId })
|
|
61
|
-
const finishedAt = Date.now()
|
|
62
|
-
const durationMs = Math.max(0, Math.round(performance.now() - startedAtPerf))
|
|
63
|
-
const details = mergeQuickForgeTiming(result.details, { startedAt, finishedAt, durationMs })
|
|
64
|
-
return {
|
|
65
|
-
content: [{ type: 'text', text: result.content }],
|
|
66
|
-
details: isPlainObject(details) ? { ...details, toolCallId: _toolCallId } : details,
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function wrapMcpToolDefinition(definition, toolPermissions) {
|
|
73
|
-
return {
|
|
74
|
-
...definition,
|
|
75
|
-
execute: async (_toolCallId, params) => {
|
|
76
|
-
if (toolPermissions) {
|
|
77
|
-
const permissionError = toolPermissions(definition.name)
|
|
78
|
-
if (permissionError) throw new Error(permissionError)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const startedAt = Date.now()
|
|
82
|
-
const startedAtPerf = performance.now()
|
|
83
|
-
const result = await callMcpTool(definition.name, params || {})
|
|
84
|
-
const finishedAt = Date.now()
|
|
85
|
-
const durationMs = Math.max(0, Math.round(performance.now() - startedAtPerf))
|
|
86
|
-
if (result.isError) {
|
|
87
|
-
throw new Error(result.content || `MCP tool failed: ${definition.name}`)
|
|
88
|
-
}
|
|
89
|
-
return {
|
|
90
|
-
content: [{ type: 'text', text: result.content }],
|
|
91
|
-
details: mergeQuickForgeTiming(result.details, { startedAt, finishedAt, durationMs }),
|
|
92
|
-
}
|
|
93
|
-
},
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
49
|
function wrapSubagentToolDefinition(definition, parentSessionId) {
|
|
98
50
|
return {
|
|
99
51
|
...definition,
|
|
@@ -154,13 +106,6 @@ async function createServerTools(projectId, projectContext, skillsContext, inclu
|
|
|
154
106
|
return tools
|
|
155
107
|
}
|
|
156
108
|
|
|
157
|
-
function sessionSkillsContext(session) {
|
|
158
|
-
return {
|
|
159
|
-
globalSkillNames: session.globalSkillNames,
|
|
160
|
-
projectSkillNames: session.projectSkillNames,
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
109
|
async function rebuildSessionTools(session) {
|
|
165
110
|
session.agent.state.tools = await createServerTools(
|
|
166
111
|
session.projectId,
|
|
@@ -181,26 +126,8 @@ const agentSessions = new Map()
|
|
|
181
126
|
/** @typedef {{ agent: Agent, projectContext: object|null, projectId: string|null, yoloMode: boolean, model: object, thinkingLevel: string, scope: string, title: string, createdAt: string, status: string, startedAt: string|null, finishedAt: string|null, listeners: Set<function>, idleTimer: NodeJS.Timeout|null, eventBus: EventEmitter }} AgentSession */
|
|
182
127
|
|
|
183
128
|
const IDLE_TIMEOUT_MS = 30 * 60 * 1000 // 30 minutes
|
|
184
|
-
const APPROVAL_TIMEOUT_MS = 5 * 60 * 1000 // 5 minutes for tool approval
|
|
185
129
|
const SUBAGENT_DEFAULT_TIMEOUT_MS = 30 * 60 * 1000
|
|
186
|
-
const
|
|
187
|
-
const safeReadTools = new Set(['read_file', 'grep_files'])
|
|
188
|
-
const pendingApprovals = new Map() // toolCallId → { resolve, reject, sessionId, toolName, args, source, timeout }
|
|
189
|
-
const pendingAutoCompactApprovals = new Map() // approvalId → { resolve, reject, sessionId, timeout }
|
|
190
|
-
|
|
191
|
-
function createCommandToolPermissions(session) {
|
|
192
|
-
return (toolName) => {
|
|
193
|
-
const permissions = session.activeCommandPermissions
|
|
194
|
-
if (!permissions || !commandRestrictedTools.has(toolName)) return null
|
|
195
|
-
if (toolName === 'run_command' && permissions.allowCommands === false) {
|
|
196
|
-
return `Custom command /${session.activeCommandName} does not allow running shell commands.`
|
|
197
|
-
}
|
|
198
|
-
if ((toolName === 'write_file' || toolName === 'edit_file') && permissions.allowEdit === false) {
|
|
199
|
-
return `Custom command /${session.activeCommandName} does not allow editing files.`
|
|
200
|
-
}
|
|
201
|
-
return null
|
|
202
|
-
}
|
|
203
|
-
}
|
|
130
|
+
const SUBAGENT_TRACE_THROTTLE_MS = 150
|
|
204
131
|
|
|
205
132
|
/**
|
|
206
133
|
* Create a Promise that only resolves when the user accepts or rejects the tool call.
|
|
@@ -219,8 +146,14 @@ function createApprovalPromise(session, toolCallId, toolName, args, source) {
|
|
|
219
146
|
resolve({ block: true, reason: `Approval timeout for ${toolName}` })
|
|
220
147
|
}, APPROVAL_TIMEOUT_MS)
|
|
221
148
|
|
|
149
|
+
let onAbort = null
|
|
150
|
+
|
|
222
151
|
const cleanup = () => {
|
|
223
152
|
clearTimeout(timeout)
|
|
153
|
+
if (onAbort) {
|
|
154
|
+
session.agent.signal?.removeEventListener('abort', onAbort)
|
|
155
|
+
onAbort = null
|
|
156
|
+
}
|
|
224
157
|
if (settled) return
|
|
225
158
|
settled = true
|
|
226
159
|
pendingApprovals.delete(toolCallId)
|
|
@@ -234,7 +167,7 @@ function createApprovalPromise(session, toolCallId, toolName, args, source) {
|
|
|
234
167
|
reject(new Error('Run aborted'))
|
|
235
168
|
return
|
|
236
169
|
}
|
|
237
|
-
|
|
170
|
+
onAbort = () => {
|
|
238
171
|
cleanup()
|
|
239
172
|
reject(new Error('Run aborted'))
|
|
240
173
|
}
|
|
@@ -284,8 +217,14 @@ function createAutoCompactApprovalPromise(session, details = {}) {
|
|
|
284
217
|
resolve(false)
|
|
285
218
|
}, APPROVAL_TIMEOUT_MS)
|
|
286
219
|
|
|
220
|
+
let onAbort = null
|
|
221
|
+
|
|
287
222
|
const cleanup = () => {
|
|
288
223
|
clearTimeout(timeout)
|
|
224
|
+
if (onAbort) {
|
|
225
|
+
session.agent.signal?.removeEventListener('abort', onAbort)
|
|
226
|
+
onAbort = null
|
|
227
|
+
}
|
|
289
228
|
if (settled) return
|
|
290
229
|
settled = true
|
|
291
230
|
pendingAutoCompactApprovals.delete(approvalId)
|
|
@@ -298,7 +237,7 @@ function createAutoCompactApprovalPromise(session, details = {}) {
|
|
|
298
237
|
reject(new Error('Run aborted'))
|
|
299
238
|
return
|
|
300
239
|
}
|
|
301
|
-
|
|
240
|
+
onAbort = () => {
|
|
302
241
|
cleanup()
|
|
303
242
|
reject(new Error('Run aborted'))
|
|
304
243
|
}
|
|
@@ -606,6 +545,14 @@ async function resolveCommandState(session, userMessage) {
|
|
|
606
545
|
if (typeof internalResponse === 'string') return { textResponse: internalResponse }
|
|
607
546
|
if (internalResponse?.clear) return { clear: internalResponse }
|
|
608
547
|
if (internalResponse?.compact) return { compact: internalResponse }
|
|
548
|
+
if (internalResponse?.plan) {
|
|
549
|
+
return {
|
|
550
|
+
userMessage,
|
|
551
|
+
commandPrompt: formatPlanCommandPrompt(internalResponse.args),
|
|
552
|
+
permissions: { allowEdit: false, allowCommands: false },
|
|
553
|
+
commandName: 'plan',
|
|
554
|
+
}
|
|
555
|
+
}
|
|
609
556
|
|
|
610
557
|
if (!session.projectContext?.workspaceRoot) return { userMessage }
|
|
611
558
|
|
|
@@ -624,65 +571,32 @@ async function resolveCommandState(session, userMessage) {
|
|
|
624
571
|
}
|
|
625
572
|
}
|
|
626
573
|
|
|
627
|
-
function
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
return copy
|
|
632
|
-
}
|
|
574
|
+
function formatPlanCommandPrompt(task) {
|
|
575
|
+
const taskText = String(task || '').trim()
|
|
576
|
+
return `<plan_command_invocation name="plan">
|
|
577
|
+
This /plan command applies only to the current user request. Generate an implementation plan before execution.
|
|
633
578
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
return messages
|
|
642
|
-
.filter(m => m.role !== 'artifact')
|
|
643
|
-
.map(m => {
|
|
644
|
-
if (m.role === 'user-with-attachments') {
|
|
645
|
-
const textContent = typeof m.content === 'string'
|
|
646
|
-
? [{ type: 'text', text: m.content }]
|
|
647
|
-
: [...m.content]
|
|
648
|
-
if (Array.isArray(m.attachments)) {
|
|
649
|
-
for (const att of m.attachments) {
|
|
650
|
-
if (att.type === 'image' && att.content) {
|
|
651
|
-
textContent.push({ type: 'image', data: att.content, mimeType: att.mimeType })
|
|
652
|
-
} else if (att.type === 'document' && att.extractedText) {
|
|
653
|
-
textContent.push({ type: 'text', text: `\n\n[Document: ${att.fileName}]\n${att.extractedText}` })
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
return omitDetailsForLlm({ ...m, role: 'user', content: textContent })
|
|
658
|
-
}
|
|
659
|
-
if (m.role === 'user' || m.role === 'assistant' || m.role === 'toolResult') return omitDetailsForLlm(m)
|
|
660
|
-
return null
|
|
661
|
-
})
|
|
662
|
-
.filter(Boolean)
|
|
663
|
-
}
|
|
579
|
+
Rules for this turn:
|
|
580
|
+
- Do not modify files.
|
|
581
|
+
- Do not create files.
|
|
582
|
+
- Do not run shell commands.
|
|
583
|
+
- Do not use write_file, edit_file, run_command, or any other state-changing tool.
|
|
584
|
+
- You may use read-only tools such as read_file and grep_files if needed to inspect the project.
|
|
585
|
+
- Output the plan and then stop. Do not start implementation.
|
|
664
586
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
.join('\n')
|
|
673
|
-
.trim()
|
|
674
|
-
}
|
|
675
|
-
return ''
|
|
676
|
-
}
|
|
587
|
+
Plan should include:
|
|
588
|
+
1. Task understanding
|
|
589
|
+
2. Relevant files or areas to inspect/change
|
|
590
|
+
3. Step-by-step implementation plan
|
|
591
|
+
4. Risks or assumptions
|
|
592
|
+
5. Validation commands/checks to run after implementation
|
|
593
|
+
6. Whether documentation/wiki updates are needed
|
|
677
594
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
if (text) return text
|
|
684
|
-
}
|
|
685
|
-
return ''
|
|
595
|
+
End by telling the user they can reply “允许”, “按计划执行”, or an equivalent approval phrase to continue in a normal follow-up turn.
|
|
596
|
+
|
|
597
|
+
User task:
|
|
598
|
+
${taskText}
|
|
599
|
+
</plan_command_invocation>`
|
|
686
600
|
}
|
|
687
601
|
|
|
688
602
|
async function runSubagent(parentSession, params, parentSignal, onUpdate) {
|
|
@@ -714,6 +628,9 @@ async function runSubagent(parentSession, params, parentSignal, onUpdate) {
|
|
|
714
628
|
let latestMessages = []
|
|
715
629
|
let latestPendingToolCalls = []
|
|
716
630
|
let toolsForClient = []
|
|
631
|
+
let lastTraceAt = 0
|
|
632
|
+
let tracePending = false
|
|
633
|
+
let traceTimer = null
|
|
717
634
|
|
|
718
635
|
const tools = await createServerTools(
|
|
719
636
|
parentSession.projectId,
|
|
@@ -733,6 +650,12 @@ async function runSubagent(parentSession, params, parentSignal, onUpdate) {
|
|
|
733
650
|
toolsForClient = tools.map(({ execute, prepareArguments, ...tool }) => tool)
|
|
734
651
|
|
|
735
652
|
const emitSubagentTrace = () => {
|
|
653
|
+
if (traceTimer) {
|
|
654
|
+
clearTimeout(traceTimer)
|
|
655
|
+
traceTimer = null
|
|
656
|
+
}
|
|
657
|
+
tracePending = false
|
|
658
|
+
lastTraceAt = Date.now()
|
|
736
659
|
onUpdate?.({
|
|
737
660
|
content: [],
|
|
738
661
|
details: {
|
|
@@ -751,6 +674,19 @@ async function runSubagent(parentSession, params, parentSignal, onUpdate) {
|
|
|
751
674
|
})
|
|
752
675
|
}
|
|
753
676
|
|
|
677
|
+
const emitSubagentTraceThrottled = () => {
|
|
678
|
+
const elapsed = Date.now() - lastTraceAt
|
|
679
|
+
if (elapsed >= SUBAGENT_TRACE_THROTTLE_MS) {
|
|
680
|
+
emitSubagentTrace()
|
|
681
|
+
return
|
|
682
|
+
}
|
|
683
|
+
tracePending = true
|
|
684
|
+
if (traceTimer) return
|
|
685
|
+
traceTimer = setTimeout(() => {
|
|
686
|
+
if (tracePending) emitSubagentTrace()
|
|
687
|
+
}, SUBAGENT_TRACE_THROTTLE_MS - elapsed)
|
|
688
|
+
}
|
|
689
|
+
|
|
754
690
|
const systemPrompt = composeSubagentSystemPrompt({
|
|
755
691
|
definition,
|
|
756
692
|
parentSystemPrompt: parentSession.agent.state.systemPrompt,
|
|
@@ -807,7 +743,11 @@ async function runSubagent(parentSession, params, parentSignal, onUpdate) {
|
|
|
807
743
|
latestMessages = [...latestMessages, event.message]
|
|
808
744
|
}
|
|
809
745
|
}
|
|
810
|
-
|
|
746
|
+
if (event.type === 'tool_execution_start' || event.type === 'tool_execution_end' || event.type === 'message_end') {
|
|
747
|
+
emitSubagentTrace()
|
|
748
|
+
} else {
|
|
749
|
+
emitSubagentTraceThrottled()
|
|
750
|
+
}
|
|
811
751
|
})
|
|
812
752
|
|
|
813
753
|
let timedOut = false
|
|
@@ -825,6 +765,7 @@ async function runSubagent(parentSession, params, parentSignal, onUpdate) {
|
|
|
825
765
|
} finally {
|
|
826
766
|
clearTimeout(timeout)
|
|
827
767
|
parentSignal?.removeEventListener?.('abort', onParentAbort)
|
|
768
|
+
emitSubagentTrace()
|
|
828
769
|
}
|
|
829
770
|
|
|
830
771
|
const content = lastAssistantText(subagent.state.messages) || `Subagent ${definition.name} completed without a text response.`
|
|
@@ -1050,9 +991,11 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1050
991
|
beforeToolCall: async (context) => {
|
|
1051
992
|
const toolName = context.toolCall?.name
|
|
1052
993
|
const toolCallId = context.toolCall?.id
|
|
994
|
+
const currentSession = agentSessions.get(sessionId)
|
|
995
|
+
const commandPermissionError = commandToolPermissionError(currentSession, toolName)
|
|
996
|
+
if (commandPermissionError) return { block: true, reason: commandPermissionError }
|
|
1053
997
|
const isSkillTool = toolName === 'activate_skill' || toolName === 'read_skill_resource'
|
|
1054
998
|
if (isSkillTool) return undefined
|
|
1055
|
-
const currentSession = agentSessions.get(sessionId)
|
|
1056
999
|
if (profileToolNames && !profileToolNames.includes(toolName)) return { block: true, reason: `Agent profile ${agentProfile.name} is not allowed to use ${toolName}.` }
|
|
1057
1000
|
if (toolName === 'run_subagent') return undefined
|
|
1058
1001
|
if (isMcpToolName(toolName)) {
|
|
@@ -1138,6 +1081,7 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1138
1081
|
if (event.type === 'agent_end') {
|
|
1139
1082
|
session.status = session.agent.state.errorMessage ? 'error' : 'idle'
|
|
1140
1083
|
session.finishedAt = new Date().toISOString()
|
|
1084
|
+
session.toolTimings?.clear()
|
|
1141
1085
|
resetIdleTimer(session)
|
|
1142
1086
|
|
|
1143
1087
|
// Persist after run ends
|
|
@@ -1194,7 +1138,7 @@ async function persistSession(session) {
|
|
|
1194
1138
|
if (messages.length === 0) {
|
|
1195
1139
|
try {
|
|
1196
1140
|
await deleteSessionValue(sessionId)
|
|
1197
|
-
await
|
|
1141
|
+
await atomicSessionMetadataUpdate(scope, projectId, (data) => {
|
|
1198
1142
|
delete data[sessionId]
|
|
1199
1143
|
return data
|
|
1200
1144
|
})
|
|
@@ -1281,7 +1225,7 @@ async function persistSession(session) {
|
|
|
1281
1225
|
// Write to storage atomically (read-modify-write within queue)
|
|
1282
1226
|
try {
|
|
1283
1227
|
await writeSessionValue(sessionId, sessionData)
|
|
1284
|
-
await
|
|
1228
|
+
await atomicSessionMetadataUpdate(scope, projectId, (data) => {
|
|
1285
1229
|
data[sessionId] = {
|
|
1286
1230
|
...metadata,
|
|
1287
1231
|
pinnedAt: data[sessionId]?.pinnedAt,
|
|
@@ -1383,6 +1327,10 @@ export async function runPrompt(sessionId, message) {
|
|
|
1383
1327
|
throw Object.assign(new Error('Session not found'), { statusCode: 404 })
|
|
1384
1328
|
}
|
|
1385
1329
|
|
|
1330
|
+
if (session.agent.state.isStreaming) {
|
|
1331
|
+
throw Object.assign(new Error('Generation is still running. Stop it or wait until it finishes.'), { statusCode: 409 })
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1386
1334
|
resetIdleTimer(session)
|
|
1387
1335
|
|
|
1388
1336
|
// Build user message
|
|
@@ -1454,6 +1402,50 @@ export async function runPrompt(sessionId, message) {
|
|
|
1454
1402
|
return { sessionId, status: session.status }
|
|
1455
1403
|
}
|
|
1456
1404
|
|
|
1405
|
+
/**
|
|
1406
|
+
* Continue generation from the current last message (must be a user or
|
|
1407
|
+
* tool-result message). Used by the retry button to regenerate a response
|
|
1408
|
+
* in-place without appending a new user message.
|
|
1409
|
+
*
|
|
1410
|
+
* Trims messages to keep up to and including the last user message,
|
|
1411
|
+
* removing the assistant response that follows it.
|
|
1412
|
+
*/
|
|
1413
|
+
export async function continueSession(sessionId) {
|
|
1414
|
+
const session = agentSessions.get(sessionId)
|
|
1415
|
+
if (!session) {
|
|
1416
|
+
throw Object.assign(new Error('Session not found'), { statusCode: 404 })
|
|
1417
|
+
}
|
|
1418
|
+
if (session.agent.state.isStreaming) {
|
|
1419
|
+
throw Object.assign(new Error('Generation is still running. Stop it or wait until it finishes.'), { statusCode: 409 })
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
const messages = Array.isArray(session.agent.state.messages) ? session.agent.state.messages : []
|
|
1423
|
+
|
|
1424
|
+
// Find the last user message and trim everything after it (the assistant response)
|
|
1425
|
+
let lastUserIndex = -1
|
|
1426
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1427
|
+
if (messages[i].role === 'user' || messages[i].role === 'user-with-attachments') {
|
|
1428
|
+
lastUserIndex = i
|
|
1429
|
+
break
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
if (lastUserIndex < 0) {
|
|
1433
|
+
throw Object.assign(new Error('Cannot continue: no user message found.'), { statusCode: 400 })
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
const trimmedMessages = messages.slice(0, lastUserIndex + 1)
|
|
1437
|
+
updateSessionMessages(session, trimmedMessages)
|
|
1438
|
+
resetSessionCompaction(session)
|
|
1439
|
+
|
|
1440
|
+
resetIdleTimer(session)
|
|
1441
|
+
session.agent.continue().catch((err) => {
|
|
1442
|
+
logger.error(`Agent continue error for session ${sessionId}:`, err, { sessionId })
|
|
1443
|
+
emitSessionEvent(session, { type: 'error', error: err.message || 'Unknown error' })
|
|
1444
|
+
})
|
|
1445
|
+
|
|
1446
|
+
return { sessionId, status: 'running' }
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1457
1449
|
/**
|
|
1458
1450
|
* Abort the current agent run.
|
|
1459
1451
|
*/
|
|
@@ -1614,6 +1606,7 @@ export async function destroyAgent(sessionId) {
|
|
|
1614
1606
|
logger.info(`Destroying session ${sessionId} (status: ${session.status})`, { sessionId, status: session.status })
|
|
1615
1607
|
|
|
1616
1608
|
if (session.idleTimer) clearTimeout(session.idleTimer)
|
|
1609
|
+
session.toolTimings?.clear()
|
|
1617
1610
|
|
|
1618
1611
|
try {
|
|
1619
1612
|
session.agent.abort()
|
|
@@ -2,7 +2,7 @@ import fs from 'node:fs'
|
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
4
4
|
import { randomUUID } from 'node:crypto'
|
|
5
|
-
import { streamSimple } from '@
|
|
5
|
+
import { streamSimple } from '@earendil-works/pi-ai'
|
|
6
6
|
import { logsDir } from './storage.mjs'
|
|
7
7
|
|
|
8
8
|
const PATCH_MARKER = Symbol.for('quickforge.aiHttpLogger.fetchPatched')
|
|
@@ -16,16 +16,31 @@ function currentLogFile() {
|
|
|
16
16
|
return path.join(logsDir, `ai-http-${date}.jsonl`)
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
let logsDirEnsured = false
|
|
20
|
+
|
|
21
|
+
async function ensureLogsDir() {
|
|
22
|
+
if (logsDirEnsured) return
|
|
21
23
|
try {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
const { promises: fsp } = await import('node:fs')
|
|
25
|
+
await fsp.mkdir(logsDir, { recursive: true })
|
|
26
|
+
logsDirEnsured = true
|
|
24
27
|
} catch {
|
|
25
28
|
// Keep AI calls working even when diagnostic logging fails.
|
|
26
29
|
}
|
|
27
30
|
}
|
|
28
31
|
|
|
32
|
+
function writeAiHttpRecord(record) {
|
|
33
|
+
if (!aiHttpLogEnabled) return
|
|
34
|
+
// Schedule async write — never blocks the event loop
|
|
35
|
+
void ensureLogsDir().then(() => {
|
|
36
|
+
try {
|
|
37
|
+
fs.appendFile(currentLogFile(), `${JSON.stringify({ ts: new Date().toISOString(), ...record })}\n`, () => {})
|
|
38
|
+
} catch {
|
|
39
|
+
// Keep AI calls working even when diagnostic logging fails.
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
29
44
|
function headersToRecord(headers) {
|
|
30
45
|
const result = {}
|
|
31
46
|
if (!headers) return result
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool approval store — shared state and permission helpers.
|
|
3
|
+
*
|
|
4
|
+
* Manages the pending approval queues and command-tool permission checks.
|
|
5
|
+
* The Promise-based approval functions (createApprovalPromise,
|
|
6
|
+
* createAutoCompactApprovalPromise) remain in agent-manager.mjs because
|
|
7
|
+
* they depend on the agent event buses.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Constants
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
export const APPROVAL_TIMEOUT_MS = 5 * 60 * 1000 // 5 minutes for tool approval
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Tool categories
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export const commandRestrictedTools = new Set([
|
|
21
|
+
'write_file',
|
|
22
|
+
'edit_file',
|
|
23
|
+
'run_command',
|
|
24
|
+
'run_subagent',
|
|
25
|
+
])
|
|
26
|
+
|
|
27
|
+
export const safeReadTools = new Set([
|
|
28
|
+
'read_file',
|
|
29
|
+
'grep_files',
|
|
30
|
+
])
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Pending approval queues
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
/** toolCallId → { resolve, reject, sessionId, toolName, args, source, timeout } */
|
|
37
|
+
export const pendingApprovals = new Map()
|
|
38
|
+
|
|
39
|
+
/** approvalId → { resolve, reject, sessionId, timeout } */
|
|
40
|
+
export const pendingAutoCompactApprovals = new Map()
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Permission helpers
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
export function commandToolPermissionError(session, toolName) {
|
|
47
|
+
const permissions = session?.activeCommandPermissions
|
|
48
|
+
if (!permissions || !commandRestrictedTools.has(toolName)) return null
|
|
49
|
+
if (toolName === 'run_command' && permissions.allowCommands === false) {
|
|
50
|
+
return `Command /${session.activeCommandName} does not allow running shell commands.`
|
|
51
|
+
}
|
|
52
|
+
if (toolName === 'run_subagent' && permissions.allowCommands === false) {
|
|
53
|
+
return `Command /${session.activeCommandName} does not allow running subagents.`
|
|
54
|
+
}
|
|
55
|
+
if ((toolName === 'write_file' || toolName === 'edit_file') && permissions.allowEdit === false) {
|
|
56
|
+
return `Command /${session.activeCommandName} does not allow editing files.`
|
|
57
|
+
}
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function createCommandToolPermissions(session) {
|
|
62
|
+
return (toolName) => commandToolPermissionError(session, toolName)
|
|
63
|
+
}
|
|
@@ -251,6 +251,9 @@ export function parseInternalCommandInvocation(message) {
|
|
|
251
251
|
if (/^\/clear\s*$/i.test(text)) return { type: 'clear' }
|
|
252
252
|
if (/^\/clear(?:\s+[\s\S]+)$/i.test(text)) return { type: 'invalid-clear-args' }
|
|
253
253
|
|
|
254
|
+
const planMatch = text.match(/^\/plan(?:\s+([\s\S]*))?$/i)
|
|
255
|
+
if (planMatch) return { type: 'plan', args: (planMatch[1] || '').trim() }
|
|
256
|
+
|
|
254
257
|
const compactMatch = text.match(/^\/compact(?:\s+([\s\S]*))?$/i)
|
|
255
258
|
if (compactMatch) return { type: 'compact', args: (compactMatch[1] || '').trim() }
|
|
256
259
|
|
|
@@ -270,6 +273,11 @@ export async function handleInternalCommand(invocation, workspaceRoot, commandDi
|
|
|
270
273
|
return { compact: true, args: invocation.args || '' }
|
|
271
274
|
}
|
|
272
275
|
|
|
276
|
+
if (invocation.type === 'plan') {
|
|
277
|
+
if (!invocation.args) return 'Usage: /plan <task>'
|
|
278
|
+
return { plan: true, args: invocation.args }
|
|
279
|
+
}
|
|
280
|
+
|
|
273
281
|
if (invocation.type === 'clear') {
|
|
274
282
|
return { clear: true }
|
|
275
283
|
}
|
package/server/index.mjs
CHANGED
|
@@ -234,7 +234,7 @@ async function handleApi(req, res, url) {
|
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
// Project workspace inspector routes
|
|
237
|
-
if (pathname === '/api/workspace/tree' || pathname === '/api/workspace/file') {
|
|
237
|
+
if (pathname === '/api/workspace/tree' || pathname === '/api/workspace/file' || pathname === '/api/workspace/resolve-path') {
|
|
238
238
|
await handleWorkspaceApi(req, res, url)
|
|
239
239
|
return
|
|
240
240
|
}
|