@shawnstack/quickforge 1.4.0 → 1.5.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/README.md +12 -12
- package/bin/quickforge.mjs +9 -0
- package/dist/assets/AgentProfilesPage-DUmXUxjA.js +1 -0
- package/dist/assets/ChatPanelHost-Syx0SSLe.js +242 -0
- package/dist/assets/PluginsPage-kiBq0gOT.js +1 -0
- package/dist/assets/ScheduledTasksPage-Dw4-tgp9.js +2 -0
- package/dist/assets/SharedConversationPage-CaE9bNb9.js +1 -0
- package/dist/assets/TerminalDock-BYJcp8Ts.js +2 -0
- package/dist/assets/WorkspaceInspector-Bzmv8Cvi.js +3 -0
- package/dist/assets/WorkspaceReaderDialog-BJo_KEWi.js +1 -0
- package/dist/assets/diff-line-counts-BZoYp5ai.js +10 -0
- package/dist/assets/icons-47L5YLKz.js +1 -0
- package/dist/assets/index-CqfScETb.js +1200 -0
- package/dist/assets/index-DzkBgHZf.css +3 -0
- package/dist/assets/{monaco-DG4TcBMc.js → monaco-CGq6uVF1.js} +1 -1
- package/dist/assets/{react-vendor-CiCXOLb5.js → react-vendor-DunfCFfp.js} +1 -1
- package/dist/favicon.svg +16 -1
- package/dist/index.html +5 -5
- package/dist/manifest.webmanifest +30 -30
- package/package.json +3 -2
- package/server/acp/server.mjs +921 -0
- package/server/agent-manager.mjs +283 -45
- package/server/agent-profile-files.mjs +179 -0
- package/server/agent-profiles.mjs +59 -5
- package/server/approval-store.mjs +13 -1
- package/server/auto-compaction.mjs +111 -112
- package/server/channels/process-channel.mjs +278 -0
- package/server/channels/providers/wechat.mjs +271 -0
- package/server/channels/registry.mjs +58 -0
- package/server/context-usage.mjs +108 -0
- package/server/custom-commands.mjs +157 -28
- package/server/frontmatter.mjs +167 -0
- package/server/index.mjs +52 -3
- package/server/mcp/registry.mjs +40 -0
- package/server/project-config.mjs +43 -6
- package/server/routes/agent-profiles.mjs +6 -2
- package/server/routes/agent.mjs +13 -2
- package/server/routes/channels.mjs +145 -0
- package/server/routes/mcp.mjs +7 -1
- package/server/routes/models.mjs +68 -0
- package/server/routes/project.mjs +34 -4
- package/server/routes/scheduled-tasks.mjs +6 -5
- package/server/routes/shared-conversation.mjs +1 -1
- package/server/routes/storage.mjs +4 -2
- package/server/routes/system.mjs +27 -0
- package/server/routes/tools.mjs +17 -6
- package/server/routes/workspace.mjs +138 -0
- package/server/session-utils.mjs +10 -2
- package/server/storage.mjs +30 -2
- package/server/subagents.mjs +8 -6
- package/server/system-prompt.mjs +3 -2
- package/server/tools/definitions.mjs +19 -1
- package/server/tools/index.mjs +83 -0
- package/server/utils/package-update.mjs +156 -0
- package/dist/assets/AgentProfilesPage-C79teCgh.js +0 -1
- package/dist/assets/ChatPanelHost-BjdIshtX.js +0 -195
- package/dist/assets/PluginsPage-Dt7Iiddo.js +0 -1
- package/dist/assets/ScheduledTasksPage-C047y3p3.js +0 -2
- package/dist/assets/SharedConversationPage-8X8kfztQ.js +0 -1
- package/dist/assets/TerminalDock-CEuJNf0m.js +0 -2
- package/dist/assets/WorkspaceInspector-BIa5gLVs.js +0 -3
- package/dist/assets/WorkspaceReaderDialog-bTeERaGd.js +0 -6
- package/dist/assets/icons-Dsc5yL3l.js +0 -1
- package/dist/assets/index-CPAWYhzz.css +0 -3
- package/dist/assets/index-YTL26wyJ.js +0 -814
package/server/agent-manager.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
formatSubagentTask,
|
|
12
12
|
} from './subagents.mjs'
|
|
13
13
|
import { agentProfileSnapshot, getAgentProfile } from './agent-profiles.mjs'
|
|
14
|
-
import { projectContextFromId, readProjectConfig } from './project-config.mjs'
|
|
14
|
+
import { projectContextFromId, defaultGlobalWorkspaceContext, readProjectConfig } from './project-config.mjs'
|
|
15
15
|
import { readStore, atomicUpdate, atomicSessionMetadataUpdate, readSessionValue, writeSessionValue, deleteSessionValue } from './storage.mjs'
|
|
16
16
|
import { logger } from './utils/logger.mjs'
|
|
17
17
|
import { buildSystemPrompt, generateAiTitle, generateTitle } from './session-utils.mjs'
|
|
@@ -23,8 +23,11 @@ import {
|
|
|
23
23
|
} from './conversation-compaction.mjs'
|
|
24
24
|
import {
|
|
25
25
|
buildAutoCompactLoopMessages,
|
|
26
|
+
compactSessionInPlace,
|
|
27
|
+
DEFAULT_AUTO_COMPACT_SETTINGS,
|
|
26
28
|
estimateSessionContextUsage,
|
|
27
29
|
maybeAutoCompactSession,
|
|
30
|
+
readAutoCompactSettings,
|
|
28
31
|
} from './auto-compaction.mjs'
|
|
29
32
|
import {
|
|
30
33
|
handleInternalCommand,
|
|
@@ -35,7 +38,6 @@ import { omitDetailsForLlm, serverConvertToLlm, messageText, lastAssistantText }
|
|
|
35
38
|
import { isPlainObject, mergeQuickForgeTiming, wrapToolDefinition, wrapMcpToolDefinition, wrapPluginToolDefinition, sessionSkillsContext } from './tool-wiring.mjs'
|
|
36
39
|
import {
|
|
37
40
|
APPROVAL_TIMEOUT_MS,
|
|
38
|
-
commandRestrictedTools,
|
|
39
41
|
safeReadTools,
|
|
40
42
|
pendingApprovals,
|
|
41
43
|
pendingAutoCompactApprovals,
|
|
@@ -93,7 +95,7 @@ async function createServerTools(projectId, projectContext, skillsContext, inclu
|
|
|
93
95
|
.filter(isAllowed)
|
|
94
96
|
.map((definition) => wrapToolDefinition(definition, toolContext, toolPermissions))
|
|
95
97
|
|
|
96
|
-
if (includeWorkspaceTools &&
|
|
98
|
+
if (includeWorkspaceTools && projectContext) {
|
|
97
99
|
const definitions = workspaceTools.filter((definition) => includeSubagentTool || definition.name !== 'run_subagent')
|
|
98
100
|
tools.push(...definitions
|
|
99
101
|
.filter(isAllowed)
|
|
@@ -118,7 +120,7 @@ async function rebuildSessionTools(session) {
|
|
|
118
120
|
session.projectId,
|
|
119
121
|
session.projectContext,
|
|
120
122
|
sessionSkillsContext(session),
|
|
121
|
-
!!
|
|
123
|
+
!!session.projectContext,
|
|
122
124
|
createCommandToolPermissions(session),
|
|
123
125
|
{ parentSessionId: session.sessionId },
|
|
124
126
|
)
|
|
@@ -130,7 +132,26 @@ async function rebuildSessionTools(session) {
|
|
|
130
132
|
|
|
131
133
|
const agentSessions = new Map()
|
|
132
134
|
|
|
133
|
-
|
|
135
|
+
const AGENT_ACCESS_MODE_DEFAULT = 'default'
|
|
136
|
+
const AGENT_ACCESS_MODE_FULL_ACCESS = 'full-access'
|
|
137
|
+
|
|
138
|
+
function normalizeAccessMode(value, fallback = AGENT_ACCESS_MODE_DEFAULT) {
|
|
139
|
+
if (value === AGENT_ACCESS_MODE_DEFAULT || value === AGENT_ACCESS_MODE_FULL_ACCESS) return value
|
|
140
|
+
if (value === true || value === 'true') return AGENT_ACCESS_MODE_FULL_ACCESS
|
|
141
|
+
if (value === false || value === 'false') return AGENT_ACCESS_MODE_DEFAULT
|
|
142
|
+
if (fallback !== value) return normalizeAccessMode(fallback, AGENT_ACCESS_MODE_DEFAULT)
|
|
143
|
+
return AGENT_ACCESS_MODE_DEFAULT
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function yoloModeFromAccessMode(accessMode) {
|
|
147
|
+
return normalizeAccessMode(accessMode) === AGENT_ACCESS_MODE_FULL_ACCESS
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function hasFullAccess(session) {
|
|
151
|
+
return normalizeAccessMode(session?.accessMode, session?.yoloMode) === AGENT_ACCESS_MODE_FULL_ACCESS
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** @typedef {{ agent: Agent, projectContext: object|null, projectId: string|null, accessMode: string, 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 */
|
|
134
155
|
|
|
135
156
|
const IDLE_TIMEOUT_MS = 30 * 60 * 1000 // 30 minutes
|
|
136
157
|
const SUBAGENT_DEFAULT_TIMEOUT_MS = 30 * 60 * 1000
|
|
@@ -371,12 +392,12 @@ function finishManualSessionRun(session, status, errorMessage) {
|
|
|
371
392
|
session.agent.state.errorMessage = errorMessage
|
|
372
393
|
}
|
|
373
394
|
|
|
374
|
-
async function
|
|
395
|
+
async function summarySession(session, initialUserMessage, summaryOptions) {
|
|
375
396
|
if (session.agent.state.isStreaming) {
|
|
376
397
|
session.agent.state.messages = [
|
|
377
398
|
...session.agent.state.messages,
|
|
378
399
|
initialUserMessage,
|
|
379
|
-
assistantTextMessage('Cannot
|
|
400
|
+
assistantTextMessage('Cannot summarize while a generation is still running. Stop it or wait until it finishes, then run /summary again.', session.model),
|
|
380
401
|
]
|
|
381
402
|
await persistSession(session)
|
|
382
403
|
const messages = session.agent.state.messages
|
|
@@ -400,13 +421,13 @@ async function compactSession(session, initialUserMessage, compactOptions) {
|
|
|
400
421
|
|
|
401
422
|
try {
|
|
402
423
|
const originalMessages = session.agent.state.messages.slice()
|
|
403
|
-
const options = parseCompactArgs(
|
|
424
|
+
const options = parseCompactArgs(summaryOptions?.args || '')
|
|
404
425
|
|
|
405
426
|
if (options.unsupported?.length) {
|
|
406
427
|
session.agent.state.messages = [
|
|
407
428
|
...originalMessages,
|
|
408
429
|
initialUserMessage,
|
|
409
|
-
assistantTextMessage(`Unsupported /
|
|
430
|
+
assistantTextMessage(`Unsupported /summary option(s): ${options.unsupported.join(', ')}\n\nSupported usage: /summary or /summary keep=0`, session.model),
|
|
410
431
|
]
|
|
411
432
|
finishManualSessionRun(session, 'idle')
|
|
412
433
|
await persistSession(session)
|
|
@@ -428,7 +449,7 @@ async function compactSession(session, initialUserMessage, compactOptions) {
|
|
|
428
449
|
session.agent.state.messages = [
|
|
429
450
|
...originalMessages,
|
|
430
451
|
initialUserMessage,
|
|
431
|
-
assistantTextMessage('Not enough earlier history to
|
|
452
|
+
assistantTextMessage('Not enough earlier history to summarize. Continue chatting and run /summary again later.', session.model),
|
|
432
453
|
]
|
|
433
454
|
finishManualSessionRun(session, 'idle')
|
|
434
455
|
await persistSession(session)
|
|
@@ -464,6 +485,7 @@ async function compactSession(session, initialUserMessage, compactOptions) {
|
|
|
464
485
|
const compactedSession = await createAgent(compactedSessionId, {
|
|
465
486
|
scope: session.scope,
|
|
466
487
|
projectId: session.projectId,
|
|
488
|
+
accessMode: session.accessMode,
|
|
467
489
|
yoloMode: session.yoloMode,
|
|
468
490
|
model: session.model,
|
|
469
491
|
thinkingLevel: session.thinkingLevel,
|
|
@@ -513,6 +535,97 @@ async function compactSession(session, initialUserMessage, compactOptions) {
|
|
|
513
535
|
}
|
|
514
536
|
}
|
|
515
537
|
|
|
538
|
+
async function compactSession(session, initialUserMessage, compactOptions) {
|
|
539
|
+
if (session.agent.state.isStreaming) {
|
|
540
|
+
session.agent.state.messages = [
|
|
541
|
+
...session.agent.state.messages,
|
|
542
|
+
initialUserMessage,
|
|
543
|
+
assistantTextMessage('Cannot compact while a generation is still running. Stop it or wait until it finishes, then run /compact again.', session.model),
|
|
544
|
+
]
|
|
545
|
+
await persistSession(session)
|
|
546
|
+
const messages = session.agent.state.messages
|
|
547
|
+
emitSessionEvent(session, { type: 'message_end', messages })
|
|
548
|
+
emitSessionEvent(session, { type: 'agent_end', messages })
|
|
549
|
+
return { sessionId: session.sessionId, status: session.status }
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const args = String(compactOptions?.args || '').trim()
|
|
553
|
+
if (args) {
|
|
554
|
+
session.agent.state.messages = [
|
|
555
|
+
...session.agent.state.messages,
|
|
556
|
+
initialUserMessage,
|
|
557
|
+
assistantTextMessage('Unsupported /compact option(s). Supported usage: /compact', session.model),
|
|
558
|
+
]
|
|
559
|
+
session.status = 'idle'
|
|
560
|
+
session.finishedAt = new Date().toISOString()
|
|
561
|
+
await persistSession(session)
|
|
562
|
+
const messages = session.agent.state.messages
|
|
563
|
+
emitSessionEvent(session, { type: 'message_end', messages })
|
|
564
|
+
emitSessionEvent(session, { type: 'agent_end', messages })
|
|
565
|
+
return { sessionId: session.sessionId, status: session.status }
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
resetIdleTimer(session)
|
|
569
|
+
session.status = 'running'
|
|
570
|
+
session.startedAt = session.startedAt ?? new Date().toISOString()
|
|
571
|
+
session.finishedAt = null
|
|
572
|
+
session.agent.state.isStreaming = true
|
|
573
|
+
session.agent.state.errorMessage = undefined
|
|
574
|
+
emitSessionEvent(session, { type: 'agent_start' })
|
|
575
|
+
|
|
576
|
+
try {
|
|
577
|
+
const messages = session.agent.state.messages.slice()
|
|
578
|
+
const settings = await readAutoCompactSettings().catch(() => DEFAULT_AUTO_COMPACT_SETTINGS)
|
|
579
|
+
const usage = getSessionContextUsage(session)
|
|
580
|
+
const result = await compactSessionInPlace({
|
|
581
|
+
session,
|
|
582
|
+
messages,
|
|
583
|
+
keepRecentTurns: settings.keepRecentTurns,
|
|
584
|
+
minSourceChars: settings.minSourceChars,
|
|
585
|
+
usage,
|
|
586
|
+
thresholdPercent: settings.thresholdPercent,
|
|
587
|
+
emitSessionEvent,
|
|
588
|
+
persistSession,
|
|
589
|
+
reason: 'manual_compact',
|
|
590
|
+
onBeforePersist: () => {
|
|
591
|
+
finishManualSessionRun(session, 'idle')
|
|
592
|
+
},
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
if (!result.compacted) {
|
|
596
|
+
session.agent.state.messages = [
|
|
597
|
+
...messages,
|
|
598
|
+
initialUserMessage,
|
|
599
|
+
assistantTextMessage('Not enough earlier history to compact. Continue chatting and run /compact again later.', session.model),
|
|
600
|
+
]
|
|
601
|
+
finishManualSessionRun(session, 'idle')
|
|
602
|
+
await persistSession(session)
|
|
603
|
+
const nextMessages = session.agent.state.messages
|
|
604
|
+
emitSessionEvent(session, { type: 'message_end', messages: nextMessages })
|
|
605
|
+
emitSessionEvent(session, { type: 'agent_end', messages: nextMessages })
|
|
606
|
+
return { sessionId: session.sessionId, status: session.status }
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const nextMessages = session.agent.state.messages
|
|
610
|
+
emitSessionEvent(session, { type: 'message_end', messages: nextMessages })
|
|
611
|
+
emitSessionEvent(session, { type: 'agent_end', messages: nextMessages })
|
|
612
|
+
return { sessionId: session.sessionId, status: session.status }
|
|
613
|
+
} catch (err) {
|
|
614
|
+
const errorMessage = err?.message || 'Conversation compaction failed'
|
|
615
|
+
session.agent.state.messages = [
|
|
616
|
+
...session.agent.state.messages,
|
|
617
|
+
initialUserMessage,
|
|
618
|
+
assistantTextMessage(`Conversation compaction failed: ${errorMessage}`, session.model),
|
|
619
|
+
]
|
|
620
|
+
finishManualSessionRun(session, 'error', errorMessage)
|
|
621
|
+
await persistSession(session)
|
|
622
|
+
const messages = session.agent.state.messages
|
|
623
|
+
emitSessionEvent(session, { type: 'error', error: errorMessage })
|
|
624
|
+
emitSessionEvent(session, { type: 'agent_end', messages, errorMessage })
|
|
625
|
+
return { sessionId: session.sessionId, status: session.status }
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
516
629
|
async function clearSession(session) {
|
|
517
630
|
if (session.agent.state.isStreaming) {
|
|
518
631
|
session.agent.state.messages = [
|
|
@@ -545,22 +658,68 @@ async function clearSession(session) {
|
|
|
545
658
|
return { sessionId: session.sessionId, status: session.status, cleared: true }
|
|
546
659
|
}
|
|
547
660
|
|
|
548
|
-
|
|
661
|
+
const QUICKFORGE_COMMAND_DETAILS_KEY = 'quickforgeCommand'
|
|
662
|
+
|
|
663
|
+
function normalizedPromptCommand(command) {
|
|
664
|
+
return command?.type === 'plan' ? { type: 'plan' } : null
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function objectDetails(message) {
|
|
668
|
+
const details = message?.details
|
|
669
|
+
return details && typeof details === 'object' && !Array.isArray(details) ? details : {}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function promptCommandFromMessage(message) {
|
|
673
|
+
return normalizedPromptCommand(objectDetails(message)[QUICKFORGE_COMMAND_DETAILS_KEY])
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function messageWithPromptCommand(message, command) {
|
|
677
|
+
const normalized = normalizedPromptCommand(command)
|
|
678
|
+
if (!normalized || !message || typeof message !== 'object') return message
|
|
679
|
+
return {
|
|
680
|
+
...message,
|
|
681
|
+
details: {
|
|
682
|
+
...objectDetails(message),
|
|
683
|
+
[QUICKFORGE_COMMAND_DETAILS_KEY]: normalized,
|
|
684
|
+
},
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function internalInvocationForPromptCommand(userMessage, command) {
|
|
689
|
+
const normalized = normalizedPromptCommand(command)
|
|
690
|
+
if (normalized?.type === 'plan') {
|
|
691
|
+
// Derive the task from the message text. Strip a leading "/plan" so that
|
|
692
|
+
// toggling plan mode while typing "/plan <task>" yields the clean task —
|
|
693
|
+
// matching the slash-command parse path and avoiding a redundant prefix.
|
|
694
|
+
const raw = messageText(userMessage).trim()
|
|
695
|
+
const planPrefix = raw.match(/^\/plan(?:\s+([\s\S]*))?$/i)
|
|
696
|
+
return { type: 'plan', args: planPrefix ? (planPrefix[1] || '').trim() : raw }
|
|
697
|
+
}
|
|
698
|
+
return parseInternalCommandInvocation(userMessage)
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function planCommandState(userMessage, args) {
|
|
702
|
+
return {
|
|
703
|
+
userMessage: messageWithPromptCommand(userMessage, { type: 'plan' }),
|
|
704
|
+
commandPrompt: formatPlanCommandPrompt(args),
|
|
705
|
+
permissions: { allowEdit: false, allowCommands: false, allowSubagents: true },
|
|
706
|
+
commandName: 'plan',
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
async function resolveCommandState(session, userMessage, promptCommand = null) {
|
|
711
|
+
const command = normalizedPromptCommand(promptCommand) || promptCommandFromMessage(userMessage)
|
|
549
712
|
const internalResponse = await handleInternalCommand(
|
|
550
|
-
|
|
713
|
+
internalInvocationForPromptCommand(userMessage, command),
|
|
551
714
|
session.projectContext?.workspaceRoot,
|
|
552
715
|
session.projectContext?.project?.commandDir,
|
|
553
716
|
)
|
|
554
717
|
if (typeof internalResponse === 'string') return { textResponse: internalResponse }
|
|
555
718
|
if (internalResponse?.clear) return { clear: internalResponse }
|
|
719
|
+
if (internalResponse?.summary) return { summary: internalResponse }
|
|
556
720
|
if (internalResponse?.compact) return { compact: internalResponse }
|
|
557
721
|
if (internalResponse?.plan) {
|
|
558
|
-
return
|
|
559
|
-
userMessage,
|
|
560
|
-
commandPrompt: formatPlanCommandPrompt(internalResponse.args),
|
|
561
|
-
permissions: { allowEdit: false, allowCommands: false, allowSubagents: true },
|
|
562
|
-
commandName: 'plan',
|
|
563
|
-
}
|
|
722
|
+
return planCommandState(userMessage, internalResponse.args)
|
|
564
723
|
}
|
|
565
724
|
if (internalResponse?.review) {
|
|
566
725
|
return {
|
|
@@ -571,7 +730,22 @@ async function resolveCommandState(session, userMessage) {
|
|
|
571
730
|
}
|
|
572
731
|
}
|
|
573
732
|
|
|
574
|
-
if (!session.projectContext?.workspaceRoot)
|
|
733
|
+
if (!session.projectContext?.workspaceRoot) {
|
|
734
|
+
// Even without a project, user-level custom commands (~/.quickforge/commands/) are available
|
|
735
|
+
const invocation = await resolveCustomCommandInvocation(
|
|
736
|
+
userMessage,
|
|
737
|
+
null,
|
|
738
|
+
session.projectContext?.project?.commandDir,
|
|
739
|
+
)
|
|
740
|
+
if (!invocation) return { userMessage }
|
|
741
|
+
|
|
742
|
+
return {
|
|
743
|
+
userMessage,
|
|
744
|
+
commandPrompt: invocation.systemPrompt,
|
|
745
|
+
permissions: invocation.permissions,
|
|
746
|
+
commandName: invocation.command.name,
|
|
747
|
+
}
|
|
748
|
+
}
|
|
575
749
|
|
|
576
750
|
const invocation = await resolveCustomCommandInvocation(
|
|
577
751
|
userMessage,
|
|
@@ -645,7 +819,7 @@ ${scopeText}
|
|
|
645
819
|
}
|
|
646
820
|
|
|
647
821
|
async function runSubagent(parentSession, params, parentSignal, onUpdate) {
|
|
648
|
-
const profile = await getAgentProfile(params?.subagent)
|
|
822
|
+
const profile = await getAgentProfile(params?.subagent, { workspaceRoot: parentSession.projectContext?.workspaceRoot })
|
|
649
823
|
if (!profile || !profile.enabledAsSubagent) {
|
|
650
824
|
const error = new Error(`Unknown or disabled subagent: ${params?.subagent || ''}`)
|
|
651
825
|
error.statusCode = 400
|
|
@@ -769,7 +943,7 @@ async function runSubagent(parentSession, params, parentSignal, onUpdate) {
|
|
|
769
943
|
}
|
|
770
944
|
const commandPermissionError = commandToolPermissionError(parentSession, toolName)
|
|
771
945
|
if (commandPermissionError) return { block: true, reason: commandPermissionError }
|
|
772
|
-
if (!parentSession
|
|
946
|
+
if (!hasFullAccess(parentSession)) {
|
|
773
947
|
if (safeReadTools.has(toolName)) return undefined
|
|
774
948
|
return createApprovalPromise(parentSession, context.toolCall?.id, toolName, context.args, {
|
|
775
949
|
type: 'subagent',
|
|
@@ -950,8 +1124,15 @@ async function transformSessionContext(session, messages, signal) {
|
|
|
950
1124
|
export const agentEvents = new EventEmitter()
|
|
951
1125
|
agentEvents.setMaxListeners(100)
|
|
952
1126
|
|
|
1127
|
+
function isIdleRetainedSession(session) {
|
|
1128
|
+
return session?.idleRetention === 'always'
|
|
1129
|
+
}
|
|
1130
|
+
|
|
953
1131
|
function resetIdleTimer(session) {
|
|
954
1132
|
if (session.idleTimer) clearTimeout(session.idleTimer)
|
|
1133
|
+
session.idleTimer = null
|
|
1134
|
+
if (isIdleRetainedSession(session)) return
|
|
1135
|
+
|
|
955
1136
|
session.idleTimer = setTimeout(() => {
|
|
956
1137
|
if (session.status === 'running') {
|
|
957
1138
|
logger.info(`Session ${session.sessionId} idle timer fired but still running, resetting...`, { sessionId: session.sessionId, status: session.status })
|
|
@@ -986,6 +1167,7 @@ export function touchSession(sessionId) {
|
|
|
986
1167
|
export async function createAgent(sessionId, config = {}) {
|
|
987
1168
|
const existing = agentSessions.get(sessionId)
|
|
988
1169
|
if (existing) {
|
|
1170
|
+
if (config.idleRetention !== undefined) existing.idleRetention = config.idleRetention
|
|
989
1171
|
resetIdleTimer(existing)
|
|
990
1172
|
return existing
|
|
991
1173
|
}
|
|
@@ -993,6 +1175,7 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
993
1175
|
const {
|
|
994
1176
|
scope = 'global',
|
|
995
1177
|
projectId = null,
|
|
1178
|
+
accessMode: rawAccessMode,
|
|
996
1179
|
yoloMode = false,
|
|
997
1180
|
model = null,
|
|
998
1181
|
thinkingLevel = 'off',
|
|
@@ -1003,17 +1186,23 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1003
1186
|
lastModified = null,
|
|
1004
1187
|
contextCompaction = null,
|
|
1005
1188
|
agentProfile = null,
|
|
1189
|
+
idleRetention = null,
|
|
1006
1190
|
} = config
|
|
1191
|
+
const accessMode = normalizeAccessMode(rawAccessMode, yoloMode)
|
|
1192
|
+
const resolvedYoloMode = yoloModeFromAccessMode(accessMode)
|
|
1007
1193
|
|
|
1008
|
-
// Resolve project context for tool calls
|
|
1194
|
+
// Resolve project context for tool calls. Project conversations resolve to
|
|
1195
|
+
// their directory; global conversations (no projectId) and any fallback fall
|
|
1196
|
+
// back to a synthetic default workspace context so file tools stay available.
|
|
1009
1197
|
let projectContext = null
|
|
1010
1198
|
if (projectId) {
|
|
1011
1199
|
try {
|
|
1012
1200
|
projectContext = await projectContextFromId(projectId)
|
|
1013
1201
|
} catch {
|
|
1014
|
-
// project not found —
|
|
1202
|
+
// project not found — fall back to the default workspace below
|
|
1015
1203
|
}
|
|
1016
1204
|
}
|
|
1205
|
+
projectContext ??= defaultGlobalWorkspaceContext()
|
|
1017
1206
|
|
|
1018
1207
|
// Build system prompt
|
|
1019
1208
|
const projectConfig = await readProjectConfig()
|
|
@@ -1046,7 +1235,7 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1046
1235
|
projectId,
|
|
1047
1236
|
projectContext,
|
|
1048
1237
|
skillsContext,
|
|
1049
|
-
!!
|
|
1238
|
+
!!projectContext,
|
|
1050
1239
|
(toolName) => {
|
|
1051
1240
|
if (profileToolNames && !profileToolNames.includes(toolName)) return `Agent profile ${agentProfile.name} is not allowed to use ${toolName}.`
|
|
1052
1241
|
const session = agentSessions.get(sessionId)
|
|
@@ -1100,14 +1289,14 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1100
1289
|
if (profileToolNames && !profileToolNames.includes(toolName)) return { block: true, reason: `Agent profile ${agentProfile.name} is not allowed to use ${toolName}.` }
|
|
1101
1290
|
if (toolName === 'run_subagent') return undefined
|
|
1102
1291
|
if (isMcpToolName(toolName) || isPluginToolName(toolName)) {
|
|
1103
|
-
if (!currentSession
|
|
1292
|
+
if (!hasFullAccess(currentSession)) return createApprovalPromise(currentSession, toolCallId, toolName, context.args)
|
|
1104
1293
|
return undefined
|
|
1105
1294
|
}
|
|
1106
|
-
if (!projectContext) {
|
|
1295
|
+
if (!projectContext?.workspaceRoot) {
|
|
1107
1296
|
return { block: true, reason: 'No active project. Select a project to use tools.' }
|
|
1108
1297
|
}
|
|
1109
|
-
if (!currentSession
|
|
1110
|
-
//
|
|
1298
|
+
if (!hasFullAccess(currentSession)) {
|
|
1299
|
+
// Default access: safe reads auto-pass, state-changing or external tools require approval
|
|
1111
1300
|
if (safeReadTools.has(toolName)) return undefined
|
|
1112
1301
|
return createApprovalPromise(currentSession, toolCallId, toolName, context.args)
|
|
1113
1302
|
}
|
|
@@ -1123,7 +1312,8 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1123
1312
|
agent,
|
|
1124
1313
|
projectContext,
|
|
1125
1314
|
projectId,
|
|
1126
|
-
|
|
1315
|
+
accessMode,
|
|
1316
|
+
yoloMode: resolvedYoloMode,
|
|
1127
1317
|
model: resolvedModel,
|
|
1128
1318
|
thinkingLevel,
|
|
1129
1319
|
scope,
|
|
@@ -1146,6 +1336,7 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1146
1336
|
getApiKey,
|
|
1147
1337
|
contextCompaction,
|
|
1148
1338
|
agentProfile: agentProfile ? agentProfileSnapshot(agentProfile) : null,
|
|
1339
|
+
idleRetention,
|
|
1149
1340
|
lastTransformedContextMessages: null,
|
|
1150
1341
|
autoCompacting: false,
|
|
1151
1342
|
stateVersion: 0,
|
|
@@ -1157,7 +1348,7 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1157
1348
|
}
|
|
1158
1349
|
|
|
1159
1350
|
// Subscribe to agent lifecycle events and forward to eventBus
|
|
1160
|
-
agent.subscribe((event) => {
|
|
1351
|
+
agent.subscribe(async (event) => {
|
|
1161
1352
|
// The pi-agent-core agent loop emits agent_end with `messages` that only
|
|
1162
1353
|
// contains messages generated during THIS run (newMessages), not the
|
|
1163
1354
|
// complete session history. Replace with the authoritative full state
|
|
@@ -1187,10 +1378,14 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1187
1378
|
session.status = 'running'
|
|
1188
1379
|
session.startedAt = session.startedAt ?? new Date().toISOString()
|
|
1189
1380
|
session.finishedAt = null
|
|
1190
|
-
// Persist running state immediately so a browser refresh still shows the green dot
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
)
|
|
1381
|
+
// Persist running state immediately so a browser refresh still shows the green dot.
|
|
1382
|
+
// Brand-new runs have no messages until the first user message_end; persisting
|
|
1383
|
+
// here would only trigger the empty-session cleanup path.
|
|
1384
|
+
if (session.agent.state.messages.length > 0) {
|
|
1385
|
+
persistSession(session).catch((err) =>
|
|
1386
|
+
logger.error(`Failed to persist session on start ${sessionId}:`, err, { sessionId }),
|
|
1387
|
+
)
|
|
1388
|
+
}
|
|
1194
1389
|
}
|
|
1195
1390
|
|
|
1196
1391
|
if (event.type === 'agent_end') {
|
|
@@ -1206,15 +1401,28 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1206
1401
|
}
|
|
1207
1402
|
|
|
1208
1403
|
if (event.type === 'message_end') {
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1404
|
+
const isInitialUserMessage = (event.message?.role === 'user' || event.message?.role === 'user-with-attachments')
|
|
1405
|
+
&& session.agent.state.messages.length === 1
|
|
1406
|
+
if (isInitialUserMessage) {
|
|
1407
|
+
// External ACP channels run in a separate process from the web UI. Persist
|
|
1408
|
+
// the first user message immediately so the session becomes visible on
|
|
1409
|
+
// disk before a long/failed agent run can exit or be restarted.
|
|
1410
|
+
try {
|
|
1411
|
+
await flushSessionPersist(session)
|
|
1412
|
+
} catch (err) {
|
|
1413
|
+
logger.error(`Failed to persist initial user message for session ${sessionId}:`, err, { sessionId })
|
|
1414
|
+
}
|
|
1415
|
+
} else {
|
|
1416
|
+
// Debounced persist for crash recovery; coalesces the many message_end
|
|
1417
|
+
// events within a single run into infrequent full-session writes.
|
|
1418
|
+
scheduleSessionPersist(session)
|
|
1419
|
+
}
|
|
1212
1420
|
}
|
|
1213
1421
|
})
|
|
1214
1422
|
|
|
1215
1423
|
agentSessions.set(sessionId, session)
|
|
1216
1424
|
resetIdleTimer(session)
|
|
1217
|
-
logger.info(`Created session ${sessionId} (scope: ${scope}, project: ${projectId || 'none'},
|
|
1425
|
+
logger.info(`Created session ${sessionId} (scope: ${scope}, project: ${projectId || 'none'}, access: ${accessMode})`, { sessionId, scope, projectId: projectId || undefined, accessMode, yoloMode: resolvedYoloMode, idleRetention: idleRetention || undefined })
|
|
1218
1426
|
return session
|
|
1219
1427
|
}
|
|
1220
1428
|
|
|
@@ -1246,7 +1454,7 @@ function sessionLastModifiedFromMessages(messages, fallback) {
|
|
|
1246
1454
|
* Persist session data to storage.
|
|
1247
1455
|
*/
|
|
1248
1456
|
async function persistSession(session) {
|
|
1249
|
-
const { sessionId, agent, scope, projectId, title, createdAt, lastModified: storedLastModified, status, startedAt, finishedAt, model, thinkingLevel, yoloMode, contextCompaction } = session
|
|
1457
|
+
const { sessionId, agent, scope, projectId, title, createdAt, lastModified: storedLastModified, status, startedAt, finishedAt, model, thinkingLevel, accessMode, yoloMode, contextCompaction } = session
|
|
1250
1458
|
const messages = agent.state.messages
|
|
1251
1459
|
|
|
1252
1460
|
if (messages.length === 0) {
|
|
@@ -1269,6 +1477,7 @@ async function persistSession(session) {
|
|
|
1269
1477
|
title,
|
|
1270
1478
|
model,
|
|
1271
1479
|
thinkingLevel,
|
|
1480
|
+
accessMode,
|
|
1272
1481
|
yoloMode,
|
|
1273
1482
|
messages,
|
|
1274
1483
|
createdAt: createdAt || now,
|
|
@@ -1279,6 +1488,7 @@ async function persistSession(session) {
|
|
|
1279
1488
|
taskStartedAt: startedAt,
|
|
1280
1489
|
taskFinishedAt: finishedAt,
|
|
1281
1490
|
contextCompaction: contextCompaction || undefined,
|
|
1491
|
+
idleRetention: session.idleRetention || undefined,
|
|
1282
1492
|
}
|
|
1283
1493
|
session.lastModified = lastModified
|
|
1284
1494
|
|
|
@@ -1320,6 +1530,7 @@ async function persistSession(session) {
|
|
|
1320
1530
|
messageCount: messages.length,
|
|
1321
1531
|
usage,
|
|
1322
1532
|
thinkingLevel,
|
|
1533
|
+
accessMode,
|
|
1323
1534
|
yoloMode,
|
|
1324
1535
|
preview,
|
|
1325
1536
|
scope,
|
|
@@ -1334,6 +1545,7 @@ async function persistSession(session) {
|
|
|
1334
1545
|
thresholdPercent: contextCompaction.thresholdPercent,
|
|
1335
1546
|
usageBefore: contextCompaction.usageBefore,
|
|
1336
1547
|
} : undefined,
|
|
1548
|
+
idleRetention: session.idleRetention || undefined,
|
|
1337
1549
|
}
|
|
1338
1550
|
|
|
1339
1551
|
// Write to storage atomically (read-modify-write within queue)
|
|
@@ -1452,7 +1664,7 @@ export async function rollbackSessionMessages(sessionId, rollbackMessageIndex) {
|
|
|
1452
1664
|
* Send a user message to the agent and start the agent loop.
|
|
1453
1665
|
* Returns immediately; events are streamed via the event bus.
|
|
1454
1666
|
*/
|
|
1455
|
-
export async function runPrompt(sessionId, message, selectedCapabilities = []) {
|
|
1667
|
+
export async function runPrompt(sessionId, message, selectedCapabilities = [], promptCommand = null) {
|
|
1456
1668
|
let session = agentSessions.get(sessionId)
|
|
1457
1669
|
if (!session) {
|
|
1458
1670
|
session = await restoreAgent(sessionId)
|
|
@@ -1471,7 +1683,7 @@ export async function runPrompt(sessionId, message, selectedCapabilities = []) {
|
|
|
1471
1683
|
const initialUserMessage = typeof message === 'string'
|
|
1472
1684
|
? { role: 'user', content: message, timestamp: new Date().toISOString() }
|
|
1473
1685
|
: message
|
|
1474
|
-
const commandState = await resolveCommandState(session, initialUserMessage)
|
|
1686
|
+
const commandState = await resolveCommandState(session, initialUserMessage, promptCommand)
|
|
1475
1687
|
const userMessage = commandState.userMessage ?? initialUserMessage
|
|
1476
1688
|
|
|
1477
1689
|
if (commandState.textResponse) {
|
|
@@ -1491,6 +1703,10 @@ export async function runPrompt(sessionId, message, selectedCapabilities = []) {
|
|
|
1491
1703
|
return clearSession(session)
|
|
1492
1704
|
}
|
|
1493
1705
|
|
|
1706
|
+
if (commandState.summary) {
|
|
1707
|
+
return summarySession(session, initialUserMessage, commandState.summary)
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1494
1710
|
if (commandState.compact) {
|
|
1495
1711
|
return compactSession(session, initialUserMessage, commandState.compact)
|
|
1496
1712
|
}
|
|
@@ -1569,14 +1785,27 @@ export async function continueSession(sessionId) {
|
|
|
1569
1785
|
throw Object.assign(new Error('Cannot continue: no user message found.'), { statusCode: 400 })
|
|
1570
1786
|
}
|
|
1571
1787
|
|
|
1572
|
-
const
|
|
1788
|
+
const lastUserMessage = messages[lastUserIndex]
|
|
1789
|
+
const commandState = await resolveCommandState(session, lastUserMessage)
|
|
1790
|
+
const continuedUserMessage = commandState.userMessage ?? lastUserMessage
|
|
1791
|
+
const trimmedMessages = messages.slice(0, lastUserIndex).concat(continuedUserMessage)
|
|
1573
1792
|
updateSessionMessages(session, trimmedMessages)
|
|
1574
1793
|
resetSessionCompaction(session)
|
|
1575
1794
|
|
|
1576
1795
|
resetIdleTimer(session)
|
|
1796
|
+
session.activeCommandName = commandState.commandName ?? null
|
|
1797
|
+
session.activeCommandPermissions = commandState.permissions ?? null
|
|
1798
|
+
session.activeCommandPrompt = commandState.commandPrompt ?? null
|
|
1799
|
+
session.activeCapabilityPrompt = null
|
|
1800
|
+
|
|
1577
1801
|
session.agent.continue().catch((err) => {
|
|
1578
1802
|
logger.error(`Agent continue error for session ${sessionId}:`, err, { sessionId })
|
|
1579
1803
|
emitSessionEvent(session, { type: 'error', error: err.message || 'Unknown error' })
|
|
1804
|
+
}).finally(() => {
|
|
1805
|
+
session.activeCommandName = null
|
|
1806
|
+
session.activeCommandPermissions = null
|
|
1807
|
+
session.activeCommandPrompt = null
|
|
1808
|
+
session.activeCapabilityPrompt = null
|
|
1580
1809
|
})
|
|
1581
1810
|
|
|
1582
1811
|
return { sessionId, status: 'running' }
|
|
@@ -1678,6 +1907,7 @@ export function getSessionState(sessionId) {
|
|
|
1678
1907
|
sessionId: session.sessionId,
|
|
1679
1908
|
scope: session.scope,
|
|
1680
1909
|
projectId: session.projectId,
|
|
1910
|
+
accessMode: session.accessMode,
|
|
1681
1911
|
yoloMode: session.yoloMode,
|
|
1682
1912
|
systemPrompt: session.agent.state.systemPrompt,
|
|
1683
1913
|
model: session.model,
|
|
@@ -1822,6 +2052,7 @@ export async function restoreAgent(sessionId) {
|
|
|
1822
2052
|
return await createAgent(sessionId, {
|
|
1823
2053
|
scope: sessionData.scope || 'global',
|
|
1824
2054
|
projectId: sessionData.projectId || null,
|
|
2055
|
+
accessMode: normalizeAccessMode(sessionData.accessMode, sessionData.yoloMode),
|
|
1825
2056
|
yoloMode: sessionData.yoloMode || false,
|
|
1826
2057
|
model: sessionData.model,
|
|
1827
2058
|
thinkingLevel: sessionData.thinkingLevel || 'off',
|
|
@@ -1830,6 +2061,7 @@ export async function restoreAgent(sessionId) {
|
|
|
1830
2061
|
createdAt: sessionData.createdAt,
|
|
1831
2062
|
lastModified: sessionData.lastModified,
|
|
1832
2063
|
contextCompaction: sessionData.contextCompaction || null,
|
|
2064
|
+
idleRetention: sessionData.idleRetention || null,
|
|
1833
2065
|
})
|
|
1834
2066
|
} catch (err) {
|
|
1835
2067
|
logger.error(`Failed to restore agent ${sessionId}:`, err, { sessionId })
|
|
@@ -1902,6 +2134,7 @@ export function listSessions() {
|
|
|
1902
2134
|
scope: session.scope,
|
|
1903
2135
|
status: session.status,
|
|
1904
2136
|
title: session.title,
|
|
2137
|
+
idleRetention: session.idleRetention || undefined,
|
|
1905
2138
|
})
|
|
1906
2139
|
}
|
|
1907
2140
|
return result
|
|
@@ -1923,20 +2156,25 @@ export async function refreshAllSessionTools() {
|
|
|
1923
2156
|
return result
|
|
1924
2157
|
}
|
|
1925
2158
|
|
|
1926
|
-
export async function
|
|
2159
|
+
export async function updateSessionAccessMode(sessionId, accessMode) {
|
|
1927
2160
|
const session = agentSessions.get(sessionId)
|
|
1928
2161
|
if (!session) {
|
|
1929
2162
|
throw Object.assign(new Error('Session not found'), { statusCode: 404 })
|
|
1930
2163
|
}
|
|
1931
2164
|
|
|
1932
|
-
session.
|
|
2165
|
+
session.accessMode = normalizeAccessMode(accessMode, session.accessMode)
|
|
2166
|
+
session.yoloMode = yoloModeFromAccessMode(session.accessMode)
|
|
1933
2167
|
await rebuildSessionTools(session)
|
|
1934
2168
|
await persistSession(session)
|
|
1935
2169
|
|
|
1936
2170
|
const state = getSessionState(sessionId)
|
|
1937
2171
|
emitSessionEvent(session, { type: 'state', ...state })
|
|
1938
2172
|
|
|
1939
|
-
return { sessionId, yoloMode: session.yoloMode }
|
|
2173
|
+
return { sessionId, accessMode: session.accessMode, yoloMode: session.yoloMode }
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
export async function updateSessionYoloMode(sessionId, yoloMode) {
|
|
2177
|
+
return updateSessionAccessMode(sessionId, yoloMode ? AGENT_ACCESS_MODE_FULL_ACCESS : AGENT_ACCESS_MODE_DEFAULT)
|
|
1940
2178
|
}
|
|
1941
2179
|
|
|
1942
2180
|
/**
|