@shawnstack/quickforge 1.3.24 → 1.3.26
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 +24 -18
- package/dist/assets/anthropic-BcnDL7hi.js +39 -0
- package/dist/assets/azure-openai-responses-BEfdv0qd.js +1 -0
- package/dist/assets/google-C2y985rW.js +1 -0
- package/dist/assets/google-shared-Cqjw1plk.js +11 -0
- package/dist/assets/google-vertex-Jf9zNsCF.js +1 -0
- package/dist/assets/{icons-DmRYmmql.js → icons-BVM5--R9.js} +1 -1
- package/dist/assets/{index-s72bxhrh.js → index-8Q1Ovled.js} +604 -550
- package/dist/assets/index-ZYbEKGUp.css +3 -0
- package/dist/assets/{mistral-DCZ8VphX.js → mistral-qYbgRY3z.js} +1 -1
- package/dist/assets/openai-codex-responses--aAgyYJM.js +7 -0
- package/dist/assets/openai-completions-CHDluyXM.js +5 -0
- package/dist/assets/openai-prompt-cache-CErE62Yt.js +1 -0
- package/dist/assets/openai-responses-UtRriBXu.js +1 -0
- package/dist/assets/{openai-responses-shared-RzgnIlMf.js → openai-responses-shared-G6WDDqJ8.js} +1 -1
- package/dist/assets/openrouter-Dz9zwzUG.js +1 -0
- package/dist/assets/{react-vendor-BsV2HYbc.js → react-vendor-DAoL5p8_.js} +1 -1
- 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 +4 -3
- package/server/agent-manager.mjs +162 -176
- package/server/ai-http-logger.mjs +20 -5
- package/server/approval-store.mjs +63 -0
- package/server/custom-commands.mjs +67 -9
- package/server/index.mjs +7 -0
- package/server/message-converters.mjs +79 -0
- package/server/plugins/loader.mjs +56 -0
- package/server/plugins/manifest.mjs +174 -0
- package/server/plugins/registry.mjs +304 -0
- package/server/project-config.mjs +53 -4
- package/server/routes/agent-profiles.mjs +1 -1
- package/server/routes/agent.mjs +1 -16
- package/server/routes/filesystem.mjs +18 -2
- package/server/routes/plugins.mjs +63 -0
- package/server/routes/project.mjs +2 -0
- package/server/routes/scheduled-tasks.mjs +1 -1
- package/server/routes/storage.mjs +66 -31
- package/server/routes/tools.mjs +12 -1
- package/server/session-utils.mjs +1 -1
- package/server/skills.mjs +64 -5
- package/server/storage.mjs +91 -8
- package/server/system-prompt.mjs +27 -5
- package/server/tool-wiring.mjs +113 -0
- package/server/utils/workspace.mjs +20 -1
- package/dist/assets/anthropic-BrbLtQkg.js +0 -39
- package/dist/assets/azure-openai-responses-q9QFpQk3.js +0 -1
- package/dist/assets/google-Bv6IeSRf.js +0 -1
- package/dist/assets/google-shared-CLc4ziON.js +0 -11
- package/dist/assets/google-vertex-Cwpe8vbn.js +0 -1
- package/dist/assets/index-C4m48ndP.css +0 -3
- package/dist/assets/openai-codex-responses-Bx7iyHzd.js +0 -7
- package/dist/assets/openai-completions-CihVV11E.js +0 -5
- package/dist/assets/openai-responses-BigEdUNS.js +0 -1
- 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
package/server/agent-manager.mjs
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
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
|
+
import { createPluginToolDefinitions, isPluginToolName } from './plugins/registry.mjs'
|
|
8
9
|
import {
|
|
9
10
|
composeSubagentSystemPrompt,
|
|
10
11
|
formatSubagentTask,
|
|
11
12
|
} from './subagents.mjs'
|
|
12
13
|
import { agentProfileSnapshot, getAgentProfile } from './agent-profiles.mjs'
|
|
13
14
|
import { projectContextFromId, readProjectConfig } from './project-config.mjs'
|
|
14
|
-
import { readStore, atomicUpdate, readSessionValue, writeSessionValue, deleteSessionValue } from './storage.mjs'
|
|
15
|
+
import { readStore, atomicUpdate, atomicSessionMetadataUpdate, readSessionValue, writeSessionValue, deleteSessionValue } from './storage.mjs'
|
|
15
16
|
import { logger } from './utils/logger.mjs'
|
|
16
17
|
import { buildSystemPrompt, generateAiTitle, generateTitle } from './session-utils.mjs'
|
|
17
18
|
import { restoreReasoningContentInPayload } from './reasoning-cache.mjs'
|
|
@@ -30,70 +31,22 @@ import {
|
|
|
30
31
|
parseInternalCommandInvocation,
|
|
31
32
|
resolveCustomCommandInvocation,
|
|
32
33
|
} from './custom-commands.mjs'
|
|
34
|
+
import { omitDetailsForLlm, serverConvertToLlm, messageText, lastAssistantText } from './message-converters.mjs'
|
|
35
|
+
import { isPlainObject, mergeQuickForgeTiming, wrapToolDefinition, wrapMcpToolDefinition, wrapPluginToolDefinition, sessionSkillsContext } from './tool-wiring.mjs'
|
|
36
|
+
import {
|
|
37
|
+
APPROVAL_TIMEOUT_MS,
|
|
38
|
+
commandRestrictedTools,
|
|
39
|
+
safeReadTools,
|
|
40
|
+
pendingApprovals,
|
|
41
|
+
pendingAutoCompactApprovals,
|
|
42
|
+
commandToolPermissionError,
|
|
43
|
+
createCommandToolPermissions,
|
|
44
|
+
} from './approval-store.mjs'
|
|
33
45
|
|
|
34
46
|
// ---------------------------------------------------------------------------
|
|
35
47
|
// Tool definitions (server-side, no REST roundtrip)
|
|
36
48
|
// ---------------------------------------------------------------------------
|
|
37
49
|
|
|
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
50
|
function wrapSubagentToolDefinition(definition, parentSessionId) {
|
|
98
51
|
return {
|
|
99
52
|
...definition,
|
|
@@ -119,6 +72,7 @@ async function createServerTools(projectId, projectContext, skillsContext, inclu
|
|
|
119
72
|
allowedToolNames = null,
|
|
120
73
|
includeSubagentTool = true,
|
|
121
74
|
includeMcpTools = true,
|
|
75
|
+
includePluginTools = true,
|
|
122
76
|
parentSessionId = null,
|
|
123
77
|
} = options
|
|
124
78
|
const allowedTools = allowedToolNames ? new Set(allowedToolNames) : null
|
|
@@ -151,14 +105,12 @@ async function createServerTools(projectId, projectContext, skillsContext, inclu
|
|
|
151
105
|
tools.push(...mcpTools.filter(isAllowed).map((definition) => wrapMcpToolDefinition(definition, toolPermissions)))
|
|
152
106
|
}
|
|
153
107
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
function sessionSkillsContext(session) {
|
|
158
|
-
return {
|
|
159
|
-
globalSkillNames: session.globalSkillNames,
|
|
160
|
-
projectSkillNames: session.projectSkillNames,
|
|
108
|
+
if (includePluginTools) {
|
|
109
|
+
const pluginTools = await createPluginToolDefinitions(projectContext)
|
|
110
|
+
tools.push(...pluginTools.filter(isAllowed).map((definition) => wrapPluginToolDefinition(definition, toolContext, toolPermissions)))
|
|
161
111
|
}
|
|
112
|
+
|
|
113
|
+
return tools
|
|
162
114
|
}
|
|
163
115
|
|
|
164
116
|
async function rebuildSessionTools(session) {
|
|
@@ -181,31 +133,8 @@ const agentSessions = new Map()
|
|
|
181
133
|
/** @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
134
|
|
|
183
135
|
const IDLE_TIMEOUT_MS = 30 * 60 * 1000 // 30 minutes
|
|
184
|
-
const APPROVAL_TIMEOUT_MS = 5 * 60 * 1000 // 5 minutes for tool approval
|
|
185
136
|
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 commandToolPermissionError(session, toolName) {
|
|
192
|
-
const permissions = session?.activeCommandPermissions
|
|
193
|
-
if (!permissions || !commandRestrictedTools.has(toolName)) return null
|
|
194
|
-
if (toolName === 'run_command' && permissions.allowCommands === false) {
|
|
195
|
-
return `Command /${session.activeCommandName} does not allow running shell commands.`
|
|
196
|
-
}
|
|
197
|
-
if (toolName === 'run_subagent' && permissions.allowCommands === false) {
|
|
198
|
-
return `Command /${session.activeCommandName} does not allow running subagents.`
|
|
199
|
-
}
|
|
200
|
-
if ((toolName === 'write_file' || toolName === 'edit_file') && permissions.allowEdit === false) {
|
|
201
|
-
return `Command /${session.activeCommandName} does not allow editing files.`
|
|
202
|
-
}
|
|
203
|
-
return null
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function createCommandToolPermissions(session) {
|
|
207
|
-
return (toolName) => commandToolPermissionError(session, toolName)
|
|
208
|
-
}
|
|
137
|
+
const SUBAGENT_TRACE_THROTTLE_MS = 150
|
|
209
138
|
|
|
210
139
|
/**
|
|
211
140
|
* Create a Promise that only resolves when the user accepts or rejects the tool call.
|
|
@@ -224,8 +153,14 @@ function createApprovalPromise(session, toolCallId, toolName, args, source) {
|
|
|
224
153
|
resolve({ block: true, reason: `Approval timeout for ${toolName}` })
|
|
225
154
|
}, APPROVAL_TIMEOUT_MS)
|
|
226
155
|
|
|
156
|
+
let onAbort = null
|
|
157
|
+
|
|
227
158
|
const cleanup = () => {
|
|
228
159
|
clearTimeout(timeout)
|
|
160
|
+
if (onAbort) {
|
|
161
|
+
session.agent.signal?.removeEventListener('abort', onAbort)
|
|
162
|
+
onAbort = null
|
|
163
|
+
}
|
|
229
164
|
if (settled) return
|
|
230
165
|
settled = true
|
|
231
166
|
pendingApprovals.delete(toolCallId)
|
|
@@ -239,7 +174,7 @@ function createApprovalPromise(session, toolCallId, toolName, args, source) {
|
|
|
239
174
|
reject(new Error('Run aborted'))
|
|
240
175
|
return
|
|
241
176
|
}
|
|
242
|
-
|
|
177
|
+
onAbort = () => {
|
|
243
178
|
cleanup()
|
|
244
179
|
reject(new Error('Run aborted'))
|
|
245
180
|
}
|
|
@@ -289,8 +224,14 @@ function createAutoCompactApprovalPromise(session, details = {}) {
|
|
|
289
224
|
resolve(false)
|
|
290
225
|
}, APPROVAL_TIMEOUT_MS)
|
|
291
226
|
|
|
227
|
+
let onAbort = null
|
|
228
|
+
|
|
292
229
|
const cleanup = () => {
|
|
293
230
|
clearTimeout(timeout)
|
|
231
|
+
if (onAbort) {
|
|
232
|
+
session.agent.signal?.removeEventListener('abort', onAbort)
|
|
233
|
+
onAbort = null
|
|
234
|
+
}
|
|
294
235
|
if (settled) return
|
|
295
236
|
settled = true
|
|
296
237
|
pendingAutoCompactApprovals.delete(approvalId)
|
|
@@ -303,7 +244,7 @@ function createAutoCompactApprovalPromise(session, details = {}) {
|
|
|
303
244
|
reject(new Error('Run aborted'))
|
|
304
245
|
return
|
|
305
246
|
}
|
|
306
|
-
|
|
247
|
+
onAbort = () => {
|
|
307
248
|
cleanup()
|
|
308
249
|
reject(new Error('Run aborted'))
|
|
309
250
|
}
|
|
@@ -619,6 +560,14 @@ async function resolveCommandState(session, userMessage) {
|
|
|
619
560
|
commandName: 'plan',
|
|
620
561
|
}
|
|
621
562
|
}
|
|
563
|
+
if (internalResponse?.review) {
|
|
564
|
+
return {
|
|
565
|
+
userMessage,
|
|
566
|
+
commandPrompt: formatReviewCommandPrompt(internalResponse.args),
|
|
567
|
+
permissions: { allowEdit: false, allowCommands: true, allowSubagents: false },
|
|
568
|
+
commandName: 'review',
|
|
569
|
+
}
|
|
570
|
+
}
|
|
622
571
|
|
|
623
572
|
if (!session.projectContext?.workspaceRoot) return { userMessage }
|
|
624
573
|
|
|
@@ -665,65 +614,31 @@ ${taskText}
|
|
|
665
614
|
</plan_command_invocation>`
|
|
666
615
|
}
|
|
667
616
|
|
|
668
|
-
function
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
return copy
|
|
673
|
-
}
|
|
617
|
+
function formatReviewCommandPrompt(scope) {
|
|
618
|
+
const scopeText = String(scope || '').trim() || '(none; review the repository changes that appear relevant for a pre-commit check)'
|
|
619
|
+
return `<review_command_invocation name="review">
|
|
620
|
+
This /review command applies only to the current user request. Perform a pre-commit self-review of the code that is about to be committed.
|
|
674
621
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
.map(m => {
|
|
685
|
-
if (m.role === 'user-with-attachments') {
|
|
686
|
-
const textContent = typeof m.content === 'string'
|
|
687
|
-
? [{ type: 'text', text: m.content }]
|
|
688
|
-
: [...m.content]
|
|
689
|
-
if (Array.isArray(m.attachments)) {
|
|
690
|
-
for (const att of m.attachments) {
|
|
691
|
-
if (att.type === 'image' && att.content) {
|
|
692
|
-
textContent.push({ type: 'image', data: att.content, mimeType: att.mimeType })
|
|
693
|
-
} else if (att.type === 'document' && att.extractedText) {
|
|
694
|
-
textContent.push({ type: 'text', text: `\n\n[Document: ${att.fileName}]\n${att.extractedText}` })
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
return omitDetailsForLlm({ ...m, role: 'user', content: textContent })
|
|
699
|
-
}
|
|
700
|
-
if (m.role === 'user' || m.role === 'assistant' || m.role === 'toolResult') return omitDetailsForLlm(m)
|
|
701
|
-
return null
|
|
702
|
-
})
|
|
703
|
-
.filter(Boolean)
|
|
704
|
-
}
|
|
622
|
+
Rules for this turn:
|
|
623
|
+
- Do not modify files.
|
|
624
|
+
- Do not create files.
|
|
625
|
+
- Do not stage, unstage, commit, tag, push, publish, or otherwise change repository state.
|
|
626
|
+
- Do not use write_file or edit_file.
|
|
627
|
+
- You may use read-only tools and shell commands to inspect the workspace and run validation checks.
|
|
628
|
+
- Do not use subagents; perform the review directly in this turn.
|
|
629
|
+
- Prefer safe inspection commands such as git status, git diff, git diff --cached, and targeted lint/build/test commands.
|
|
630
|
+
- Treat command output as evidence; distinguish confirmed issues from risks or suggestions.
|
|
705
631
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
.map((block) => block.text ?? '')
|
|
713
|
-
.join('\n')
|
|
714
|
-
.trim()
|
|
715
|
-
}
|
|
716
|
-
return ''
|
|
717
|
-
}
|
|
632
|
+
Review checklist:
|
|
633
|
+
1. Identify the changes under review, prioritizing staged changes when present and otherwise unstaged working tree changes.
|
|
634
|
+
2. Look for correctness bugs, regressions, edge cases, missing error handling, security or privacy risks, and unintended side effects.
|
|
635
|
+
3. Check whether tests, lint/build validation, or documentation/wiki updates are needed.
|
|
636
|
+
4. Call out any risky commands that should not be run automatically.
|
|
637
|
+
5. Output a concise review with severity, file/area, evidence, and recommended next steps. If no blocking issues are found, say so clearly.
|
|
718
638
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
if (message?.role !== 'assistant') continue
|
|
723
|
-
const text = messageText(message)
|
|
724
|
-
if (text) return text
|
|
725
|
-
}
|
|
726
|
-
return ''
|
|
639
|
+
User review scope or focus:
|
|
640
|
+
${scopeText}
|
|
641
|
+
</review_command_invocation>`
|
|
727
642
|
}
|
|
728
643
|
|
|
729
644
|
async function runSubagent(parentSession, params, parentSignal, onUpdate) {
|
|
@@ -755,6 +670,9 @@ async function runSubagent(parentSession, params, parentSignal, onUpdate) {
|
|
|
755
670
|
let latestMessages = []
|
|
756
671
|
let latestPendingToolCalls = []
|
|
757
672
|
let toolsForClient = []
|
|
673
|
+
let lastTraceAt = 0
|
|
674
|
+
let tracePending = false
|
|
675
|
+
let traceTimer = null
|
|
758
676
|
|
|
759
677
|
const tools = await createServerTools(
|
|
760
678
|
parentSession.projectId,
|
|
@@ -774,6 +692,12 @@ async function runSubagent(parentSession, params, parentSignal, onUpdate) {
|
|
|
774
692
|
toolsForClient = tools.map(({ execute, prepareArguments, ...tool }) => tool)
|
|
775
693
|
|
|
776
694
|
const emitSubagentTrace = () => {
|
|
695
|
+
if (traceTimer) {
|
|
696
|
+
clearTimeout(traceTimer)
|
|
697
|
+
traceTimer = null
|
|
698
|
+
}
|
|
699
|
+
tracePending = false
|
|
700
|
+
lastTraceAt = Date.now()
|
|
777
701
|
onUpdate?.({
|
|
778
702
|
content: [],
|
|
779
703
|
details: {
|
|
@@ -792,6 +716,19 @@ async function runSubagent(parentSession, params, parentSignal, onUpdate) {
|
|
|
792
716
|
})
|
|
793
717
|
}
|
|
794
718
|
|
|
719
|
+
const emitSubagentTraceThrottled = () => {
|
|
720
|
+
const elapsed = Date.now() - lastTraceAt
|
|
721
|
+
if (elapsed >= SUBAGENT_TRACE_THROTTLE_MS) {
|
|
722
|
+
emitSubagentTrace()
|
|
723
|
+
return
|
|
724
|
+
}
|
|
725
|
+
tracePending = true
|
|
726
|
+
if (traceTimer) return
|
|
727
|
+
traceTimer = setTimeout(() => {
|
|
728
|
+
if (tracePending) emitSubagentTrace()
|
|
729
|
+
}, SUBAGENT_TRACE_THROTTLE_MS - elapsed)
|
|
730
|
+
}
|
|
731
|
+
|
|
795
732
|
const systemPrompt = composeSubagentSystemPrompt({
|
|
796
733
|
definition,
|
|
797
734
|
parentSystemPrompt: parentSession.agent.state.systemPrompt,
|
|
@@ -848,7 +785,11 @@ async function runSubagent(parentSession, params, parentSignal, onUpdate) {
|
|
|
848
785
|
latestMessages = [...latestMessages, event.message]
|
|
849
786
|
}
|
|
850
787
|
}
|
|
851
|
-
|
|
788
|
+
if (event.type === 'tool_execution_start' || event.type === 'tool_execution_end' || event.type === 'message_end') {
|
|
789
|
+
emitSubagentTrace()
|
|
790
|
+
} else {
|
|
791
|
+
emitSubagentTraceThrottled()
|
|
792
|
+
}
|
|
852
793
|
})
|
|
853
794
|
|
|
854
795
|
let timedOut = false
|
|
@@ -866,6 +807,7 @@ async function runSubagent(parentSession, params, parentSignal, onUpdate) {
|
|
|
866
807
|
} finally {
|
|
867
808
|
clearTimeout(timeout)
|
|
868
809
|
parentSignal?.removeEventListener?.('abort', onParentAbort)
|
|
810
|
+
emitSubagentTrace()
|
|
869
811
|
}
|
|
870
812
|
|
|
871
813
|
const content = lastAssistantText(subagent.state.messages) || `Subagent ${definition.name} completed without a text response.`
|
|
@@ -905,6 +847,57 @@ function applyActiveCommandPrompt(messages, commandPrompt) {
|
|
|
905
847
|
return messages
|
|
906
848
|
}
|
|
907
849
|
|
|
850
|
+
function textFromMessageContent(content) {
|
|
851
|
+
if (typeof content === 'string') return content
|
|
852
|
+
if (Array.isArray(content)) {
|
|
853
|
+
return content.filter((block) => block?.type === 'text').map((block) => block.text ?? '').join('\n')
|
|
854
|
+
}
|
|
855
|
+
return ''
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function selectedCapabilityPrompt(capabilities) {
|
|
859
|
+
if (!Array.isArray(capabilities) || capabilities.length === 0) return null
|
|
860
|
+
const normalized = capabilities
|
|
861
|
+
.filter((capability) => capability && typeof capability === 'object')
|
|
862
|
+
.map((capability) => ({
|
|
863
|
+
type: String(capability.type || '').slice(0, 32),
|
|
864
|
+
pluginName: String(capability.pluginName || '').slice(0, 120),
|
|
865
|
+
name: String(capability.name || '').slice(0, 120),
|
|
866
|
+
label: String(capability.label || capability.name || '').slice(0, 160),
|
|
867
|
+
description: String(capability.description || '').slice(0, 400),
|
|
868
|
+
}))
|
|
869
|
+
.filter((capability) => capability.type && capability.pluginName && capability.name)
|
|
870
|
+
.slice(0, 4)
|
|
871
|
+
if (normalized.length === 0) return null
|
|
872
|
+
|
|
873
|
+
const lines = normalized.map((capability) => {
|
|
874
|
+
const toolHint = capability.type === 'tool' ? ` Tool name: plugin__${capability.pluginName}__${capability.name}.` : ''
|
|
875
|
+
const description = capability.description ? ` Description: ${capability.description}` : ''
|
|
876
|
+
return `- ${capability.label} (${capability.type}, plugin: ${capability.pluginName}, name: ${capability.name}).${toolHint}${description}`
|
|
877
|
+
}).join('\n')
|
|
878
|
+
|
|
879
|
+
return `The user selected the following QuickForge plugin capability mentions for this turn. Treat them as an explicit preference for routing and context. Use the selected capability when relevant, but do not force it if it is unrelated to the actual request.\n\n${lines}`
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function applyActiveCapabilityPrompt(messages, capabilityPrompt) {
|
|
883
|
+
if (!capabilityPrompt) return messages
|
|
884
|
+
|
|
885
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
886
|
+
const message = messages[index]
|
|
887
|
+
if (message?.role !== 'user' && message?.role !== 'user-with-attachments') continue
|
|
888
|
+
|
|
889
|
+
const visibleText = textFromMessageContent(message.content)
|
|
890
|
+
const transformed = messages.slice()
|
|
891
|
+
transformed[index] = {
|
|
892
|
+
...message,
|
|
893
|
+
content: `${capabilityPrompt}\n\nUser request:\n${visibleText}`,
|
|
894
|
+
}
|
|
895
|
+
return transformed
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
return messages
|
|
899
|
+
}
|
|
900
|
+
|
|
908
901
|
function compactSummaryIndex(messages) {
|
|
909
902
|
for (let index = messages.length - 1; index >= 0; index--) {
|
|
910
903
|
const message = messages[index]
|
|
@@ -943,7 +936,10 @@ async function transformSessionContext(session, messages, signal) {
|
|
|
943
936
|
}
|
|
944
937
|
const transformedMessages = buildAutoCompactLoopMessages(session, messages)
|
|
945
938
|
session.lastTransformedContextMessages = transformedMessages
|
|
946
|
-
return
|
|
939
|
+
return applyActiveCapabilityPrompt(
|
|
940
|
+
applyActiveCommandPrompt(compactedContextMessages(transformedMessages), session?.activeCommandPrompt),
|
|
941
|
+
session?.activeCapabilityPrompt,
|
|
942
|
+
)
|
|
947
943
|
}
|
|
948
944
|
|
|
949
945
|
export const agentEvents = new EventEmitter()
|
|
@@ -1098,7 +1094,7 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1098
1094
|
if (isSkillTool) return undefined
|
|
1099
1095
|
if (profileToolNames && !profileToolNames.includes(toolName)) return { block: true, reason: `Agent profile ${agentProfile.name} is not allowed to use ${toolName}.` }
|
|
1100
1096
|
if (toolName === 'run_subagent') return undefined
|
|
1101
|
-
if (isMcpToolName(toolName)) {
|
|
1097
|
+
if (isMcpToolName(toolName) || isPluginToolName(toolName)) {
|
|
1102
1098
|
if (!currentSession?.yoloMode) return createApprovalPromise(currentSession, toolCallId, toolName, context.args)
|
|
1103
1099
|
return undefined
|
|
1104
1100
|
}
|
|
@@ -1181,6 +1177,7 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1181
1177
|
if (event.type === 'agent_end') {
|
|
1182
1178
|
session.status = session.agent.state.errorMessage ? 'error' : 'idle'
|
|
1183
1179
|
session.finishedAt = new Date().toISOString()
|
|
1180
|
+
session.toolTimings?.clear()
|
|
1184
1181
|
resetIdleTimer(session)
|
|
1185
1182
|
|
|
1186
1183
|
// Persist after run ends
|
|
@@ -1237,7 +1234,7 @@ async function persistSession(session) {
|
|
|
1237
1234
|
if (messages.length === 0) {
|
|
1238
1235
|
try {
|
|
1239
1236
|
await deleteSessionValue(sessionId)
|
|
1240
|
-
await
|
|
1237
|
+
await atomicSessionMetadataUpdate(scope, projectId, (data) => {
|
|
1241
1238
|
delete data[sessionId]
|
|
1242
1239
|
return data
|
|
1243
1240
|
})
|
|
@@ -1324,7 +1321,7 @@ async function persistSession(session) {
|
|
|
1324
1321
|
// Write to storage atomically (read-modify-write within queue)
|
|
1325
1322
|
try {
|
|
1326
1323
|
await writeSessionValue(sessionId, sessionData)
|
|
1327
|
-
await
|
|
1324
|
+
await atomicSessionMetadataUpdate(scope, projectId, (data) => {
|
|
1328
1325
|
data[sessionId] = {
|
|
1329
1326
|
...metadata,
|
|
1330
1327
|
pinnedAt: data[sessionId]?.pinnedAt,
|
|
@@ -1395,29 +1392,11 @@ export async function rollbackSessionMessages(sessionId, rollbackMessageIndex) {
|
|
|
1395
1392
|
return { session: getSessionState(sessionId), rollbackIndex }
|
|
1396
1393
|
}
|
|
1397
1394
|
|
|
1398
|
-
export async function replaceSessionMessages(sessionId, messages) {
|
|
1399
|
-
const session = agentSessions.get(sessionId)
|
|
1400
|
-
if (!session) return null
|
|
1401
|
-
if (session.agent.state.isStreaming) {
|
|
1402
|
-
throw Object.assign(new Error('Generation is still running. Stop it or wait until it finishes before rolling back.'), { statusCode: 409 })
|
|
1403
|
-
}
|
|
1404
|
-
updateSessionMessages(session, Array.isArray(messages) ? messages : [])
|
|
1405
|
-
resetSessionCompaction(session)
|
|
1406
|
-
session.status = 'idle'
|
|
1407
|
-
session.finishedAt = new Date().toISOString()
|
|
1408
|
-
await persistSession(session)
|
|
1409
|
-
const nextMessages = session.agent.state.messages
|
|
1410
|
-
const contextUsage = getSessionContextUsage(session)
|
|
1411
|
-
emitSessionEvent(session, { type: 'message_end', messages: nextMessages, contextUsage })
|
|
1412
|
-
emitSessionEvent(session, { type: 'agent_end', messages: nextMessages, contextUsage })
|
|
1413
|
-
return getSessionState(sessionId)
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
1395
|
/**
|
|
1417
1396
|
* Send a user message to the agent and start the agent loop.
|
|
1418
1397
|
* Returns immediately; events are streamed via the event bus.
|
|
1419
1398
|
*/
|
|
1420
|
-
export async function runPrompt(sessionId, message) {
|
|
1399
|
+
export async function runPrompt(sessionId, message, selectedCapabilities = []) {
|
|
1421
1400
|
let session = agentSessions.get(sessionId)
|
|
1422
1401
|
if (!session) {
|
|
1423
1402
|
session = await restoreAgent(sessionId)
|
|
@@ -1426,6 +1405,10 @@ export async function runPrompt(sessionId, message) {
|
|
|
1426
1405
|
throw Object.assign(new Error('Session not found'), { statusCode: 404 })
|
|
1427
1406
|
}
|
|
1428
1407
|
|
|
1408
|
+
if (session.agent.state.isStreaming) {
|
|
1409
|
+
throw Object.assign(new Error('Generation is still running. Stop it or wait until it finishes.'), { statusCode: 409 })
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1429
1412
|
resetIdleTimer(session)
|
|
1430
1413
|
|
|
1431
1414
|
// Build user message
|
|
@@ -1479,6 +1462,7 @@ export async function runPrompt(sessionId, message) {
|
|
|
1479
1462
|
session.activeCommandName = commandState.commandName ?? null
|
|
1480
1463
|
session.activeCommandPermissions = commandState.permissions ?? null
|
|
1481
1464
|
session.activeCommandPrompt = commandState.commandPrompt ?? null
|
|
1465
|
+
session.activeCapabilityPrompt = selectedCapabilityPrompt(selectedCapabilities)
|
|
1482
1466
|
|
|
1483
1467
|
// Fire and forget — events come through eventBus
|
|
1484
1468
|
session.agent.prompt(userMessage).catch((err) => {
|
|
@@ -1492,6 +1476,7 @@ export async function runPrompt(sessionId, message) {
|
|
|
1492
1476
|
session.activeCommandName = null
|
|
1493
1477
|
session.activeCommandPermissions = null
|
|
1494
1478
|
session.activeCommandPrompt = null
|
|
1479
|
+
session.activeCapabilityPrompt = null
|
|
1495
1480
|
})
|
|
1496
1481
|
|
|
1497
1482
|
return { sessionId, status: session.status }
|
|
@@ -1701,6 +1686,7 @@ export async function destroyAgent(sessionId) {
|
|
|
1701
1686
|
logger.info(`Destroying session ${sessionId} (status: ${session.status})`, { sessionId, status: session.status })
|
|
1702
1687
|
|
|
1703
1688
|
if (session.idleTimer) clearTimeout(session.idleTimer)
|
|
1689
|
+
session.toolTimings?.clear()
|
|
1704
1690
|
|
|
1705
1691
|
try {
|
|
1706
1692
|
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.allowSubagents === false || 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
|
+
}
|