longer-agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +227 -0
- package/README.zh-CN.md +227 -0
- package/agent_templates/executor/agent.yaml +22 -0
- package/agent_templates/executor/system_prompt.md +17 -0
- package/agent_templates/explorer/agent.yaml +13 -0
- package/agent_templates/explorer/system_prompt.md +19 -0
- package/agent_templates/main/agent.yaml +7 -0
- package/agent_templates/main/system_prompt.md +45 -0
- package/configExample.yaml +83 -0
- package/dist/agents/agent.d.ts +79 -0
- package/dist/agents/agent.d.ts.map +1 -0
- package/dist/agents/agent.js +156 -0
- package/dist/agents/agent.js.map +1 -0
- package/dist/agents/tool-loop.d.ts +140 -0
- package/dist/agents/tool-loop.d.ts.map +1 -0
- package/dist/agents/tool-loop.js +465 -0
- package/dist/agents/tool-loop.js.map +1 -0
- package/dist/ask.d.ts +81 -0
- package/dist/ask.d.ts.map +1 -0
- package/dist/ask.js +34 -0
- package/dist/ask.js.map +1 -0
- package/dist/auth/openai-oauth.d.ts +66 -0
- package/dist/auth/openai-oauth.d.ts.map +1 -0
- package/dist/auth/openai-oauth.js +640 -0
- package/dist/auth/openai-oauth.js.map +1 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +254 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands.d.ts +118 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +862 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.d.ts +130 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +648 -0
- package/dist/config.js.map +1 -0
- package/dist/context-rendering.d.ts +69 -0
- package/dist/context-rendering.d.ts.map +1 -0
- package/dist/context-rendering.js +250 -0
- package/dist/context-rendering.js.map +1 -0
- package/dist/document-projection.d.ts +12 -0
- package/dist/document-projection.d.ts.map +1 -0
- package/dist/document-projection.js +75 -0
- package/dist/document-projection.js.map +1 -0
- package/dist/ephemeral-log.d.ts +15 -0
- package/dist/ephemeral-log.d.ts.map +1 -0
- package/dist/ephemeral-log.js +173 -0
- package/dist/ephemeral-log.js.map +1 -0
- package/dist/file-attach.d.ts +89 -0
- package/dist/file-attach.d.ts.map +1 -0
- package/dist/file-attach.js +571 -0
- package/dist/file-attach.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/init-wizard.d.ts +13 -0
- package/dist/init-wizard.d.ts.map +1 -0
- package/dist/init-wizard.js +328 -0
- package/dist/init-wizard.js.map +1 -0
- package/dist/log-entry.d.ts +104 -0
- package/dist/log-entry.d.ts.map +1 -0
- package/dist/log-entry.js +292 -0
- package/dist/log-entry.js.map +1 -0
- package/dist/log-projection.d.ts +73 -0
- package/dist/log-projection.d.ts.map +1 -0
- package/dist/log-projection.js +651 -0
- package/dist/log-projection.js.map +1 -0
- package/dist/mcp-client.d.ts +55 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +402 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/model-selection.d.ts +16 -0
- package/dist/model-selection.d.ts.map +1 -0
- package/dist/model-selection.js +181 -0
- package/dist/model-selection.js.map +1 -0
- package/dist/network-retry.d.ts +38 -0
- package/dist/network-retry.d.ts.map +1 -0
- package/dist/network-retry.js +140 -0
- package/dist/network-retry.js.map +1 -0
- package/dist/persistence.d.ts +104 -0
- package/dist/persistence.d.ts.map +1 -0
- package/dist/persistence.js +644 -0
- package/dist/persistence.js.map +1 -0
- package/dist/primitives/context.d.ts +29 -0
- package/dist/primitives/context.d.ts.map +1 -0
- package/dist/primitives/context.js +85 -0
- package/dist/primitives/context.js.map +1 -0
- package/dist/progress.d.ts +51 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/progress.js +229 -0
- package/dist/progress.js.map +1 -0
- package/dist/provider-presets.d.ts +34 -0
- package/dist/provider-presets.d.ts.map +1 -0
- package/dist/provider-presets.js +181 -0
- package/dist/provider-presets.js.map +1 -0
- package/dist/providers/anthropic.d.ts +32 -0
- package/dist/providers/anthropic.d.ts.map +1 -0
- package/dist/providers/anthropic.js +450 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/base.d.ts +135 -0
- package/dist/providers/base.d.ts.map +1 -0
- package/dist/providers/base.js +104 -0
- package/dist/providers/base.js.map +1 -0
- package/dist/providers/glm.d.ts +18 -0
- package/dist/providers/glm.d.ts.map +1 -0
- package/dist/providers/glm.js +59 -0
- package/dist/providers/glm.js.map +1 -0
- package/dist/providers/kimi.d.ts +23 -0
- package/dist/providers/kimi.d.ts.map +1 -0
- package/dist/providers/kimi.js +89 -0
- package/dist/providers/kimi.js.map +1 -0
- package/dist/providers/minimax.d.ts +20 -0
- package/dist/providers/minimax.d.ts.map +1 -0
- package/dist/providers/minimax.js +192 -0
- package/dist/providers/minimax.js.map +1 -0
- package/dist/providers/openai-chat.d.ts +33 -0
- package/dist/providers/openai-chat.d.ts.map +1 -0
- package/dist/providers/openai-chat.js +543 -0
- package/dist/providers/openai-chat.js.map +1 -0
- package/dist/providers/openai-responses.d.ts +26 -0
- package/dist/providers/openai-responses.d.ts.map +1 -0
- package/dist/providers/openai-responses.js +443 -0
- package/dist/providers/openai-responses.js.map +1 -0
- package/dist/providers/openrouter.d.ts +24 -0
- package/dist/providers/openrouter.d.ts.map +1 -0
- package/dist/providers/openrouter.js +177 -0
- package/dist/providers/openrouter.js.map +1 -0
- package/dist/providers/registry.d.ts +7 -0
- package/dist/providers/registry.d.ts.map +1 -0
- package/dist/providers/registry.js +38 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/security/path.d.ts +51 -0
- package/dist/security/path.d.ts.map +1 -0
- package/dist/security/path.js +187 -0
- package/dist/security/path.js.map +1 -0
- package/dist/security/sensitive-files.d.ts +3 -0
- package/dist/security/sensitive-files.d.ts.map +1 -0
- package/dist/security/sensitive-files.js +41 -0
- package/dist/security/sensitive-files.js.map +1 -0
- package/dist/session.d.ts +446 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +4595 -0
- package/dist/session.js.map +1 -0
- package/dist/settings.d.ts +46 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +134 -0
- package/dist/settings.js.map +1 -0
- package/dist/show-context.d.ts +35 -0
- package/dist/show-context.d.ts.map +1 -0
- package/dist/show-context.js +320 -0
- package/dist/show-context.js.map +1 -0
- package/dist/skills/loader.d.ts +49 -0
- package/dist/skills/loader.d.ts.map +1 -0
- package/dist/skills/loader.js +166 -0
- package/dist/skills/loader.js.map +1 -0
- package/dist/summarize-context.d.ts +29 -0
- package/dist/summarize-context.d.ts.map +1 -0
- package/dist/summarize-context.js +247 -0
- package/dist/summarize-context.js.map +1 -0
- package/dist/templates/loader.d.ts +104 -0
- package/dist/templates/loader.d.ts.map +1 -0
- package/dist/templates/loader.js +514 -0
- package/dist/templates/loader.js.map +1 -0
- package/dist/tools/basic.d.ts +29 -0
- package/dist/tools/basic.d.ts.map +1 -0
- package/dist/tools/basic.js +2079 -0
- package/dist/tools/basic.js.map +1 -0
- package/dist/tools/comm.d.ts +17 -0
- package/dist/tools/comm.d.ts.map +1 -0
- package/dist/tools/comm.js +192 -0
- package/dist/tools/comm.js.map +1 -0
- package/dist/tools/web-fetch.d.ts +11 -0
- package/dist/tools/web-fetch.d.ts.map +1 -0
- package/dist/tools/web-fetch.js +237 -0
- package/dist/tools/web-fetch.js.map +1 -0
- package/dist/tools/web-search.d.ts +24 -0
- package/dist/tools/web-search.d.ts.map +1 -0
- package/dist/tools/web-search.js +51 -0
- package/dist/tools/web-search.js.map +1 -0
- package/dist/tui/app.d.ts +35 -0
- package/dist/tui/app.d.ts.map +1 -0
- package/dist/tui/app.js +1042 -0
- package/dist/tui/app.js.map +1 -0
- package/dist/tui/checkbox-picker.d.ts +35 -0
- package/dist/tui/checkbox-picker.d.ts.map +1 -0
- package/dist/tui/checkbox-picker.js +85 -0
- package/dist/tui/checkbox-picker.js.map +1 -0
- package/dist/tui/command-picker.d.ts +31 -0
- package/dist/tui/command-picker.d.ts.map +1 -0
- package/dist/tui/command-picker.js +113 -0
- package/dist/tui/command-picker.js.map +1 -0
- package/dist/tui/components/ask-panel.d.ts +21 -0
- package/dist/tui/components/ask-panel.d.ts.map +1 -0
- package/dist/tui/components/ask-panel.js +81 -0
- package/dist/tui/components/ask-panel.js.map +1 -0
- package/dist/tui/components/conversation-panel.d.ts +68 -0
- package/dist/tui/components/conversation-panel.d.ts.map +1 -0
- package/dist/tui/components/conversation-panel.js +611 -0
- package/dist/tui/components/conversation-panel.js.map +1 -0
- package/dist/tui/components/input-panel.d.ts +27 -0
- package/dist/tui/components/input-panel.d.ts.map +1 -0
- package/dist/tui/components/input-panel.js +725 -0
- package/dist/tui/components/input-panel.js.map +1 -0
- package/dist/tui/components/logo-panel.d.ts +14 -0
- package/dist/tui/components/logo-panel.d.ts.map +1 -0
- package/dist/tui/components/logo-panel.js +37 -0
- package/dist/tui/components/logo-panel.js.map +1 -0
- package/dist/tui/components/plan-panel.d.ts +10 -0
- package/dist/tui/components/plan-panel.d.ts.map +1 -0
- package/dist/tui/components/plan-panel.js +8 -0
- package/dist/tui/components/plan-panel.js.map +1 -0
- package/dist/tui/components/status-bar.d.ts +24 -0
- package/dist/tui/components/status-bar.d.ts.map +1 -0
- package/dist/tui/components/status-bar.js +80 -0
- package/dist/tui/components/status-bar.js.map +1 -0
- package/dist/tui/input/editor-state.d.ts +22 -0
- package/dist/tui/input/editor-state.d.ts.map +1 -0
- package/dist/tui/input/editor-state.js +157 -0
- package/dist/tui/input/editor-state.js.map +1 -0
- package/dist/tui/input/keymap.d.ts +3 -0
- package/dist/tui/input/keymap.d.ts.map +1 -0
- package/dist/tui/input/keymap.js +72 -0
- package/dist/tui/input/keymap.js.map +1 -0
- package/dist/tui/input/paste-slots.d.ts +17 -0
- package/dist/tui/input/paste-slots.d.ts.map +1 -0
- package/dist/tui/input/paste-slots.js +46 -0
- package/dist/tui/input/paste-slots.js.map +1 -0
- package/dist/tui/input/paste.d.ts +15 -0
- package/dist/tui/input/paste.d.ts.map +1 -0
- package/dist/tui/input/paste.js +35 -0
- package/dist/tui/input/paste.js.map +1 -0
- package/dist/tui/input/protocol.d.ts +9 -0
- package/dist/tui/input/protocol.d.ts.map +1 -0
- package/dist/tui/input/protocol.js +387 -0
- package/dist/tui/input/protocol.js.map +1 -0
- package/dist/tui/input/sanitize.d.ts +6 -0
- package/dist/tui/input/sanitize.d.ts.map +1 -0
- package/dist/tui/input/sanitize.js +20 -0
- package/dist/tui/input/sanitize.js.map +1 -0
- package/dist/tui/input/types.d.ts +18 -0
- package/dist/tui/input/types.d.ts.map +1 -0
- package/dist/tui/input/types.js +2 -0
- package/dist/tui/input/types.js.map +1 -0
- package/dist/tui/launch.d.ts +23 -0
- package/dist/tui/launch.d.ts.map +1 -0
- package/dist/tui/launch.js +104 -0
- package/dist/tui/launch.js.map +1 -0
- package/dist/tui/theme.d.ts +20 -0
- package/dist/tui/theme.d.ts.map +1 -0
- package/dist/tui/theme.js +29 -0
- package/dist/tui/theme.js.map +1 -0
- package/dist/tui/types.d.ts +136 -0
- package/dist/tui/types.d.ts.map +1 -0
- package/dist/tui/types.js +9 -0
- package/dist/tui/types.js.map +1 -0
- package/package.json +76 -0
- package/prompts/sections/agents_md.md +23 -0
- package/prompts/sections/important_log.md +16 -0
- package/prompts/sections/system_mechanisms.md +18 -0
- package/prompts/tools/apply_patch.md +31 -0
- package/prompts/tools/ask.md +18 -0
- package/prompts/tools/bash.md +13 -0
- package/prompts/tools/bash_background.md +9 -0
- package/prompts/tools/bash_output.md +9 -0
- package/prompts/tools/check_status.md +3 -0
- package/prompts/tools/diff.md +5 -0
- package/prompts/tools/edit_file.md +11 -0
- package/prompts/tools/glob.md +7 -0
- package/prompts/tools/grep.md +20 -0
- package/prompts/tools/kill_agent.md +3 -0
- package/prompts/tools/kill_shell.md +5 -0
- package/prompts/tools/list_dir.md +5 -0
- package/prompts/tools/plan.md +252 -0
- package/prompts/tools/read_file.md +9 -0
- package/prompts/tools/show_context.md +12 -0
- package/prompts/tools/skill.md +7 -0
- package/prompts/tools/spawn_agent.md +195 -0
- package/prompts/tools/summarize_context.md +122 -0
- package/prompts/tools/test.md +5 -0
- package/prompts/tools/wait.md +17 -0
- package/prompts/tools/web_fetch.md +9 -0
- package/prompts/tools/web_search.md +5 -0
- package/prompts/tools/write_file.md +11 -0
- package/skills/.staging/.gitkeep +0 -0
- package/skills/explain-code/SKILL.md +15 -0
- package/skills/skill-manager/SKILL.md +83 -0
package/dist/session.js
ADDED
|
@@ -0,0 +1,4595 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-turn conversation session with context management.
|
|
3
|
+
*
|
|
4
|
+
* Provides the Session class — the core runtime orchestrator.
|
|
5
|
+
* Manages the Primary Agent's conversation, important log,
|
|
6
|
+
* auto-compact, and sub-agent lifecycle.
|
|
7
|
+
*/
|
|
8
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs";
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join, resolve } from "node:path";
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
import * as yaml from "js-yaml";
|
|
14
|
+
import { loadTemplate, validateTemplate } from "./templates/loader.js";
|
|
15
|
+
import { Agent, isNoReply, NO_REPLY_MARKER } from "./agents/agent.js";
|
|
16
|
+
import { createEphemeralLogState } from "./ephemeral-log.js";
|
|
17
|
+
import { allocateContextId, stripContextTags, ContextTagStripBuffer } from "./context-rendering.js";
|
|
18
|
+
import { generateShowContext } from "./show-context.js";
|
|
19
|
+
import { getThinkingLevels, getModelMaxOutputTokens } from "./config.js";
|
|
20
|
+
import { ToolResult } from "./providers/base.js";
|
|
21
|
+
import { SPAWN_AGENT_TOOL, KILL_AGENT_TOOL, CHECK_STATUS_TOOL, WAIT_TOOL, SHOW_CONTEXT_TOOL, SUMMARIZE_CONTEXT_TOOL, ASK_TOOL, PLAN_TOOL, } from "./tools/comm.js";
|
|
22
|
+
import { buildBashEnv, executeTool, } from "./tools/basic.js";
|
|
23
|
+
import { execSummarizeContextOnLog } from "./summarize-context.js";
|
|
24
|
+
import { resolveSkillContent, loadSkillsMulti } from "./skills/loader.js";
|
|
25
|
+
import { toolBuiltinWebSearchPassthrough } from "./tools/web-search.js";
|
|
26
|
+
import { processFileAttachments, hasFiles as fileAttachHasFiles, hasImages as fileAttachHasImages, parseReferences, } from "./file-attach.js";
|
|
27
|
+
import { SafePathError, safePath } from "./security/path.js";
|
|
28
|
+
import { AskPendingError, ASK_CUSTOM_OPTION_LABEL, ASK_DISCUSS_FURTHER_GUIDANCE, ASK_DISCUSS_OPTION_LABEL, isAskPendingError, toPendingAskUi, } from "./ask.js";
|
|
29
|
+
import { LogIdAllocator, createSystemPrompt, createTurnStart, createUserMessage as createUserMessageEntry, createAssistantText, createReasoning, createToolResult as createToolResultEntry, createNoReply, createCompactMarker, createCompactContext, createSummary, createSubAgentStart, createSubAgentToolCall, createSubAgentEnd, createStatus, createError as createErrorEntry, createTokenUpdate, createAskRequest, createAskResolution, } from "./log-entry.js";
|
|
30
|
+
import { projectToApiMessages } from "./log-projection.js";
|
|
31
|
+
import { archiveWindow, createGlobalTuiPreferences, createLogSessionMeta, } from "./persistence.js";
|
|
32
|
+
import { resolvePersistedModelSelection, } from "./model-selection.js";
|
|
33
|
+
import { DEFAULT_THRESHOLDS, computeHysteresisThresholds, } from "./settings.js";
|
|
34
|
+
// ------------------------------------------------------------------
|
|
35
|
+
// Constants
|
|
36
|
+
// ------------------------------------------------------------------
|
|
37
|
+
const MAX_ACTIVATIONS_PER_TURN = 30;
|
|
38
|
+
const SUB_AGENT_OUTPUT_LIMIT = 12_000;
|
|
39
|
+
const SUB_AGENT_TIMEOUT = 600_000; // milliseconds
|
|
40
|
+
const MAX_COMPACT_PHASE_ROUNDS = 10; // max activations during compact phase
|
|
41
|
+
// -- Compact Prompt: Output scenario --
|
|
42
|
+
const COMPACT_PROMPT_OUTPUT = `Distill this conversation into a continuation prompt — imagine you're writing a briefing for a fresh instance of yourself who must seamlessly pick up where we left off, with zero access to the original conversation.
|
|
43
|
+
|
|
44
|
+
**Before writing the continuation prompt**, update your important log with any key discoveries, decisions, or insights from this session that aren't already recorded there. The important log survives compaction and will be visible to the new instance — this is your last chance to persist valuable knowledge.
|
|
45
|
+
|
|
46
|
+
**What the new instance will already have:** your system prompt, the important log, AGENTS.md persistent memory, and the active plan file (if any) are automatically re-injected after compact. Do not duplicate their contents in the continuation prompt — focus on what they don't cover: current progress, session-specific context, and in-flight work state. If you've discovered stable, long-term knowledge during this session, consider persisting it to the project AGENTS.md before compaction.
|
|
47
|
+
|
|
48
|
+
Your summary should capture everything that matters and nothing that doesn't. Use whatever structure best fits the actual content — there is no fixed template. But as you write, pressure-test yourself against these questions:
|
|
49
|
+
|
|
50
|
+
- **What are we trying to do?** The user's intent, goals, and any constraints or preferences they've expressed — stated or implied.
|
|
51
|
+
- **What do we know now that we didn't at the start?** Key discoveries, failed approaches, edge cases encountered, decisions made and *why*. (Skip anything already in your important log.)
|
|
52
|
+
- **Where exactly are we?** What's done, what's in progress, what's next. Be specific enough that work won't be repeated or skipped. (Skip anything already in your plan file.)
|
|
53
|
+
- **What artifacts exist?** Files read, created, or modified — with enough context about each to be actionable (not just a path list).
|
|
54
|
+
- **What tone/style/working relationship has been established?** If the user has shown preferences for how they like to collaborate, note them.
|
|
55
|
+
- **What explicit rules has the user stated?** Direct instructions about how to work, what not to do, approval requirements, or behavioral constraints the user has explicitly communicated (e.g., "don't modify code until I approve", "always run tests before committing"). Preserve these verbatim — they are binding rules, not suggestions.
|
|
56
|
+
|
|
57
|
+
**Err on the side of preserving more, not less.** The continuation prompt is the sole bridge between this conversation and the next — anything omitted is permanently lost to the new instance. Include all information that could plausibly be useful for subsequent work: partial findings, open questions, code snippets you'll need to reference, relevant file paths with context. A longer, thorough continuation prompt that preserves useful context is far better than a terse one that forces the new instance to re-discover things.
|
|
58
|
+
|
|
59
|
+
Write in natural prose. Use structure where it aids clarity, not for its own sake.`;
|
|
60
|
+
// -- Compact Prompt: Tool Call scenario --
|
|
61
|
+
const COMPACT_PROMPT_TOOLCALL = `[SYSTEM: COMPACT REQUIRED] The conversation has exceeded the context limit. Do NOT continue the task. Instead, produce a **continuation prompt** — a briefing that will allow a fresh instance of you (with no access to this conversation) to seamlessly resume the work.
|
|
62
|
+
|
|
63
|
+
You just made a tool call and received its result above. That result is real and should be reflected in your summary, but do not act on it — your only job right now is to write the continuation prompt.
|
|
64
|
+
|
|
65
|
+
**Before writing the continuation prompt**, update your important log with any key discoveries, decisions, or insights from this session that aren't already recorded there. The important log survives compaction and will be visible to the new instance — this is your last chance to persist valuable knowledge.
|
|
66
|
+
|
|
67
|
+
**What the new instance will already have:** your system prompt, the important log, AGENTS.md persistent memory, and the active plan file (if any) are automatically re-injected after compact. Do not duplicate their contents in the continuation prompt — focus on what they don't cover: current progress, session-specific context, and in-flight work state. If you've discovered stable, long-term knowledge during this session, consider persisting it to the project AGENTS.md before compaction.
|
|
68
|
+
|
|
69
|
+
Write in natural prose. Use structure where it aids clarity, not for its own sake. As you write, pressure-test yourself against these questions:
|
|
70
|
+
|
|
71
|
+
- **What are we trying to do?** The user's intent, goals, constraints, and preferences — stated or implied.
|
|
72
|
+
- **What do we know now that we didn't at the start?** Key discoveries, failed approaches, edge cases encountered, decisions made and why. (Skip anything already in your important log.)
|
|
73
|
+
- **Where exactly did we stop?** Be precise: what was the last tool call, what did it return, and what was supposed to happen next? The new instance must be able to pick up mid-step without repeating or skipping anything.
|
|
74
|
+
- **What's done, what's in progress, what remains?** Give a clear picture of overall progress, not just the interrupted step. (Skip anything already in your plan file.)
|
|
75
|
+
- **What artifacts exist?** Files read, created, or modified — with enough context about each to be actionable.
|
|
76
|
+
- **What working style has the user shown?** Communication preferences, collaboration patterns, or explicit instructions about how they like to work.
|
|
77
|
+
- **What explicit rules has the user stated?** Direct instructions about how to work, what not to do, approval requirements, or behavioral constraints (e.g., "don't modify code until I approve", "always run tests before committing"). Preserve these verbatim — they are binding rules, not suggestions.
|
|
78
|
+
|
|
79
|
+
**Err on the side of preserving more, not less.** The continuation prompt is the sole bridge between this conversation and the next — anything omitted is permanently lost to the new instance. Include all information that could plausibly be useful for subsequent work: partial findings, open questions, code snippets you'll need to reference, relevant file paths with context. A longer, thorough continuation prompt that preserves useful context is far better than a terse one that forces the new instance to re-discover things.
|
|
80
|
+
|
|
81
|
+
End the summary with a clear, imperative statement of what the next instance should do first upon resuming.`;
|
|
82
|
+
// -- Compact Prompt: Sub-agent (output scenario) --
|
|
83
|
+
const SUB_AGENT_COMPACT_PROMPT_OUTPUT = `Your context is full. Write a continuation summary so a fresh instance of you can resume this task seamlessly.
|
|
84
|
+
|
|
85
|
+
Capture:
|
|
86
|
+
- **Task**: What you were asked to do and any constraints.
|
|
87
|
+
- **Progress**: What's done, what's in progress, what remains.
|
|
88
|
+
- **Key findings**: Discoveries, file paths, code references, decisions — anything the next instance needs to avoid re-doing work.
|
|
89
|
+
- **Next step**: What to do first upon resuming.
|
|
90
|
+
|
|
91
|
+
Be thorough — include all information that could be useful. The next instance has no access to this conversation.`;
|
|
92
|
+
// -- Compact Prompt: Sub-agent (tool call scenario) --
|
|
93
|
+
const SUB_AGENT_COMPACT_PROMPT_TOOLCALL = `[SYSTEM: COMPACT REQUIRED] Your context is full. Do NOT continue the task. Write a continuation summary instead.
|
|
94
|
+
|
|
95
|
+
You just made a tool call and received its result above. Reflect that result in your summary, but do not act on it further.
|
|
96
|
+
|
|
97
|
+
Capture:
|
|
98
|
+
- **Task**: What you were asked to do and any constraints.
|
|
99
|
+
- **Progress**: What's done, what's in progress, what remains.
|
|
100
|
+
- **Last action**: What tool call you just made, what it returned, and what you planned to do next.
|
|
101
|
+
- **Key findings**: Discoveries, file paths, code references, decisions — anything the next instance needs to avoid re-doing work.
|
|
102
|
+
- **Next step**: What to do first upon resuming.
|
|
103
|
+
|
|
104
|
+
Be thorough — include all information that could be useful. The next instance has no access to this conversation.`;
|
|
105
|
+
const MANUAL_SUMMARIZE_PROMPT = [
|
|
106
|
+
"Review the current active context and use `summarize_context` to compress older groups that are no longer needed in full.",
|
|
107
|
+
"Preserve the latest working context and anything you still need verbatim.",
|
|
108
|
+
"Do not continue the main task beyond this summarize request.",
|
|
109
|
+
"After summarizing, reply briefly with what you compressed and stop.",
|
|
110
|
+
].join(" ");
|
|
111
|
+
function appendManualInstruction(basePrompt, instruction, kind) {
|
|
112
|
+
const trimmed = instruction?.trim();
|
|
113
|
+
if (!trimmed)
|
|
114
|
+
return basePrompt;
|
|
115
|
+
return `${basePrompt}\n\nAdditional user instruction for this manual ${kind} request:\n${trimmed}`;
|
|
116
|
+
}
|
|
117
|
+
// -- Hint Prompt generators (two-tier) --
|
|
118
|
+
function HINT_LEVEL1_PROMPT(pct) {
|
|
119
|
+
return `[SYSTEM: Context usage has reached ${pct}. Consider reviewing your context to free up space. You can call \`show_context\` to see the current context distribution, then use \`summarize_context\` to compress older groups that are no longer needed in full. Prioritize: completed subtasks, large tool results you've already extracted key info from, and exploratory steps that led to a conclusion. After summarizing, continue your work normally.]`;
|
|
120
|
+
}
|
|
121
|
+
function HINT_LEVEL2_PROMPT(pct) {
|
|
122
|
+
return `[SYSTEM: Context usage has reached ${pct} — auto-compact will trigger soon. Strongly recommended: call \`show_context\` now to see context distribution, then immediately use \`summarize_context\` to compress older groups. Prioritize: completed subtasks, large tool results, and exploratory steps. After summarizing, continue your work.]`;
|
|
123
|
+
}
|
|
124
|
+
const SYSTEM_PREFIXES = [
|
|
125
|
+
"[IMPORTANT LOG]",
|
|
126
|
+
"[AUTO-COMPACT]",
|
|
127
|
+
"[Context After Auto-Compact]",
|
|
128
|
+
"[MASTER PLAN:",
|
|
129
|
+
"[PHASE PLAN:",
|
|
130
|
+
"[SUB-AGENT UPDATE]",
|
|
131
|
+
"[SESSION INTERRUPTED]",
|
|
132
|
+
"[SKILL:",
|
|
133
|
+
];
|
|
134
|
+
const COMM_TOOL_NAMES = new Set([
|
|
135
|
+
"spawn_agent", "kill_agent", "check_status", "wait", "show_context", "summarize_context", "ask", "skill", "reload_skills",
|
|
136
|
+
"bash_background", "bash_output", "kill_shell", "plan",
|
|
137
|
+
]);
|
|
138
|
+
// ------------------------------------------------------------------
|
|
139
|
+
// NoReplyStreamBuffer
|
|
140
|
+
// ------------------------------------------------------------------
|
|
141
|
+
class NoReplyStreamBuffer {
|
|
142
|
+
static MARKER = "<NO_REPLY>";
|
|
143
|
+
static MARKER_LEN = 10;
|
|
144
|
+
_downstream;
|
|
145
|
+
_buffer = "";
|
|
146
|
+
_phase = "detect";
|
|
147
|
+
detectedNoReply = false;
|
|
148
|
+
constructor(downstream) {
|
|
149
|
+
this._downstream = downstream;
|
|
150
|
+
}
|
|
151
|
+
feed(chunk) {
|
|
152
|
+
if (this._phase === "forwarding") {
|
|
153
|
+
this._downstream(chunk);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (this._phase === "suppressed") {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
this._buffer += chunk;
|
|
160
|
+
const stripped = this._buffer.trimStart();
|
|
161
|
+
if (stripped && !stripped.startsWith("<")) {
|
|
162
|
+
this._flushAndForward();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (stripped.length < NoReplyStreamBuffer.MARKER_LEN) {
|
|
166
|
+
if (stripped && !NoReplyStreamBuffer.MARKER.startsWith(stripped)) {
|
|
167
|
+
this._flushAndForward();
|
|
168
|
+
}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (stripped.startsWith(NoReplyStreamBuffer.MARKER)) {
|
|
172
|
+
this.detectedNoReply = true;
|
|
173
|
+
this._buffer = "";
|
|
174
|
+
this._phase = "suppressed";
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
this._flushAndForward();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
_flushAndForward() {
|
|
181
|
+
this._phase = "forwarding";
|
|
182
|
+
if (this._buffer) {
|
|
183
|
+
this._downstream(this._buffer);
|
|
184
|
+
this._buffer = "";
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// ------------------------------------------------------------------
|
|
189
|
+
// Session
|
|
190
|
+
// ------------------------------------------------------------------
|
|
191
|
+
export class Session {
|
|
192
|
+
primaryAgent;
|
|
193
|
+
config;
|
|
194
|
+
agentTemplates;
|
|
195
|
+
_promptsDirs;
|
|
196
|
+
_progress;
|
|
197
|
+
_mcpManager;
|
|
198
|
+
_mcpConnected = false;
|
|
199
|
+
_createdAt;
|
|
200
|
+
// Structured log (v2 architecture — dual-array transition)
|
|
201
|
+
_log = [];
|
|
202
|
+
_idAllocator = new LogIdAllocator();
|
|
203
|
+
_logListeners = new Set();
|
|
204
|
+
// Token tracking
|
|
205
|
+
_lastInputTokens = 0;
|
|
206
|
+
_lastTotalTokens = 0;
|
|
207
|
+
_lastCacheReadTokens = 0;
|
|
208
|
+
// Compact phase
|
|
209
|
+
_compactInProgress = false;
|
|
210
|
+
// Context thresholds (from settings.json, or defaults)
|
|
211
|
+
_thresholds = { ...DEFAULT_THRESHOLDS };
|
|
212
|
+
_hintResetNone = DEFAULT_THRESHOLDS.summarize_hint_level1 / 100 - 0.20;
|
|
213
|
+
_hintResetLevel1 = (DEFAULT_THRESHOLDS.summarize_hint_level1 + DEFAULT_THRESHOLDS.summarize_hint_level2) / 200;
|
|
214
|
+
// Global max_output_tokens override from settings.json
|
|
215
|
+
_settingsMaxOutputTokens;
|
|
216
|
+
// Hint compression (two-tier state machine)
|
|
217
|
+
_hintState = "none";
|
|
218
|
+
// show_context: number of remaining rounds where annotations are active
|
|
219
|
+
_showContextRoundsRemaining = 0;
|
|
220
|
+
_showContextAnnotations = null;
|
|
221
|
+
// Plan tracking
|
|
222
|
+
_activePlanFile = null;
|
|
223
|
+
_activePlanCheckpoints = [];
|
|
224
|
+
_activePlanChecked = [];
|
|
225
|
+
// Skills
|
|
226
|
+
_skills = new Map();
|
|
227
|
+
_skillRoots = [];
|
|
228
|
+
_disabledSkills = new Set();
|
|
229
|
+
// Artifacts / persistence
|
|
230
|
+
_store;
|
|
231
|
+
// Path variables
|
|
232
|
+
_projectRoot;
|
|
233
|
+
_sessionArtifactsOverride;
|
|
234
|
+
_systemData;
|
|
235
|
+
// Sub-agents
|
|
236
|
+
_activeAgents = new Map();
|
|
237
|
+
_subAgentCounter = 0;
|
|
238
|
+
_activeShells = new Map();
|
|
239
|
+
_shellCounter = 0;
|
|
240
|
+
// Thinking level + cache hit + accent
|
|
241
|
+
_persistedModelSelection = {};
|
|
242
|
+
_preferredThinkingLevel = "default";
|
|
243
|
+
_preferredCacheHitEnabled = true;
|
|
244
|
+
_preferredAccentColor;
|
|
245
|
+
_thinkingLevel = "default";
|
|
246
|
+
_cacheHitEnabled = true;
|
|
247
|
+
// Agent runtime state (for message delivery mode selection)
|
|
248
|
+
_agentState = "idle";
|
|
249
|
+
// Message queue (check_status pull model)
|
|
250
|
+
_messageQueue = [];
|
|
251
|
+
_currentTurnSignal = null;
|
|
252
|
+
_interruptSnapshot = null;
|
|
253
|
+
/** Callback for incremental persistence — called at save-worthy checkpoints. */
|
|
254
|
+
onSaveRequest;
|
|
255
|
+
// Counters
|
|
256
|
+
_turnCount = 0;
|
|
257
|
+
_compactCount = 0;
|
|
258
|
+
_usedContextIds = new Set();
|
|
259
|
+
// Tool executors
|
|
260
|
+
_toolExecutors;
|
|
261
|
+
// Ask state
|
|
262
|
+
_activeAsk = null;
|
|
263
|
+
_askHistory = [];
|
|
264
|
+
_pendingTurnState = null;
|
|
265
|
+
/** Allocate a unique random hex context ID. */
|
|
266
|
+
_allocateContextId() {
|
|
267
|
+
return allocateContextId(this._usedContextIds);
|
|
268
|
+
}
|
|
269
|
+
constructor(opts) {
|
|
270
|
+
this.primaryAgent = opts.primaryAgent;
|
|
271
|
+
this.config = opts.config;
|
|
272
|
+
this.agentTemplates = opts.agentTemplates ?? {};
|
|
273
|
+
this._skills = opts.skills ?? new Map();
|
|
274
|
+
this._skillRoots = opts.skillRoots ?? [];
|
|
275
|
+
this._progress = opts.progress;
|
|
276
|
+
this._mcpManager = opts.mcpManager;
|
|
277
|
+
this._promptsDirs = opts.promptsDirs;
|
|
278
|
+
// Apply user settings (thresholds + max_output_tokens)
|
|
279
|
+
if (opts.settings) {
|
|
280
|
+
this._applySettings(opts.settings);
|
|
281
|
+
}
|
|
282
|
+
// Attach store if provided (must be set before _initConversation)
|
|
283
|
+
if (opts.store) {
|
|
284
|
+
this._store = opts.store;
|
|
285
|
+
}
|
|
286
|
+
// Resolve path variables
|
|
287
|
+
const pathOverrides = opts.config.pathOverrides;
|
|
288
|
+
this._projectRoot = pathOverrides.projectRoot ?? process.cwd();
|
|
289
|
+
this._sessionArtifactsOverride = pathOverrides.sessionArtifacts ?? "";
|
|
290
|
+
this._systemData = pathOverrides.systemData ?? "";
|
|
291
|
+
this._createdAt = new Date().toISOString();
|
|
292
|
+
this._initConversation();
|
|
293
|
+
this._toolExecutors = this._buildToolExecutors();
|
|
294
|
+
this._ensureCommTools();
|
|
295
|
+
this._ensureSkillTool();
|
|
296
|
+
this._persistedModelSelection = this._buildPersistedModelSelection();
|
|
297
|
+
}
|
|
298
|
+
_buildPersistedModelSelection(overrides) {
|
|
299
|
+
return {
|
|
300
|
+
modelConfigName: this.currentModelConfigName || undefined,
|
|
301
|
+
modelProvider: this.primaryAgent.modelConfig.provider || undefined,
|
|
302
|
+
modelSelectionKey: this.primaryAgent.modelConfig.model || undefined,
|
|
303
|
+
modelId: this.primaryAgent.modelConfig.model || undefined,
|
|
304
|
+
...overrides,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
setPersistedModelSelection(selection) {
|
|
308
|
+
this._persistedModelSelection = this._buildPersistedModelSelection(selection);
|
|
309
|
+
}
|
|
310
|
+
// ==================================================================
|
|
311
|
+
// Initialisation helpers
|
|
312
|
+
// ==================================================================
|
|
313
|
+
_initConversation() {
|
|
314
|
+
this._createdAt = new Date().toISOString();
|
|
315
|
+
const systemPrompt = this._renderSystemPrompt(this.primaryAgent.systemPrompt);
|
|
316
|
+
this._log = [];
|
|
317
|
+
this._idAllocator = new LogIdAllocator();
|
|
318
|
+
this._appendEntry(createSystemPrompt(this._nextLogId("system_prompt"), systemPrompt), false);
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Apply resolved user settings (thresholds + max_output_tokens).
|
|
322
|
+
*/
|
|
323
|
+
_applySettings(s) {
|
|
324
|
+
this._thresholds = { ...s.thresholds };
|
|
325
|
+
const hysteresis = computeHysteresisThresholds(s.thresholds);
|
|
326
|
+
this._hintResetNone = hysteresis.hintResetNone / 100;
|
|
327
|
+
this._hintResetLevel1 = hysteresis.hintResetLevel1 / 100;
|
|
328
|
+
this._settingsMaxOutputTokens = s.maxOutputTokens;
|
|
329
|
+
// Apply to current primary agent's model config
|
|
330
|
+
this._applyMaxOutputTokensOverride(this.primaryAgent.modelConfig);
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Effective maxTokens for a given ModelConfig, taking settings override into account.
|
|
334
|
+
* Clamps to [4096, modelMaxOutputTokens].
|
|
335
|
+
*/
|
|
336
|
+
_effectiveMaxTokens(mc) {
|
|
337
|
+
if (this._settingsMaxOutputTokens === undefined)
|
|
338
|
+
return mc.maxTokens;
|
|
339
|
+
const modelMax = getModelMaxOutputTokens(mc.model);
|
|
340
|
+
return Math.max(4096, Math.min(this._settingsMaxOutputTokens, modelMax ?? mc.maxTokens));
|
|
341
|
+
}
|
|
342
|
+
// ==================================================================
|
|
343
|
+
// Message infrastructure
|
|
344
|
+
// ==================================================================
|
|
345
|
+
/**
|
|
346
|
+
* Append a LogEntry to the structured log.
|
|
347
|
+
* Auto-triggers save request and notifies log listeners.
|
|
348
|
+
*/
|
|
349
|
+
_appendEntry(entry, save = true) {
|
|
350
|
+
this._log.push(entry);
|
|
351
|
+
this._notifyLogListeners();
|
|
352
|
+
if (save)
|
|
353
|
+
this.onSaveRequest?.();
|
|
354
|
+
}
|
|
355
|
+
_touchLog() {
|
|
356
|
+
this._notifyLogListeners();
|
|
357
|
+
}
|
|
358
|
+
_notifyLogListeners() {
|
|
359
|
+
for (const listener of this._logListeners) {
|
|
360
|
+
listener();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
/** Allocate the next log entry ID for a given type. */
|
|
364
|
+
_nextLogId(type) {
|
|
365
|
+
return this._idAllocator.next(type);
|
|
366
|
+
}
|
|
367
|
+
/** Compute the next roundIndex for the current turn based on existing entries. */
|
|
368
|
+
_computeNextRoundIndex() {
|
|
369
|
+
let maxRound = -1;
|
|
370
|
+
for (let i = this._log.length - 1; i >= 0; i--) {
|
|
371
|
+
const e = this._log[i];
|
|
372
|
+
if (e.turnIndex !== this._turnCount)
|
|
373
|
+
break;
|
|
374
|
+
if (e.roundIndex !== undefined && e.roundIndex > maxRound) {
|
|
375
|
+
maxRound = e.roundIndex;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return maxRound + 1;
|
|
379
|
+
}
|
|
380
|
+
_findRoundContextId(turnIndex, roundIndex) {
|
|
381
|
+
for (let i = this._log.length - 1; i >= 0; i--) {
|
|
382
|
+
const entry = this._log[i];
|
|
383
|
+
if (entry.turnIndex < turnIndex)
|
|
384
|
+
break;
|
|
385
|
+
if (entry.discarded)
|
|
386
|
+
continue;
|
|
387
|
+
if (entry.turnIndex !== turnIndex)
|
|
388
|
+
continue;
|
|
389
|
+
if (entry.roundIndex !== roundIndex)
|
|
390
|
+
continue;
|
|
391
|
+
const contextId = entry.meta["contextId"];
|
|
392
|
+
if (typeof contextId === "string" && contextId.trim()) {
|
|
393
|
+
return contextId;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return undefined;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Find the most recent user-side contextId by scanning backward through the log.
|
|
400
|
+
* "User-side" means entries with apiRole "user" or "tool_result" that carry a contextId.
|
|
401
|
+
* Used for context ID inheritance: text-only final rounds inherit this ID.
|
|
402
|
+
*/
|
|
403
|
+
_findPrecedingUserSideContextId() {
|
|
404
|
+
for (let i = this._log.length - 1; i >= 0; i--) {
|
|
405
|
+
const entry = this._log[i];
|
|
406
|
+
if (entry.discarded || entry.summarized)
|
|
407
|
+
continue;
|
|
408
|
+
if (entry.apiRole === "user" || entry.apiRole === "tool_result") {
|
|
409
|
+
const ctxId = entry.meta["contextId"];
|
|
410
|
+
if (typeof ctxId === "string" && ctxId.trim()) {
|
|
411
|
+
return ctxId;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return undefined;
|
|
416
|
+
}
|
|
417
|
+
_roundHasToolCalls(turnIndex, roundIndex) {
|
|
418
|
+
for (let i = this._log.length - 1; i >= 0; i--) {
|
|
419
|
+
const entry = this._log[i];
|
|
420
|
+
if (entry.turnIndex < turnIndex)
|
|
421
|
+
break;
|
|
422
|
+
if (entry.discarded)
|
|
423
|
+
continue;
|
|
424
|
+
if (entry.turnIndex !== turnIndex)
|
|
425
|
+
continue;
|
|
426
|
+
if (entry.roundIndex !== roundIndex)
|
|
427
|
+
continue;
|
|
428
|
+
if (entry.type === "tool_call")
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
_resolveOutputRoundContextId(turnIndex, roundIndex) {
|
|
434
|
+
const roundContextId = this._findRoundContextId(turnIndex, roundIndex);
|
|
435
|
+
if (this._roundHasToolCalls(turnIndex, roundIndex)) {
|
|
436
|
+
return roundContextId ?? this._allocateContextId();
|
|
437
|
+
}
|
|
438
|
+
return this._findPrecedingUserSideContextId() ?? roundContextId ?? this._allocateContextId();
|
|
439
|
+
}
|
|
440
|
+
_retagRoundEntries(turnIndex, roundIndex, contextId) {
|
|
441
|
+
let changed = false;
|
|
442
|
+
for (let i = this._log.length - 1; i >= 0; i--) {
|
|
443
|
+
const entry = this._log[i];
|
|
444
|
+
if (entry.turnIndex < turnIndex)
|
|
445
|
+
break;
|
|
446
|
+
if (entry.discarded)
|
|
447
|
+
continue;
|
|
448
|
+
if (entry.turnIndex !== turnIndex)
|
|
449
|
+
continue;
|
|
450
|
+
if (entry.roundIndex !== roundIndex)
|
|
451
|
+
continue;
|
|
452
|
+
if (entry.type !== "assistant_text" &&
|
|
453
|
+
entry.type !== "reasoning" &&
|
|
454
|
+
entry.type !== "tool_call" &&
|
|
455
|
+
entry.type !== "tool_result" &&
|
|
456
|
+
entry.type !== "no_reply") {
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (entry.meta["contextId"] === contextId)
|
|
460
|
+
continue;
|
|
461
|
+
entry.meta["contextId"] = contextId;
|
|
462
|
+
changed = true;
|
|
463
|
+
}
|
|
464
|
+
if (changed)
|
|
465
|
+
this._touchLog();
|
|
466
|
+
}
|
|
467
|
+
_findToolCallContextId(toolCallId, roundIndex) {
|
|
468
|
+
for (let i = this._log.length - 1; i >= 0; i--) {
|
|
469
|
+
const entry = this._log[i];
|
|
470
|
+
if (entry.turnIndex < this._turnCount)
|
|
471
|
+
break;
|
|
472
|
+
if (entry.discarded)
|
|
473
|
+
continue;
|
|
474
|
+
if (entry.type !== "tool_call")
|
|
475
|
+
continue;
|
|
476
|
+
if (entry.turnIndex !== this._turnCount)
|
|
477
|
+
continue;
|
|
478
|
+
const meta = entry.meta;
|
|
479
|
+
if (String(meta["toolCallId"] ?? "") !== toolCallId)
|
|
480
|
+
continue;
|
|
481
|
+
const contextId = meta["contextId"];
|
|
482
|
+
if (typeof contextId === "string" && contextId.trim()) {
|
|
483
|
+
return contextId;
|
|
484
|
+
}
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
if (typeof roundIndex === "number") {
|
|
488
|
+
return this._findRoundContextId(this._turnCount, roundIndex);
|
|
489
|
+
}
|
|
490
|
+
return undefined;
|
|
491
|
+
}
|
|
492
|
+
// ------------------------------------------------------------------
|
|
493
|
+
// Unified message delivery (v2 architecture)
|
|
494
|
+
// ------------------------------------------------------------------
|
|
495
|
+
/**
|
|
496
|
+
* Unified message delivery entry point.
|
|
497
|
+
* Routes based on _agentState:
|
|
498
|
+
* idle → direct injection into _log
|
|
499
|
+
* working → queue (delivered via tool_result notification or activation boundary drain)
|
|
500
|
+
* waiting → queue + wake wait
|
|
501
|
+
*/
|
|
502
|
+
_deliverMessage(source, content) {
|
|
503
|
+
if (this._agentState === "idle") {
|
|
504
|
+
this._injectMessageDirect(source, content);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
// working / waiting → enqueue
|
|
508
|
+
this._messageQueue.push({ source, content, timestamp: Date.now() });
|
|
509
|
+
if (this._agentState === "waiting") {
|
|
510
|
+
this._wakeWait();
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Public wrapper for TUI to deliver messages (replaces enqueueUserMessage).
|
|
515
|
+
*/
|
|
516
|
+
deliverMessage(source, content) {
|
|
517
|
+
this._deliverMessage(source, content);
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Direct injection (idle-state safety net).
|
|
521
|
+
*/
|
|
522
|
+
_injectMessageDirect(source, content) {
|
|
523
|
+
const ctxId = this._allocateContextId();
|
|
524
|
+
const formatted = `[Message from ${source}]\n${content}`;
|
|
525
|
+
// v2 log (source of truth)
|
|
526
|
+
this._appendEntry(createUserMessageEntry(this._nextLogId("user_message"), this._turnCount, formatted, formatted, ctxId), false);
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Check whether the message queue has pending messages.
|
|
530
|
+
*/
|
|
531
|
+
_hasQueuedMessages() {
|
|
532
|
+
return this._messageQueue.length > 0;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Check whether any agent has finished/errored but not yet delivered.
|
|
536
|
+
*/
|
|
537
|
+
_hasUndeliveredAgentResults() {
|
|
538
|
+
for (const entry of this._activeAgents.values()) {
|
|
539
|
+
if ((entry.status === "finished" || entry.status === "error") && !entry.delivered) {
|
|
540
|
+
return true;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
_hasTrackedShells() {
|
|
546
|
+
return this._activeShells.size > 0;
|
|
547
|
+
}
|
|
548
|
+
_hasRunningShells() {
|
|
549
|
+
for (const entry of this._activeShells.values()) {
|
|
550
|
+
if (entry.status === "running")
|
|
551
|
+
return true;
|
|
552
|
+
}
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
_getShellsDir() {
|
|
556
|
+
const dir = join(this._resolveSessionArtifacts(), "shells");
|
|
557
|
+
mkdirSync(dir, { recursive: true });
|
|
558
|
+
return dir;
|
|
559
|
+
}
|
|
560
|
+
_normalizeShellId(id) {
|
|
561
|
+
const trimmed = id.trim();
|
|
562
|
+
if (!trimmed)
|
|
563
|
+
return null;
|
|
564
|
+
return /^[A-Za-z0-9._-]+$/.test(trimmed) ? trimmed : null;
|
|
565
|
+
}
|
|
566
|
+
_recordShellChunk(entry, chunk) {
|
|
567
|
+
if (!chunk)
|
|
568
|
+
return;
|
|
569
|
+
appendFileSync(entry.logPath, chunk, "utf-8");
|
|
570
|
+
const lines = chunk
|
|
571
|
+
.split("\n")
|
|
572
|
+
.map((line) => line.trim())
|
|
573
|
+
.filter(Boolean);
|
|
574
|
+
for (const line of lines) {
|
|
575
|
+
entry.recentOutput.push(line);
|
|
576
|
+
if (entry.recentOutput.length > 3)
|
|
577
|
+
entry.recentOutput.shift();
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
_buildShellReport() {
|
|
581
|
+
if (this._activeShells.size === 0) {
|
|
582
|
+
return "No shells tracked.";
|
|
583
|
+
}
|
|
584
|
+
const lines = [];
|
|
585
|
+
for (const [id, entry] of this._activeShells) {
|
|
586
|
+
const elapsedSec = ((performance.now() - entry.startTime) / 1000).toFixed(1);
|
|
587
|
+
let line = `- [${id}] ${entry.status} (${elapsedSec}s)`;
|
|
588
|
+
if (entry.status === "exited" || entry.status === "failed") {
|
|
589
|
+
line += ` | exit=${entry.exitCode ?? "?"}`;
|
|
590
|
+
}
|
|
591
|
+
else if (entry.status === "killed") {
|
|
592
|
+
line += ` | signal=${entry.signal ?? "TERM"}`;
|
|
593
|
+
}
|
|
594
|
+
line += ` | log: ${entry.logPath}`;
|
|
595
|
+
if (entry.recentOutput.length > 0) {
|
|
596
|
+
line += `\n recent: ${entry.recentOutput.join(" → ")}`;
|
|
597
|
+
}
|
|
598
|
+
lines.push(line);
|
|
599
|
+
}
|
|
600
|
+
return lines.join("\n");
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Build unified delivery content: drain queue + build agent report.
|
|
604
|
+
* Used by check_status, wait, and activation boundary injection.
|
|
605
|
+
*/
|
|
606
|
+
_buildDeliveryContent(opts) {
|
|
607
|
+
const drainQueue = opts?.drainQueue ?? true;
|
|
608
|
+
const queued = drainQueue ? this._messageQueue : [...this._messageQueue];
|
|
609
|
+
// 1. Drain queue, group by source
|
|
610
|
+
const bySource = {};
|
|
611
|
+
for (const msg of queued) {
|
|
612
|
+
if (!bySource[msg.source])
|
|
613
|
+
bySource[msg.source] = [];
|
|
614
|
+
bySource[msg.source].push(msg.content);
|
|
615
|
+
}
|
|
616
|
+
if (drainQueue) {
|
|
617
|
+
this._messageQueue = [];
|
|
618
|
+
}
|
|
619
|
+
// 2. Build three-section format
|
|
620
|
+
const sections = [];
|
|
621
|
+
sections.push("# User");
|
|
622
|
+
sections.push(bySource["user"]?.join("\n\n") ?? "No new message.");
|
|
623
|
+
sections.push("# System");
|
|
624
|
+
sections.push(bySource["system"]?.join("\n\n") ?? "No new message.");
|
|
625
|
+
// 3. Sub-Agent section: always use live report when agents exist
|
|
626
|
+
sections.push("# Sub-Agent");
|
|
627
|
+
if (this._activeAgents.size > 0) {
|
|
628
|
+
sections.push(this._buildAgentReport());
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
sections.push("No agents tracked.");
|
|
632
|
+
}
|
|
633
|
+
sections.push("# Shell");
|
|
634
|
+
sections.push(this._buildShellReport());
|
|
635
|
+
return sections.join("\n");
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Inject all pending messages at activation boundary.
|
|
639
|
+
* Drains queue + builds agent report → pushes as user message.
|
|
640
|
+
*/
|
|
641
|
+
_injectPendingMessages() {
|
|
642
|
+
const content = this._buildDeliveryContent();
|
|
643
|
+
const ctxId = this._allocateContextId();
|
|
644
|
+
const formatted = `[New Messages]\n\n${content}`;
|
|
645
|
+
// v2 log (source of truth)
|
|
646
|
+
this._appendEntry(createUserMessageEntry(this._nextLogId("user_message"), this._turnCount, formatted, formatted, ctxId), false);
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Build a notification summary line for pending messages (counts only, no content).
|
|
650
|
+
* Returns null if nothing pending.
|
|
651
|
+
*/
|
|
652
|
+
_buildNotificationSummary() {
|
|
653
|
+
const hasMsgs = this._messageQueue.length > 0;
|
|
654
|
+
const hasAgentResults = this._hasUndeliveredAgentResults();
|
|
655
|
+
if (!hasMsgs && !hasAgentResults)
|
|
656
|
+
return null;
|
|
657
|
+
const parts = [];
|
|
658
|
+
if (hasMsgs) {
|
|
659
|
+
const counts = {};
|
|
660
|
+
for (const msg of this._messageQueue) {
|
|
661
|
+
counts[msg.source] = (counts[msg.source] || 0) + 1;
|
|
662
|
+
}
|
|
663
|
+
for (const [src, n] of Object.entries(counts)) {
|
|
664
|
+
parts.push(`${n} new message${n > 1 ? "s" : ""} from ${src}`);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
if (hasAgentResults) {
|
|
668
|
+
let count = 0;
|
|
669
|
+
for (const entry of this._activeAgents.values()) {
|
|
670
|
+
if ((entry.status === "finished" || entry.status === "error") && !entry.delivered)
|
|
671
|
+
count++;
|
|
672
|
+
}
|
|
673
|
+
parts.push(`${count} agent result${count > 1 ? "s" : ""} ready`);
|
|
674
|
+
}
|
|
675
|
+
return `\n\n[Message Notification]\n${parts.join(", ")}. Use \`check_status\` to read.`;
|
|
676
|
+
}
|
|
677
|
+
// Wait wake-up signal
|
|
678
|
+
_waitResolver = null;
|
|
679
|
+
_wakeWait() {
|
|
680
|
+
if (this._waitResolver) {
|
|
681
|
+
this._waitResolver();
|
|
682
|
+
this._waitResolver = null;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
_makeAbortPromise(signal) {
|
|
686
|
+
if (!signal)
|
|
687
|
+
return null;
|
|
688
|
+
if (signal.aborted)
|
|
689
|
+
return Promise.resolve("aborted");
|
|
690
|
+
return new Promise((resolve) => {
|
|
691
|
+
signal.addEventListener("abort", () => resolve("aborted"), { once: true });
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Prepare and execute interruption cleanup for the current turn.
|
|
696
|
+
*
|
|
697
|
+
* This captures a non-destructive delivery snapshot first, then kills active
|
|
698
|
+
* workers and drops unconsumed runtime state.
|
|
699
|
+
*/
|
|
700
|
+
requestTurnInterrupt() {
|
|
701
|
+
if (this._compactInProgress) {
|
|
702
|
+
return { accepted: false, reason: "compact_in_progress" };
|
|
703
|
+
}
|
|
704
|
+
let hadActiveAgents = false;
|
|
705
|
+
for (const entry of this._activeAgents.values()) {
|
|
706
|
+
if (entry.status === "working") {
|
|
707
|
+
hadActiveAgents = true;
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
const hadActiveShells = this._hasRunningShells();
|
|
712
|
+
const hadUnconsumed = this._hasQueuedMessages() || this._hasUndeliveredAgentResults();
|
|
713
|
+
this._interruptSnapshot = {
|
|
714
|
+
turnIndex: this._turnCount,
|
|
715
|
+
hadActiveAgents,
|
|
716
|
+
hadActiveShells,
|
|
717
|
+
hadUnconsumed,
|
|
718
|
+
deliveryContent: hadActiveAgents || hadActiveShells || hadUnconsumed
|
|
719
|
+
? this._buildDeliveryContent({ drainQueue: false })
|
|
720
|
+
: "",
|
|
721
|
+
};
|
|
722
|
+
this._activeAsk = null;
|
|
723
|
+
this._pendingTurnState = null;
|
|
724
|
+
this._messageQueue = [];
|
|
725
|
+
this._wakeWait();
|
|
726
|
+
if (this._activeAgents.size > 0) {
|
|
727
|
+
this._forceKillAllAgents();
|
|
728
|
+
}
|
|
729
|
+
if (this._activeShells.size > 0) {
|
|
730
|
+
this._forceKillAllShells();
|
|
731
|
+
}
|
|
732
|
+
return { accepted: true };
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Backward-compatible alias.
|
|
736
|
+
*/
|
|
737
|
+
cancelCurrentTurn() {
|
|
738
|
+
this.requestTurnInterrupt();
|
|
739
|
+
}
|
|
740
|
+
_resetTransientState() {
|
|
741
|
+
this._lastInputTokens = 0;
|
|
742
|
+
this._lastTotalTokens = 0;
|
|
743
|
+
this._lastCacheReadTokens = 0;
|
|
744
|
+
this._compactInProgress = false;
|
|
745
|
+
this._hintState = "none";
|
|
746
|
+
this._agentState = "idle";
|
|
747
|
+
this._messageQueue = [];
|
|
748
|
+
this._waitResolver = null;
|
|
749
|
+
this._interruptSnapshot = null;
|
|
750
|
+
this._activeAsk = null;
|
|
751
|
+
this._askHistory = [];
|
|
752
|
+
this._pendingTurnState = null;
|
|
753
|
+
if (this._activeAgents.size > 0) {
|
|
754
|
+
this._forceKillAllAgents();
|
|
755
|
+
}
|
|
756
|
+
if (this._activeShells.size > 0) {
|
|
757
|
+
this._forceKillAllShells();
|
|
758
|
+
}
|
|
759
|
+
this._subAgentCounter = 0;
|
|
760
|
+
this._shellCounter = 0;
|
|
761
|
+
this._showContextRoundsRemaining = 0;
|
|
762
|
+
this._showContextAnnotations = null;
|
|
763
|
+
this._activePlanFile = null;
|
|
764
|
+
this._activePlanCheckpoints = [];
|
|
765
|
+
this._activePlanChecked = [];
|
|
766
|
+
}
|
|
767
|
+
// ------------------------------------------------------------------
|
|
768
|
+
// Log accessors (v2)
|
|
769
|
+
// ------------------------------------------------------------------
|
|
770
|
+
/** Read-only snapshot of the structured log. */
|
|
771
|
+
get log() {
|
|
772
|
+
return this._log;
|
|
773
|
+
}
|
|
774
|
+
/** Subscribe to log changes. Returns an unsubscribe function. */
|
|
775
|
+
subscribeLog(listener) {
|
|
776
|
+
this._logListeners.add(listener);
|
|
777
|
+
return () => { this._logListeners.delete(listener); };
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Restore session from a loaded log.
|
|
781
|
+
*/
|
|
782
|
+
restoreFromLog(meta, entries, idAllocator) {
|
|
783
|
+
const restoredSelection = resolvePersistedModelSelection(this, {
|
|
784
|
+
modelConfigName: meta.modelConfigName || undefined,
|
|
785
|
+
modelProvider: meta.modelProvider,
|
|
786
|
+
modelSelectionKey: meta.modelSelectionKey,
|
|
787
|
+
modelId: meta.modelId,
|
|
788
|
+
});
|
|
789
|
+
const restoredModelConfig = this.config.getModel(restoredSelection.selectedConfigName);
|
|
790
|
+
const restoredThinkingPreference = meta.thinkingLevel ?? "default";
|
|
791
|
+
const restoredCachePreference = meta.cacheHitEnabled ?? true;
|
|
792
|
+
this._resetTransientState();
|
|
793
|
+
this._applyMaxOutputTokensOverride(restoredModelConfig);
|
|
794
|
+
this.primaryAgent.replaceModelConfig(restoredModelConfig);
|
|
795
|
+
this._persistedModelSelection = this._buildPersistedModelSelection({
|
|
796
|
+
modelConfigName: restoredSelection.selectedConfigName,
|
|
797
|
+
modelProvider: restoredSelection.modelProvider,
|
|
798
|
+
modelSelectionKey: restoredSelection.modelSelectionKey,
|
|
799
|
+
modelId: restoredSelection.modelId,
|
|
800
|
+
});
|
|
801
|
+
// Core log state
|
|
802
|
+
this._log = entries;
|
|
803
|
+
this._idAllocator = idAllocator;
|
|
804
|
+
// Counters from meta
|
|
805
|
+
this._turnCount = meta.turnCount;
|
|
806
|
+
this._compactCount = meta.compactCount;
|
|
807
|
+
this._preferredThinkingLevel = restoredThinkingPreference;
|
|
808
|
+
this._preferredCacheHitEnabled = restoredCachePreference;
|
|
809
|
+
this._thinkingLevel = this._resolveThinkingLevelForModel(restoredModelConfig.model, restoredThinkingPreference);
|
|
810
|
+
this._cacheHitEnabled = restoredCachePreference;
|
|
811
|
+
this._createdAt = meta.createdAt || this._createdAt;
|
|
812
|
+
// Rebuild usedContextIds from entries
|
|
813
|
+
this._usedContextIds = new Set();
|
|
814
|
+
for (const e of entries) {
|
|
815
|
+
const ctxId = e.meta["contextId"];
|
|
816
|
+
if (ctxId)
|
|
817
|
+
this._usedContextIds.add(String(ctxId));
|
|
818
|
+
}
|
|
819
|
+
// Restore last token counts from log
|
|
820
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
821
|
+
if (entries[i].type === "token_update") {
|
|
822
|
+
this._lastInputTokens = entries[i].meta["inputTokens"] ?? 0;
|
|
823
|
+
this._lastTotalTokens = entries[i].meta["totalTokens"] ?? 0;
|
|
824
|
+
this._lastCacheReadTokens = entries[i].meta["cacheReadTokens"] ?? 0;
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
// Restore ask state from log: find unclosed ask_request
|
|
829
|
+
this._restoreAskStateFromLog(entries);
|
|
830
|
+
// Restore active plan from meta
|
|
831
|
+
if (meta.activePlanFile) {
|
|
832
|
+
try {
|
|
833
|
+
const content = readFileSync(meta.activePlanFile, "utf-8");
|
|
834
|
+
const { checkpoints, checked } = this._parsePlanCheckpoints(content);
|
|
835
|
+
if (checkpoints.length > 0) {
|
|
836
|
+
this._activePlanFile = meta.activePlanFile;
|
|
837
|
+
this._activePlanCheckpoints = checkpoints;
|
|
838
|
+
this._activePlanChecked = checked;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
catch {
|
|
842
|
+
// Plan file no longer exists — skip restoration
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
// Rebuild ask history from ask_resolution entries
|
|
846
|
+
this._askHistory = [];
|
|
847
|
+
for (const e of entries) {
|
|
848
|
+
if (e.type === "ask_resolution" && !e.discarded) {
|
|
849
|
+
const m = e.meta;
|
|
850
|
+
this._askHistory.push({
|
|
851
|
+
askId: String(m["askId"] ?? ""),
|
|
852
|
+
kind: m["askKind"] ?? "agent_question",
|
|
853
|
+
summary: "",
|
|
854
|
+
decidedAt: new Date(e.timestamp).toISOString(),
|
|
855
|
+
decision: "answered",
|
|
856
|
+
source: { agentId: this.primaryAgent.name },
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
this._notifyLogListeners();
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Get log data for persistence (v2).
|
|
864
|
+
* Returns meta + entries suitable for saveLog().
|
|
865
|
+
*/
|
|
866
|
+
getLogForPersistence() {
|
|
867
|
+
return {
|
|
868
|
+
meta: createLogSessionMeta({
|
|
869
|
+
createdAt: this._createdAt,
|
|
870
|
+
projectPath: this._projectRoot,
|
|
871
|
+
modelConfigName: this._persistedModelSelection.modelConfigName ?? "",
|
|
872
|
+
modelProvider: this._persistedModelSelection.modelProvider,
|
|
873
|
+
modelSelectionKey: this._persistedModelSelection.modelSelectionKey,
|
|
874
|
+
modelId: this._persistedModelSelection.modelId,
|
|
875
|
+
turnCount: this._turnCount,
|
|
876
|
+
compactCount: this._compactCount,
|
|
877
|
+
thinkingLevel: this._thinkingLevel,
|
|
878
|
+
cacheHitEnabled: this._cacheHitEnabled,
|
|
879
|
+
summary: this._generateSummary(),
|
|
880
|
+
activePlanFile: this._activePlanFile ?? undefined,
|
|
881
|
+
}),
|
|
882
|
+
entries: this._log,
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
setStore(store) {
|
|
886
|
+
this._store = store;
|
|
887
|
+
// Re-render system prompt in conversation to reflect correct paths
|
|
888
|
+
this._refreshSystemPromptPaths();
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Full reset for /new — equivalent to constructing a fresh Session.
|
|
892
|
+
* Leaves storage unbound; session/artifacts directories are created lazily
|
|
893
|
+
* on the first subsequent turn.
|
|
894
|
+
*/
|
|
895
|
+
resetForNewSession(newStore) {
|
|
896
|
+
// 1. Kill active sub-agents, reset transient flags
|
|
897
|
+
this._resetTransientState();
|
|
898
|
+
// 2. Update store FIRST (so path resolution picks up new session)
|
|
899
|
+
if (newStore !== undefined) {
|
|
900
|
+
this._store = newStore;
|
|
901
|
+
}
|
|
902
|
+
// 3. Reset counters
|
|
903
|
+
this._turnCount = 0;
|
|
904
|
+
this._compactCount = 0;
|
|
905
|
+
this._usedContextIds = new Set();
|
|
906
|
+
// 4. Reset thinking/cache state
|
|
907
|
+
this._thinkingLevel = this._resolveThinkingLevelForModel(this.primaryAgent.modelConfig.model, this._preferredThinkingLevel);
|
|
908
|
+
this._cacheHitEnabled = this._preferredCacheHitEnabled;
|
|
909
|
+
// 5. Reset MCP connection flag (will reconnect on next turn)
|
|
910
|
+
this._mcpConnected = false;
|
|
911
|
+
// 6. Re-init conversation LAST (fresh session state, storage may still be lazy)
|
|
912
|
+
// _initConversation also resets _log and _idAllocator
|
|
913
|
+
this._initConversation();
|
|
914
|
+
}
|
|
915
|
+
_buildToolExecutors() {
|
|
916
|
+
const scopedBuiltin = (toolName) => (args) => executeTool(toolName, args, {
|
|
917
|
+
projectRoot: this._projectRoot,
|
|
918
|
+
externalPathAllowlist: [this._resolveSessionArtifacts()],
|
|
919
|
+
sessionArtifactsDir: this._resolveSessionArtifacts(),
|
|
920
|
+
supportsMultimodal: this.primaryAgent.modelConfig.supportsMultimodal,
|
|
921
|
+
});
|
|
922
|
+
return {
|
|
923
|
+
read_file: scopedBuiltin("read_file"),
|
|
924
|
+
list_dir: scopedBuiltin("list_dir"),
|
|
925
|
+
glob: scopedBuiltin("glob"),
|
|
926
|
+
grep: scopedBuiltin("grep"),
|
|
927
|
+
edit_file: scopedBuiltin("edit_file"),
|
|
928
|
+
write_file: scopedBuiltin("write_file"),
|
|
929
|
+
apply_patch: scopedBuiltin("apply_patch"),
|
|
930
|
+
diff: scopedBuiltin("diff"),
|
|
931
|
+
web_fetch: (args) => executeTool("web_fetch", args),
|
|
932
|
+
bash: (args) => executeTool("bash", args, {
|
|
933
|
+
projectRoot: this._projectRoot,
|
|
934
|
+
externalPathAllowlist: [this._resolveSessionArtifacts()],
|
|
935
|
+
}),
|
|
936
|
+
test: (args) => executeTool("test", args, { projectRoot: this._projectRoot }),
|
|
937
|
+
bash_background: (args) => this._execBashBackground(args),
|
|
938
|
+
bash_output: (args) => this._execBashOutput(args),
|
|
939
|
+
kill_shell: (args) => this._execKillShell(args),
|
|
940
|
+
spawn_agent: (args) => this._execSpawnAgents(args),
|
|
941
|
+
kill_agent: (args) => this._execKillAgent(args),
|
|
942
|
+
check_status: (args) => this._execCheckStatus(args),
|
|
943
|
+
wait: (args) => this._execWait(args),
|
|
944
|
+
show_context: (args) => this._execShowContext(args),
|
|
945
|
+
summarize_context: (args) => this._execSummarizeContext(args),
|
|
946
|
+
ask: (args) => this._execAsk(args),
|
|
947
|
+
plan: (args) => this._execPlan(args),
|
|
948
|
+
skill: (args) => this._execSkill(args),
|
|
949
|
+
reload_skills: () => this._execReloadSkills(),
|
|
950
|
+
$web_search: (args) => toolBuiltinWebSearchPassthrough(args),
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
_ensureCommTools() {
|
|
954
|
+
const existing = new Set(this.primaryAgent.tools.map((t) => t.name));
|
|
955
|
+
for (const toolDef of [
|
|
956
|
+
SPAWN_AGENT_TOOL, KILL_AGENT_TOOL, CHECK_STATUS_TOOL, WAIT_TOOL,
|
|
957
|
+
SHOW_CONTEXT_TOOL, SUMMARIZE_CONTEXT_TOOL,
|
|
958
|
+
ASK_TOOL, PLAN_TOOL,
|
|
959
|
+
]) {
|
|
960
|
+
if (!existing.has(toolDef.name)) {
|
|
961
|
+
this.primaryAgent.tools.push(toolDef);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
// ==================================================================
|
|
966
|
+
// Skills
|
|
967
|
+
// ==================================================================
|
|
968
|
+
/** Read-only access to loaded skills (for command registration). */
|
|
969
|
+
get skills() {
|
|
970
|
+
return this._skills;
|
|
971
|
+
}
|
|
972
|
+
/** Read-only access to disabled skill names. */
|
|
973
|
+
get disabledSkills() {
|
|
974
|
+
return this._disabledSkills;
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Return all skills from disk (both enabled and disabled) for UI display.
|
|
978
|
+
*/
|
|
979
|
+
getAllSkillNames() {
|
|
980
|
+
const allOnDisk = loadSkillsMulti(this._skillRoots);
|
|
981
|
+
return [...allOnDisk.values()].map((s) => ({
|
|
982
|
+
name: s.name,
|
|
983
|
+
description: s.description,
|
|
984
|
+
enabled: !this._disabledSkills.has(s.name),
|
|
985
|
+
}));
|
|
986
|
+
}
|
|
987
|
+
/** Enable or disable a skill by name. Call reloadSkills() afterwards. */
|
|
988
|
+
setSkillEnabled(name, enabled) {
|
|
989
|
+
if (enabled) {
|
|
990
|
+
this._disabledSkills.delete(name);
|
|
991
|
+
}
|
|
992
|
+
else {
|
|
993
|
+
this._disabledSkills.add(name);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Rescan skill directories, apply disabled filter, and rebuild
|
|
998
|
+
* the skill tool definition + re-register slash commands.
|
|
999
|
+
*/
|
|
1000
|
+
reloadSkills() {
|
|
1001
|
+
const oldNames = new Set(this._skills.keys());
|
|
1002
|
+
const freshAll = loadSkillsMulti(this._skillRoots);
|
|
1003
|
+
// Apply disabled filter
|
|
1004
|
+
const filtered = new Map();
|
|
1005
|
+
for (const [name, skill] of freshAll) {
|
|
1006
|
+
if (!this._disabledSkills.has(name)) {
|
|
1007
|
+
filtered.set(name, skill);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
const newNames = new Set(filtered.keys());
|
|
1011
|
+
const added = [...newNames].filter((n) => !oldNames.has(n));
|
|
1012
|
+
const removed = [...oldNames].filter((n) => !newNames.has(n));
|
|
1013
|
+
this._skills = filtered;
|
|
1014
|
+
this._ensureSkillTool();
|
|
1015
|
+
return { added, removed, total: filtered.size };
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Build the `skill` tool definition dynamically from loaded skills.
|
|
1019
|
+
* Returns null if no skills are available for the agent.
|
|
1020
|
+
*/
|
|
1021
|
+
_buildSkillToolDef() {
|
|
1022
|
+
const available = [...this._skills.values()].filter((s) => !s.disableModelInvocation);
|
|
1023
|
+
if (available.length === 0)
|
|
1024
|
+
return null;
|
|
1025
|
+
const listing = available
|
|
1026
|
+
.map((s) => `- ${s.name}: ${s.description}`)
|
|
1027
|
+
.join("\n");
|
|
1028
|
+
return {
|
|
1029
|
+
name: "skill",
|
|
1030
|
+
description: "Invoke a skill by name. The skill's full instructions are returned for you to follow.\n\n" +
|
|
1031
|
+
"Available skills:\n" +
|
|
1032
|
+
listing,
|
|
1033
|
+
parameters: {
|
|
1034
|
+
type: "object",
|
|
1035
|
+
properties: {
|
|
1036
|
+
name: {
|
|
1037
|
+
type: "string",
|
|
1038
|
+
description: "The skill name to invoke.",
|
|
1039
|
+
},
|
|
1040
|
+
arguments: {
|
|
1041
|
+
type: "string",
|
|
1042
|
+
description: "Arguments to pass to the skill (e.g. file path, module name). " +
|
|
1043
|
+
"Referenced via $ARGUMENTS in the skill instructions.",
|
|
1044
|
+
},
|
|
1045
|
+
},
|
|
1046
|
+
required: ["name"],
|
|
1047
|
+
},
|
|
1048
|
+
summaryTemplate: "{agent} is invoking skill {name}",
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
_buildReloadSkillsToolDef() {
|
|
1052
|
+
return {
|
|
1053
|
+
name: "reload_skills",
|
|
1054
|
+
description: "Rescan skill directories, update the in-memory skills map, and rebuild the skill tool definition. " +
|
|
1055
|
+
"Use after installing, removing, or modifying skills on disk.",
|
|
1056
|
+
parameters: { type: "object", properties: {} },
|
|
1057
|
+
summaryTemplate: "{agent} is reloading skills",
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
/** Add the skill + reload_skills tools to the primary agent. */
|
|
1061
|
+
_ensureSkillTool() {
|
|
1062
|
+
// Remove old skill-related tools
|
|
1063
|
+
this.primaryAgent.tools = this.primaryAgent.tools.filter((t) => t.name !== "skill" && t.name !== "reload_skills");
|
|
1064
|
+
const skillDef = this._buildSkillToolDef();
|
|
1065
|
+
if (skillDef) {
|
|
1066
|
+
this.primaryAgent.tools.push(skillDef);
|
|
1067
|
+
}
|
|
1068
|
+
// Always add reload_skills if there are skill roots configured
|
|
1069
|
+
if (this._skillRoots.length > 0) {
|
|
1070
|
+
this.primaryAgent.tools.push(this._buildReloadSkillsToolDef());
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
/** Execute the `reload_skills` tool. */
|
|
1074
|
+
_execReloadSkills() {
|
|
1075
|
+
const result = this.reloadSkills();
|
|
1076
|
+
const lines = [`Skills reloaded. Total active: ${result.total}`];
|
|
1077
|
+
if (result.added.length)
|
|
1078
|
+
lines.push(`Added: ${result.added.join(", ")}`);
|
|
1079
|
+
if (result.removed.length)
|
|
1080
|
+
lines.push(`Removed: ${result.removed.join(", ")}`);
|
|
1081
|
+
const current = [...this._skills.keys()];
|
|
1082
|
+
lines.push(`\nCurrently available: ${current.join(", ") || "(none)"}`);
|
|
1083
|
+
return new ToolResult({ content: lines.join("\n") });
|
|
1084
|
+
}
|
|
1085
|
+
/** Execute the `skill` tool — load and return skill instructions. */
|
|
1086
|
+
_execSkill(args) {
|
|
1087
|
+
const name = (args["name"] ?? "").trim();
|
|
1088
|
+
if (!name) {
|
|
1089
|
+
return new ToolResult({ content: "Error: 'name' parameter is required." });
|
|
1090
|
+
}
|
|
1091
|
+
const skill = this._skills.get(name);
|
|
1092
|
+
if (!skill) {
|
|
1093
|
+
const available = [...this._skills.keys()].join(", ");
|
|
1094
|
+
return new ToolResult({
|
|
1095
|
+
content: `Error: Unknown skill "${name}". Available: ${available || "(none)"}`,
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
if (skill.disableModelInvocation) {
|
|
1099
|
+
return new ToolResult({
|
|
1100
|
+
content: `Error: Skill "${name}" can only be invoked by the user via /${name}.`,
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
const skillArgs = (args["arguments"] ?? "").trim();
|
|
1104
|
+
const content = resolveSkillContent(skill, skillArgs);
|
|
1105
|
+
return new ToolResult({
|
|
1106
|
+
content: `[SKILL: ${skill.name}]\n` +
|
|
1107
|
+
`Skill directory: ${skill.dir}\n\n` +
|
|
1108
|
+
content,
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
// ==================================================================
|
|
1112
|
+
// Thinking level + cache hit
|
|
1113
|
+
// ==================================================================
|
|
1114
|
+
get thinkingLevel() {
|
|
1115
|
+
return this._thinkingLevel;
|
|
1116
|
+
}
|
|
1117
|
+
set thinkingLevel(value) {
|
|
1118
|
+
this._preferredThinkingLevel = value;
|
|
1119
|
+
this._thinkingLevel = this._resolveThinkingLevelForModel(this.primaryAgent.modelConfig.model, value);
|
|
1120
|
+
}
|
|
1121
|
+
get cacheHitEnabled() {
|
|
1122
|
+
return this._cacheHitEnabled;
|
|
1123
|
+
}
|
|
1124
|
+
set cacheHitEnabled(value) {
|
|
1125
|
+
this._preferredCacheHitEnabled = value;
|
|
1126
|
+
this._cacheHitEnabled = value;
|
|
1127
|
+
}
|
|
1128
|
+
get accentColor() {
|
|
1129
|
+
return this._preferredAccentColor;
|
|
1130
|
+
}
|
|
1131
|
+
set accentColor(value) {
|
|
1132
|
+
this._preferredAccentColor = value;
|
|
1133
|
+
}
|
|
1134
|
+
/** The model name from the primary agent's config. */
|
|
1135
|
+
get currentModelName() {
|
|
1136
|
+
return this.primaryAgent.modelConfig.model;
|
|
1137
|
+
}
|
|
1138
|
+
/** The config name for the current model (e.g., "my-claude"). */
|
|
1139
|
+
get currentModelConfigName() {
|
|
1140
|
+
return this.primaryAgent.modelConfig.name;
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Switch the primary agent to a different model config.
|
|
1144
|
+
* Only callable between turns (not while a turn is in progress).
|
|
1145
|
+
*/
|
|
1146
|
+
switchModel(modelConfigName) {
|
|
1147
|
+
const newModelConfig = this.config.getModel(modelConfigName);
|
|
1148
|
+
this._applyMaxOutputTokensOverride(newModelConfig);
|
|
1149
|
+
this.primaryAgent.replaceModelConfig(newModelConfig);
|
|
1150
|
+
this._persistedModelSelection = this._buildPersistedModelSelection({
|
|
1151
|
+
modelConfigName,
|
|
1152
|
+
modelProvider: newModelConfig.provider,
|
|
1153
|
+
modelSelectionKey: newModelConfig.model,
|
|
1154
|
+
modelId: newModelConfig.model,
|
|
1155
|
+
});
|
|
1156
|
+
this._thinkingLevel = this._resolveThinkingLevelForModel(newModelConfig.model, this._preferredThinkingLevel);
|
|
1157
|
+
this._cacheHitEnabled = this._preferredCacheHitEnabled;
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* If settings.json specifies max_output_tokens, clamp the ModelConfig.maxTokens
|
|
1161
|
+
* to [4096, modelMaxOutputTokens]. This mutates the ModelConfig in place.
|
|
1162
|
+
*/
|
|
1163
|
+
_applyMaxOutputTokensOverride(mc) {
|
|
1164
|
+
if (this._settingsMaxOutputTokens === undefined)
|
|
1165
|
+
return;
|
|
1166
|
+
const modelMax = getModelMaxOutputTokens(mc.model) ?? mc.maxTokens;
|
|
1167
|
+
mc.maxTokens = Math.max(4096, Math.min(this._settingsMaxOutputTokens, modelMax));
|
|
1168
|
+
}
|
|
1169
|
+
applyGlobalPreferences(preferences) {
|
|
1170
|
+
const prefs = createGlobalTuiPreferences(preferences);
|
|
1171
|
+
this._preferredThinkingLevel = prefs.thinkingLevel;
|
|
1172
|
+
this._preferredCacheHitEnabled = prefs.cacheHitEnabled;
|
|
1173
|
+
this._preferredAccentColor = prefs.accentColor;
|
|
1174
|
+
this._thinkingLevel = this._resolveThinkingLevelForModel(this.primaryAgent.modelConfig.model, prefs.thinkingLevel);
|
|
1175
|
+
this._cacheHitEnabled = prefs.cacheHitEnabled;
|
|
1176
|
+
// Restore disabled skills
|
|
1177
|
+
if (prefs.disabledSkills && prefs.disabledSkills.length > 0) {
|
|
1178
|
+
this._disabledSkills = new Set(prefs.disabledSkills);
|
|
1179
|
+
this.reloadSkills();
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
getGlobalPreferences() {
|
|
1183
|
+
return createGlobalTuiPreferences({
|
|
1184
|
+
modelConfigName: this._persistedModelSelection.modelConfigName ?? undefined,
|
|
1185
|
+
modelProvider: this._persistedModelSelection.modelProvider ?? undefined,
|
|
1186
|
+
modelSelectionKey: this._persistedModelSelection.modelSelectionKey ?? undefined,
|
|
1187
|
+
modelId: this._persistedModelSelection.modelId ?? undefined,
|
|
1188
|
+
thinkingLevel: this._preferredThinkingLevel,
|
|
1189
|
+
cacheHitEnabled: this._preferredCacheHitEnabled,
|
|
1190
|
+
accentColor: this._preferredAccentColor,
|
|
1191
|
+
disabledSkills: this._disabledSkills.size > 0
|
|
1192
|
+
? [...this._disabledSkills]
|
|
1193
|
+
: undefined,
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
_resolveThinkingLevelForModel(modelName, preferredLevel) {
|
|
1197
|
+
if (!preferredLevel || preferredLevel === "default")
|
|
1198
|
+
return "default";
|
|
1199
|
+
const levels = getThinkingLevels(modelName);
|
|
1200
|
+
if (levels.length === 0)
|
|
1201
|
+
return "default";
|
|
1202
|
+
return levels.includes(preferredLevel) ? preferredLevel : "default";
|
|
1203
|
+
}
|
|
1204
|
+
/** Input tokens from the most recent provider response. */
|
|
1205
|
+
get lastInputTokens() {
|
|
1206
|
+
return this._lastInputTokens;
|
|
1207
|
+
}
|
|
1208
|
+
set lastInputTokens(value) {
|
|
1209
|
+
this._lastInputTokens = value;
|
|
1210
|
+
}
|
|
1211
|
+
/** Total tokens (input + output) from the most recent provider response. */
|
|
1212
|
+
get lastTotalTokens() {
|
|
1213
|
+
return this._lastTotalTokens;
|
|
1214
|
+
}
|
|
1215
|
+
set lastTotalTokens(value) {
|
|
1216
|
+
this._lastTotalTokens = value;
|
|
1217
|
+
}
|
|
1218
|
+
/** Cache-read tokens from the most recent provider response. */
|
|
1219
|
+
get lastCacheReadTokens() {
|
|
1220
|
+
return this._lastCacheReadTokens;
|
|
1221
|
+
}
|
|
1222
|
+
set lastCacheReadTokens(value) {
|
|
1223
|
+
this._lastCacheReadTokens = value;
|
|
1224
|
+
}
|
|
1225
|
+
appendStatusMessage(text, statusType = "status") {
|
|
1226
|
+
this._appendEntry(createStatus(this._nextLogId("status"), this._turnCount, text, statusType), true);
|
|
1227
|
+
}
|
|
1228
|
+
appendErrorMessage(text, errorType) {
|
|
1229
|
+
this._appendEntry(createErrorEntry(this._nextLogId("error"), this._turnCount, text, errorType), true);
|
|
1230
|
+
}
|
|
1231
|
+
_getManualContextCommandBlocker(command) {
|
|
1232
|
+
if (this._compactInProgress) {
|
|
1233
|
+
return `Cannot run ${command} while compact is in progress.`;
|
|
1234
|
+
}
|
|
1235
|
+
if (this._agentState !== "idle") {
|
|
1236
|
+
return `Cannot run ${command} while the current turn is still running.`;
|
|
1237
|
+
}
|
|
1238
|
+
if (this._activeAsk) {
|
|
1239
|
+
return `Cannot run ${command} while an ask is pending.`;
|
|
1240
|
+
}
|
|
1241
|
+
if (this._pendingTurnState) {
|
|
1242
|
+
return `Cannot run ${command} while a turn is waiting to resume.`;
|
|
1243
|
+
}
|
|
1244
|
+
if (this._hasActiveAgents()) {
|
|
1245
|
+
return `Cannot run ${command} while sub-agents are still running.`;
|
|
1246
|
+
}
|
|
1247
|
+
if (this._hasRunningShells()) {
|
|
1248
|
+
return `Cannot run ${command} while background shells are still running.`;
|
|
1249
|
+
}
|
|
1250
|
+
if (this._hasQueuedMessages()) {
|
|
1251
|
+
return `Cannot run ${command} while queued messages are waiting to be delivered.`;
|
|
1252
|
+
}
|
|
1253
|
+
if (this._hasUndeliveredAgentResults()) {
|
|
1254
|
+
return `Cannot run ${command} while sub-agent results are waiting to be delivered.`;
|
|
1255
|
+
}
|
|
1256
|
+
return null;
|
|
1257
|
+
}
|
|
1258
|
+
_armShowContextAnnotations() {
|
|
1259
|
+
const mc = this.primaryAgent.modelConfig;
|
|
1260
|
+
const provider = this.primaryAgent._provider;
|
|
1261
|
+
const effectiveMax = this._effectiveMaxTokens(mc);
|
|
1262
|
+
const budget = provider.budgetCalcMode === "full_context"
|
|
1263
|
+
? mc.contextLength
|
|
1264
|
+
: mc.contextLength - effectiveMax;
|
|
1265
|
+
const result = generateShowContext(this._log, this._lastInputTokens, budget);
|
|
1266
|
+
this._showContextRoundsRemaining = 1;
|
|
1267
|
+
this._showContextAnnotations = result.annotations;
|
|
1268
|
+
}
|
|
1269
|
+
async _runInjectedTurn(displayText, content, opts) {
|
|
1270
|
+
if (opts?.armShowContext) {
|
|
1271
|
+
this._armShowContextAnnotations();
|
|
1272
|
+
}
|
|
1273
|
+
const userCtxId = this._allocateContextId();
|
|
1274
|
+
this._turnCount += 1;
|
|
1275
|
+
this._appendEntry(createTurnStart(this._nextLogId("turn_start"), this._turnCount), false);
|
|
1276
|
+
this._appendEntry(createUserMessageEntry(this._nextLogId("user_message"), this._turnCount, displayText, content, userCtxId), false);
|
|
1277
|
+
this.onSaveRequest?.();
|
|
1278
|
+
const textAccumulator = { text: "" };
|
|
1279
|
+
const reasoningAccumulator = { text: "" };
|
|
1280
|
+
return this._runTurnActivationLoop(opts?.signal, textAccumulator, reasoningAccumulator);
|
|
1281
|
+
}
|
|
1282
|
+
async runManualSummarize(instruction, options) {
|
|
1283
|
+
this._ensureSessionStorageReady();
|
|
1284
|
+
await this._ensureMcp();
|
|
1285
|
+
const blocker = this._getManualContextCommandBlocker("/summarize");
|
|
1286
|
+
if (blocker)
|
|
1287
|
+
throw new Error(blocker);
|
|
1288
|
+
const prompt = appendManualInstruction(MANUAL_SUMMARIZE_PROMPT, instruction, "summarize");
|
|
1289
|
+
return this._runInjectedTurn("[Manual summarize request]", prompt, { signal: options?.signal, armShowContext: true });
|
|
1290
|
+
}
|
|
1291
|
+
async runManualCompact(instruction, options) {
|
|
1292
|
+
this._ensureSessionStorageReady();
|
|
1293
|
+
const blocker = this._getManualContextCommandBlocker("/compact");
|
|
1294
|
+
if (blocker)
|
|
1295
|
+
throw new Error(blocker);
|
|
1296
|
+
this._turnCount += 1;
|
|
1297
|
+
this._appendEntry(createTurnStart(this._nextLogId("turn_start"), this._turnCount), false);
|
|
1298
|
+
this._appendEntry(createStatus(this._nextLogId("status"), this._turnCount, "[Manual compact requested]", "manual_compact"), false);
|
|
1299
|
+
this.onSaveRequest?.();
|
|
1300
|
+
const prompt = appendManualInstruction(COMPACT_PROMPT_OUTPUT, instruction, "compact");
|
|
1301
|
+
const prevAgentState = this._agentState;
|
|
1302
|
+
const prevTurnSignal = this._currentTurnSignal;
|
|
1303
|
+
this._agentState = "working";
|
|
1304
|
+
this._currentTurnSignal = options?.signal ?? null;
|
|
1305
|
+
try {
|
|
1306
|
+
await this._doAutoCompact("output", options?.signal, prompt);
|
|
1307
|
+
this._hintState = "none";
|
|
1308
|
+
this.onSaveRequest?.();
|
|
1309
|
+
}
|
|
1310
|
+
finally {
|
|
1311
|
+
this._currentTurnSignal = prevTurnSignal;
|
|
1312
|
+
this._agentState = prevAgentState;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
// ==================================================================
|
|
1316
|
+
// Ask state
|
|
1317
|
+
// ==================================================================
|
|
1318
|
+
/**
|
|
1319
|
+
* Restore ask state from log entries.
|
|
1320
|
+
* Scans for unclosed ask_request (no matching ask_resolution).
|
|
1321
|
+
*/
|
|
1322
|
+
_restoreAskStateFromLog(entries) {
|
|
1323
|
+
// Build set of resolved ask IDs
|
|
1324
|
+
const resolvedAskIds = new Set();
|
|
1325
|
+
for (const e of entries) {
|
|
1326
|
+
if (e.type === "ask_resolution" && !e.discarded) {
|
|
1327
|
+
resolvedAskIds.add(String(e.meta["askId"] ?? ""));
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
// Find unclosed ask_request (has no matching ask_resolution)
|
|
1331
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
1332
|
+
const e = entries[i];
|
|
1333
|
+
if (e.type !== "ask_request" || e.discarded)
|
|
1334
|
+
continue;
|
|
1335
|
+
const askId = String(e.meta["askId"] ?? "");
|
|
1336
|
+
if (resolvedAskIds.has(askId))
|
|
1337
|
+
continue;
|
|
1338
|
+
// Found an unclosed ask — restore it as active
|
|
1339
|
+
const payload = e.content;
|
|
1340
|
+
const askKind = String(e.meta["askKind"] ?? "agent_question");
|
|
1341
|
+
if (askKind === "agent_question") {
|
|
1342
|
+
const meta = e.meta;
|
|
1343
|
+
this._activeAsk = {
|
|
1344
|
+
id: askId,
|
|
1345
|
+
kind: "agent_question",
|
|
1346
|
+
createdAt: new Date(e.timestamp).toISOString(),
|
|
1347
|
+
source: { agentId: this.primaryAgent.name, agentName: this.primaryAgent.name },
|
|
1348
|
+
roundIndex: typeof meta["roundIndex"] === "number" ? meta["roundIndex"] : undefined,
|
|
1349
|
+
summary: `Restored ask`,
|
|
1350
|
+
payload: payload,
|
|
1351
|
+
options: [],
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
break;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
getPendingAsk() {
|
|
1358
|
+
return toPendingAskUi(this._activeAsk);
|
|
1359
|
+
}
|
|
1360
|
+
hasPendingTurnToResume() {
|
|
1361
|
+
return this._pendingTurnState !== null;
|
|
1362
|
+
}
|
|
1363
|
+
resolveAsk(askId, _decision, _inputText) {
|
|
1364
|
+
const ask = this._activeAsk;
|
|
1365
|
+
if (!ask) {
|
|
1366
|
+
throw new Error("No active ask to resolve.");
|
|
1367
|
+
}
|
|
1368
|
+
if (ask.id !== askId) {
|
|
1369
|
+
throw new Error(`Ask id mismatch (active=${ask.id}, got=${askId}).`);
|
|
1370
|
+
}
|
|
1371
|
+
throw new Error("Use resolveAgentQuestionAsk() for agent_question asks.");
|
|
1372
|
+
}
|
|
1373
|
+
_emitAskRequestedProgress(ask) {
|
|
1374
|
+
if (!this._progress)
|
|
1375
|
+
return;
|
|
1376
|
+
this._progress.emit({
|
|
1377
|
+
step: this._turnCount,
|
|
1378
|
+
agent: ask.source.agentName || this.primaryAgent.name,
|
|
1379
|
+
action: "ask_requested",
|
|
1380
|
+
message: ` [ask] ${ask.summary}`,
|
|
1381
|
+
level: "normal",
|
|
1382
|
+
timestamp: Date.now() / 1000,
|
|
1383
|
+
usage: {},
|
|
1384
|
+
extra: { ask: toPendingAskUi(ask) },
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
_emitAskResolvedProgress(askId, decision, askKind) {
|
|
1388
|
+
if (!this._progress)
|
|
1389
|
+
return;
|
|
1390
|
+
this._progress.emit({
|
|
1391
|
+
step: this._turnCount,
|
|
1392
|
+
agent: this.primaryAgent.name,
|
|
1393
|
+
action: "ask_resolved",
|
|
1394
|
+
message: ` [ask] resolved: ${decision}`,
|
|
1395
|
+
level: "normal",
|
|
1396
|
+
timestamp: Date.now() / 1000,
|
|
1397
|
+
usage: {},
|
|
1398
|
+
extra: { askId, decision, askKind },
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
_beforeToolExecute = (_ctx) => {
|
|
1402
|
+
return;
|
|
1403
|
+
};
|
|
1404
|
+
// ==================================================================
|
|
1405
|
+
// Main turn loop
|
|
1406
|
+
// ==================================================================
|
|
1407
|
+
async resumePendingTurn(options) {
|
|
1408
|
+
if (this._activeAsk) {
|
|
1409
|
+
throw new Error("Cannot resume while an ask is still pending approval.");
|
|
1410
|
+
}
|
|
1411
|
+
const pending = this._pendingTurnState;
|
|
1412
|
+
if (!pending)
|
|
1413
|
+
return "";
|
|
1414
|
+
this._pendingTurnState = null;
|
|
1415
|
+
if (pending.stage === "pre_user_input") {
|
|
1416
|
+
return this.turn(pending.userInput ?? "", options);
|
|
1417
|
+
}
|
|
1418
|
+
const textAccumulator = { text: "" };
|
|
1419
|
+
const reasoningAccumulator = { text: "" };
|
|
1420
|
+
return this._runTurnActivationLoop(options?.signal, textAccumulator, reasoningAccumulator);
|
|
1421
|
+
}
|
|
1422
|
+
async _runTurnActivationLoop(signal, textAccumulator, reasoningAccumulator) {
|
|
1423
|
+
let finalText = "";
|
|
1424
|
+
const prevTurnSignal = this._currentTurnSignal;
|
|
1425
|
+
this._currentTurnSignal = signal ?? null;
|
|
1426
|
+
try {
|
|
1427
|
+
let reachedLimit = true;
|
|
1428
|
+
for (let activationIdx = 0; activationIdx < MAX_ACTIVATIONS_PER_TURN; activationIdx++) {
|
|
1429
|
+
if (signal?.aborted)
|
|
1430
|
+
break;
|
|
1431
|
+
const t0 = performance.now();
|
|
1432
|
+
const logLenBeforeActivation = this._log.length;
|
|
1433
|
+
textAccumulator.text = "";
|
|
1434
|
+
reasoningAccumulator.text = "";
|
|
1435
|
+
this._agentState = "working";
|
|
1436
|
+
if (this._progress) {
|
|
1437
|
+
this._progress.onAgentStart(this._turnCount, this.primaryAgent.name);
|
|
1438
|
+
}
|
|
1439
|
+
let result;
|
|
1440
|
+
try {
|
|
1441
|
+
result = await this._runActivation(signal, textAccumulator, reasoningAccumulator);
|
|
1442
|
+
}
|
|
1443
|
+
catch (err) {
|
|
1444
|
+
if (err?.name === "AbortError" || signal?.aborted) {
|
|
1445
|
+
this._handleInterruption(logLenBeforeActivation, textAccumulator.text, {
|
|
1446
|
+
activationCompleted: false,
|
|
1447
|
+
});
|
|
1448
|
+
this.onSaveRequest?.();
|
|
1449
|
+
finalText = textAccumulator.text.trim() || "";
|
|
1450
|
+
break;
|
|
1451
|
+
}
|
|
1452
|
+
throw err;
|
|
1453
|
+
}
|
|
1454
|
+
// Check abort AFTER successful completion — handles providers that
|
|
1455
|
+
// don't throw AbortError (stream finishes before abort takes effect).
|
|
1456
|
+
if (signal?.aborted) {
|
|
1457
|
+
this._handleInterruption(logLenBeforeActivation, textAccumulator.text, {
|
|
1458
|
+
activationCompleted: true,
|
|
1459
|
+
});
|
|
1460
|
+
this.onSaveRequest?.();
|
|
1461
|
+
finalText = textAccumulator.text.trim() || "";
|
|
1462
|
+
break;
|
|
1463
|
+
}
|
|
1464
|
+
this._lastInputTokens = result.lastInputTokens;
|
|
1465
|
+
this._lastTotalTokens = result.lastTotalTokens ?? 0;
|
|
1466
|
+
this._updateHintStateAfterApiCall();
|
|
1467
|
+
if (result.suspendedAsk) {
|
|
1468
|
+
const askContextId = this._findToolCallContextId(result.suspendedAsk.toolCallId, result.suspendedAsk.roundIndex);
|
|
1469
|
+
this._activeAsk = result.suspendedAsk.ask;
|
|
1470
|
+
this._emitAskRequestedProgress(this._activeAsk);
|
|
1471
|
+
this._appendEntry(createAskRequest(this._nextLogId("ask_request"), this._turnCount, this._activeAsk.payload, this._activeAsk.id, this._activeAsk.kind, result.suspendedAsk.toolCallId, result.suspendedAsk.roundIndex, askContextId), false);
|
|
1472
|
+
if (!result.compactNeeded) {
|
|
1473
|
+
this._checkAndInjectHint(result);
|
|
1474
|
+
}
|
|
1475
|
+
this.onSaveRequest?.();
|
|
1476
|
+
reachedLimit = false;
|
|
1477
|
+
break;
|
|
1478
|
+
}
|
|
1479
|
+
const elapsed = (performance.now() - t0) / 1000;
|
|
1480
|
+
let agentEndEmitted = false;
|
|
1481
|
+
const emitAgentEndOnce = () => {
|
|
1482
|
+
if (agentEndEmitted || !this._progress)
|
|
1483
|
+
return;
|
|
1484
|
+
this._progress.onAgentEnd(this._turnCount, this.primaryAgent.name, elapsed, result.totalUsage);
|
|
1485
|
+
agentEndEmitted = true;
|
|
1486
|
+
};
|
|
1487
|
+
const _trimmedText = result.text.trimEnd();
|
|
1488
|
+
const _hasNoReply = isNoReply(result.text) || _trimmedText.endsWith(NO_REPLY_MARKER);
|
|
1489
|
+
if (_hasNoReply) {
|
|
1490
|
+
const _precedingText = _trimmedText
|
|
1491
|
+
.slice(0, _trimmedText.length - NO_REPLY_MARKER.length)
|
|
1492
|
+
.trim();
|
|
1493
|
+
if (this._progress) {
|
|
1494
|
+
this._progress.onNoReplyClear(this.primaryAgent.name);
|
|
1495
|
+
}
|
|
1496
|
+
emitAgentEndOnce();
|
|
1497
|
+
if (this._progress) {
|
|
1498
|
+
this._progress.onAgentNoReply(this.primaryAgent.name);
|
|
1499
|
+
}
|
|
1500
|
+
if (!this._hasActiveAgents()) {
|
|
1501
|
+
// Silently ignore <NO_REPLY> when no sub-agents are running
|
|
1502
|
+
continue;
|
|
1503
|
+
}
|
|
1504
|
+
const noReplyContent = _precedingText || "<NO_REPLY>";
|
|
1505
|
+
const noReplyRound = result.reasoningHandledInLog
|
|
1506
|
+
? Math.max(0, this._computeNextRoundIndex() - 1)
|
|
1507
|
+
: this._computeNextRoundIndex();
|
|
1508
|
+
const noReplyContextId = this._resolveOutputRoundContextId(this._turnCount, noReplyRound);
|
|
1509
|
+
if (result.textHandledInLog || result.reasoningHandledInLog) {
|
|
1510
|
+
this._retagRoundEntries(this._turnCount, noReplyRound, noReplyContextId);
|
|
1511
|
+
}
|
|
1512
|
+
// v2 log: create no_reply entry (+ reasoning if present)
|
|
1513
|
+
{
|
|
1514
|
+
if (result.reasoningContent && !result.reasoningHandledInLog) {
|
|
1515
|
+
this._appendEntry(createReasoning(this._nextLogId("reasoning"), this._turnCount, noReplyRound, result.reasoningContent, result.reasoningContent, result.reasoningState, noReplyContextId), false);
|
|
1516
|
+
}
|
|
1517
|
+
this._appendEntry(createNoReply(this._nextLogId("no_reply"), this._turnCount, noReplyRound, noReplyContent, noReplyContextId), false);
|
|
1518
|
+
}
|
|
1519
|
+
this.onSaveRequest?.();
|
|
1520
|
+
await this._waitForAnyAgent(signal);
|
|
1521
|
+
if (signal?.aborted) {
|
|
1522
|
+
this._handleInterruption(logLenBeforeActivation, textAccumulator.text, {
|
|
1523
|
+
activationCompleted: true,
|
|
1524
|
+
});
|
|
1525
|
+
this.onSaveRequest?.();
|
|
1526
|
+
finalText = textAccumulator.text.trim() || "";
|
|
1527
|
+
break;
|
|
1528
|
+
}
|
|
1529
|
+
this.onSaveRequest?.();
|
|
1530
|
+
// Fall through to activation boundary drain (★) below
|
|
1531
|
+
}
|
|
1532
|
+
const shouldMaterializeFinalResponse = !result.compactNeeded || result.compactScenario === "output";
|
|
1533
|
+
if (result.text && shouldMaterializeFinalResponse) {
|
|
1534
|
+
finalText = result.text;
|
|
1535
|
+
// v2 log: create final assistant_text + optional reasoning entries
|
|
1536
|
+
{
|
|
1537
|
+
const finalRound = (result.textHandledInLog || result.reasoningHandledInLog)
|
|
1538
|
+
? Math.max(0, this._computeNextRoundIndex() - 1)
|
|
1539
|
+
: this._computeNextRoundIndex();
|
|
1540
|
+
const finalContextId = this._resolveOutputRoundContextId(this._turnCount, finalRound);
|
|
1541
|
+
if (result.textHandledInLog || result.reasoningHandledInLog) {
|
|
1542
|
+
this._retagRoundEntries(this._turnCount, finalRound, finalContextId);
|
|
1543
|
+
}
|
|
1544
|
+
if (result.reasoningContent && !result.reasoningHandledInLog) {
|
|
1545
|
+
this._appendEntry(createReasoning(this._nextLogId("reasoning"), this._turnCount, finalRound, result.reasoningContent, result.reasoningContent, result.reasoningState, finalContextId), false);
|
|
1546
|
+
}
|
|
1547
|
+
if (!result.textHandledInLog) {
|
|
1548
|
+
const displayText = stripContextTags(result.text);
|
|
1549
|
+
this._appendEntry(createAssistantText(this._nextLogId("assistant_text"), this._turnCount, finalRound, displayText, stripContextTags(result.text), finalContextId), false);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
emitAgentEndOnce();
|
|
1554
|
+
this.onSaveRequest?.();
|
|
1555
|
+
if (result.compactNeeded && result.compactScenario) {
|
|
1556
|
+
if (this._hasQueuedMessages() || this._hasUndeliveredAgentResults() || this._hasActiveAgents()) {
|
|
1557
|
+
this._injectPendingMessages();
|
|
1558
|
+
}
|
|
1559
|
+
const logLenBefore = this._log.length;
|
|
1560
|
+
try {
|
|
1561
|
+
await this._doAutoCompact(result.compactScenario, signal);
|
|
1562
|
+
}
|
|
1563
|
+
catch (compactErr) {
|
|
1564
|
+
if (compactErr?.name === "AbortError" || signal?.aborted) {
|
|
1565
|
+
// Mark compact-phase entries as discarded
|
|
1566
|
+
for (let ci = logLenBefore; ci < this._log.length; ci++) {
|
|
1567
|
+
this._log[ci].discarded = true;
|
|
1568
|
+
}
|
|
1569
|
+
this._appendEntry(createStatus(this._nextLogId("status"), this._turnCount, "[This turn was interrupted during context compaction.]", "compact_interrupted"), false);
|
|
1570
|
+
this.onSaveRequest?.();
|
|
1571
|
+
finalText = textAccumulator.text.trim() || "";
|
|
1572
|
+
break;
|
|
1573
|
+
}
|
|
1574
|
+
throw compactErr;
|
|
1575
|
+
}
|
|
1576
|
+
this.onSaveRequest?.();
|
|
1577
|
+
if (result.compactScenario === "output") {
|
|
1578
|
+
reachedLimit = false;
|
|
1579
|
+
break;
|
|
1580
|
+
}
|
|
1581
|
+
else {
|
|
1582
|
+
// Reset activation budget after compact — the agent gets a fresh
|
|
1583
|
+
// context and should not be penalised for pre-compact activations.
|
|
1584
|
+
activationIdx = -1; // for-loop increment will set it to 0
|
|
1585
|
+
continue;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
if (!result.compactNeeded) {
|
|
1589
|
+
this._checkAndInjectHint(result);
|
|
1590
|
+
}
|
|
1591
|
+
// Wait for active agents (if any and no queued messages yet)
|
|
1592
|
+
if (this._hasActiveAgents() && !this._hasQueuedMessages() && !this._hasUndeliveredAgentResults()) {
|
|
1593
|
+
await this._waitForAnyAgent(signal);
|
|
1594
|
+
if (signal?.aborted) {
|
|
1595
|
+
this._handleInterruption(logLenBeforeActivation, textAccumulator.text, {
|
|
1596
|
+
activationCompleted: true,
|
|
1597
|
+
});
|
|
1598
|
+
this.onSaveRequest?.();
|
|
1599
|
+
finalText = textAccumulator.text.trim() || "";
|
|
1600
|
+
break;
|
|
1601
|
+
}
|
|
1602
|
+
this.onSaveRequest?.();
|
|
1603
|
+
}
|
|
1604
|
+
// ★ ACTIVATION BOUNDARY DRAIN — unified exit point ★
|
|
1605
|
+
if (this._hasQueuedMessages() || this._hasUndeliveredAgentResults()) {
|
|
1606
|
+
this._injectPendingMessages();
|
|
1607
|
+
continue; // new activation to process injected messages
|
|
1608
|
+
}
|
|
1609
|
+
// Still have active agents but nothing pending yet — wait more
|
|
1610
|
+
if (this._hasActiveAgents()) {
|
|
1611
|
+
await this._waitForAnyAgent(signal);
|
|
1612
|
+
if (signal?.aborted) {
|
|
1613
|
+
this._handleInterruption(logLenBeforeActivation, textAccumulator.text, {
|
|
1614
|
+
activationCompleted: true,
|
|
1615
|
+
});
|
|
1616
|
+
this.onSaveRequest?.();
|
|
1617
|
+
finalText = textAccumulator.text.trim() || "";
|
|
1618
|
+
break;
|
|
1619
|
+
}
|
|
1620
|
+
this.onSaveRequest?.();
|
|
1621
|
+
continue; // loop back to drain check
|
|
1622
|
+
}
|
|
1623
|
+
// Nothing pending, no active agents → turn ends
|
|
1624
|
+
reachedLimit = false;
|
|
1625
|
+
this._agentState = "idle";
|
|
1626
|
+
break;
|
|
1627
|
+
}
|
|
1628
|
+
if (reachedLimit && !signal?.aborted) {
|
|
1629
|
+
console.warn(`Turn reached activation limit (${MAX_ACTIVATIONS_PER_TURN})`);
|
|
1630
|
+
if (!finalText) {
|
|
1631
|
+
finalText =
|
|
1632
|
+
"[Turn terminated: reached maximum activation limit " +
|
|
1633
|
+
"without producing output. This may indicate a stuck loop.]";
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
finally {
|
|
1638
|
+
this._currentTurnSignal = prevTurnSignal;
|
|
1639
|
+
this._agentState = "idle";
|
|
1640
|
+
if (!this._activeAsk && this._hasActiveAgents()) {
|
|
1641
|
+
this._forceKillAllAgents();
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
return finalText;
|
|
1645
|
+
}
|
|
1646
|
+
async turn(userInput, options) {
|
|
1647
|
+
this._ensureSessionStorageReady();
|
|
1648
|
+
await this._ensureMcp();
|
|
1649
|
+
const signal = options?.signal;
|
|
1650
|
+
if (this._pendingTurnState && !this._activeAsk) {
|
|
1651
|
+
return this.resumePendingTurn(options);
|
|
1652
|
+
}
|
|
1653
|
+
let userContent;
|
|
1654
|
+
try {
|
|
1655
|
+
userContent = await this._processFileAttachments(userInput);
|
|
1656
|
+
}
|
|
1657
|
+
catch (err) {
|
|
1658
|
+
if (isAskPendingError(err)) {
|
|
1659
|
+
this._pendingTurnState = { stage: "pre_user_input", userInput };
|
|
1660
|
+
this.onSaveRequest?.();
|
|
1661
|
+
return "";
|
|
1662
|
+
}
|
|
1663
|
+
throw err;
|
|
1664
|
+
}
|
|
1665
|
+
// Assign context_id to user message (metadata only, no visible §{id}§ tag in content)
|
|
1666
|
+
const userCtxId = this._allocateContextId();
|
|
1667
|
+
this._turnCount += 1;
|
|
1668
|
+
// v2 log: turn_start + user_message
|
|
1669
|
+
this._appendEntry(createTurnStart(this._nextLogId("turn_start"), this._turnCount), false);
|
|
1670
|
+
const displayText = typeof userContent === "string"
|
|
1671
|
+
? userContent
|
|
1672
|
+
: "[multimodal input]";
|
|
1673
|
+
// For the log entry, replace inline base64 images with image_ref file paths
|
|
1674
|
+
const logContent = this._extractAndSaveImages(userContent);
|
|
1675
|
+
this._appendEntry(createUserMessageEntry(this._nextLogId("user_message"), this._turnCount, displayText, logContent, userCtxId), false);
|
|
1676
|
+
this.onSaveRequest?.();
|
|
1677
|
+
// Track streamed content for abort recovery
|
|
1678
|
+
const textAccumulator = { text: "" };
|
|
1679
|
+
const reasoningAccumulator = { text: "" };
|
|
1680
|
+
return this._runTurnActivationLoop(signal, textAccumulator, reasoningAccumulator);
|
|
1681
|
+
}
|
|
1682
|
+
/**
|
|
1683
|
+
* Handle interruption using structured log (v2).
|
|
1684
|
+
*
|
|
1685
|
+
* Rules:
|
|
1686
|
+
* - Keep completed reasoning, drop incomplete reasoning of the currently interrupted round
|
|
1687
|
+
* - Keep partial text and append " [Interrupted here.]" when interruption happens mid-activation
|
|
1688
|
+
* - For each complete tool_call lacking result, append interrupted tool_result
|
|
1689
|
+
* - Append synthetic interruption user message (with optional snapshot)
|
|
1690
|
+
*/
|
|
1691
|
+
_handleInterruption(logLenBefore, accumulatedText, opts) {
|
|
1692
|
+
const activationCompleted = opts?.activationCompleted ?? false;
|
|
1693
|
+
const interruptedSuffix = " [Interrupted here.]";
|
|
1694
|
+
const interruptedMarker = "[Interrupted here.]";
|
|
1695
|
+
// Clear ask runtime state for interrupted turn.
|
|
1696
|
+
this._activeAsk = null;
|
|
1697
|
+
this._pendingTurnState = null;
|
|
1698
|
+
let latestRound;
|
|
1699
|
+
let latestRoundHasToolCall = false;
|
|
1700
|
+
let hasAssistantInActivation = false;
|
|
1701
|
+
let latestAssistantEntry = null;
|
|
1702
|
+
for (let i = logLenBefore; i < this._log.length; i++) {
|
|
1703
|
+
const e = this._log[i];
|
|
1704
|
+
if (e.discarded)
|
|
1705
|
+
continue;
|
|
1706
|
+
if (e.roundIndex !== undefined && (latestRound === undefined || e.roundIndex > latestRound)) {
|
|
1707
|
+
latestRound = e.roundIndex;
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
if (latestRound !== undefined) {
|
|
1711
|
+
for (let i = logLenBefore; i < this._log.length; i++) {
|
|
1712
|
+
const e = this._log[i];
|
|
1713
|
+
if (e.discarded || e.roundIndex !== latestRound)
|
|
1714
|
+
continue;
|
|
1715
|
+
if (e.type === "tool_call")
|
|
1716
|
+
latestRoundHasToolCall = true;
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
// Drop incomplete reasoning in the interrupted in-flight round only.
|
|
1720
|
+
if (!activationCompleted && latestRound !== undefined && !latestRoundHasToolCall) {
|
|
1721
|
+
for (let i = logLenBefore; i < this._log.length; i++) {
|
|
1722
|
+
const e = this._log[i];
|
|
1723
|
+
if (e.discarded)
|
|
1724
|
+
continue;
|
|
1725
|
+
if (e.roundIndex !== latestRound)
|
|
1726
|
+
continue;
|
|
1727
|
+
if (e.type === "reasoning") {
|
|
1728
|
+
e.discarded = true;
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
for (let i = logLenBefore; i < this._log.length; i++) {
|
|
1733
|
+
const e = this._log[i];
|
|
1734
|
+
if (e.type === "assistant_text" && !e.discarded) {
|
|
1735
|
+
hasAssistantInActivation = true;
|
|
1736
|
+
latestAssistantEntry = e;
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
// Mid-activation interruption keeps partial text and marks it explicitly.
|
|
1740
|
+
if (!activationCompleted) {
|
|
1741
|
+
if (latestAssistantEntry) {
|
|
1742
|
+
const currentDisplay = String(latestAssistantEntry.display ?? "");
|
|
1743
|
+
const currentContent = String(latestAssistantEntry.content ?? "");
|
|
1744
|
+
if (!currentDisplay.trimEnd().endsWith(interruptedSuffix)) {
|
|
1745
|
+
latestAssistantEntry.display = `${currentDisplay.trimEnd()}${interruptedSuffix}`;
|
|
1746
|
+
}
|
|
1747
|
+
if (!currentContent.trimEnd().endsWith(interruptedSuffix)) {
|
|
1748
|
+
latestAssistantEntry.content = `${currentContent.trimEnd()}${interruptedSuffix}`;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
else {
|
|
1752
|
+
const partialText = stripContextTags(accumulatedText).trim();
|
|
1753
|
+
if (partialText) {
|
|
1754
|
+
const partialContextId = this._findPrecedingUserSideContextId() ?? this._allocateContextId();
|
|
1755
|
+
this._appendEntry(createAssistantText(this._nextLogId("assistant_text"), this._turnCount, this._computeNextRoundIndex(), `${partialText}${interruptedSuffix}`, `${partialText}${interruptedSuffix}`, partialContextId), false);
|
|
1756
|
+
hasAssistantInActivation = true;
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
// Complete all materialized tool calls that have no results yet.
|
|
1761
|
+
this._completeMissingToolResultsFromLog(logLenBefore, interruptedMarker);
|
|
1762
|
+
// If protocol-side currently ends at user-side, add a synthetic assistant marker.
|
|
1763
|
+
const lastRole = this._getLastSendableRole();
|
|
1764
|
+
if (this._isUserSideProtocolRole(lastRole) && !hasAssistantInActivation) {
|
|
1765
|
+
const ctxId = this._findPrecedingUserSideContextId() ?? this._allocateContextId();
|
|
1766
|
+
this._appendEntry(createAssistantText(this._nextLogId("assistant_text"), this._turnCount, this._computeNextRoundIndex(), interruptedMarker, interruptedMarker, ctxId), false);
|
|
1767
|
+
}
|
|
1768
|
+
const snapshot = this._interruptSnapshot && this._interruptSnapshot.turnIndex === this._turnCount
|
|
1769
|
+
? this._interruptSnapshot
|
|
1770
|
+
: null;
|
|
1771
|
+
this._interruptSnapshot = null;
|
|
1772
|
+
const lines = ["Last turn was interrupted by the user."];
|
|
1773
|
+
if (snapshot && (snapshot.hadActiveAgents || snapshot.hadActiveShells || snapshot.hadUnconsumed)) {
|
|
1774
|
+
const killedKinds = [];
|
|
1775
|
+
if (snapshot.hadActiveAgents)
|
|
1776
|
+
killedKinds.push("sub-agents");
|
|
1777
|
+
if (snapshot.hadActiveShells)
|
|
1778
|
+
killedKinds.push("shells");
|
|
1779
|
+
if (killedKinds.length > 0) {
|
|
1780
|
+
lines.push(`Active ${killedKinds.join(" and ")} were killed.`);
|
|
1781
|
+
}
|
|
1782
|
+
if (snapshot.hadUnconsumed) {
|
|
1783
|
+
lines.push("Unconsumed queued information was discarded.");
|
|
1784
|
+
}
|
|
1785
|
+
if (snapshot.deliveryContent.trim()) {
|
|
1786
|
+
lines.push("");
|
|
1787
|
+
lines.push("[Snapshot]");
|
|
1788
|
+
lines.push(snapshot.deliveryContent);
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
const interruptionMessage = lines.join("\n");
|
|
1792
|
+
const interruptionCtxId = this._allocateContextId();
|
|
1793
|
+
const interruptionEntry = createUserMessageEntry(this._nextLogId("user_message"), this._turnCount, interruptionMessage, interruptionMessage, interruptionCtxId);
|
|
1794
|
+
// Keep interruption recovery context for the provider, but don't surface
|
|
1795
|
+
// this synthetic message in the conversation UI.
|
|
1796
|
+
interruptionEntry.tuiVisible = false;
|
|
1797
|
+
interruptionEntry.displayKind = null;
|
|
1798
|
+
this._appendEntry(interruptionEntry, false);
|
|
1799
|
+
}
|
|
1800
|
+
/**
|
|
1801
|
+
* Scan log entries from `fromIdx` onwards: for each tool_call entry,
|
|
1802
|
+
* check if a tool_result exists for it. Create missing tool_results.
|
|
1803
|
+
*/
|
|
1804
|
+
_completeMissingToolResultsFromLog(fromIdx, interruptedContent) {
|
|
1805
|
+
const pendingToolCalls = [];
|
|
1806
|
+
const resolvedToolCallIds = new Set();
|
|
1807
|
+
for (let i = fromIdx; i < this._log.length; i++) {
|
|
1808
|
+
const e = this._log[i];
|
|
1809
|
+
if (e.type === "tool_call") {
|
|
1810
|
+
const meta = e.meta;
|
|
1811
|
+
pendingToolCalls.push({
|
|
1812
|
+
id: meta["toolCallId"] ?? "",
|
|
1813
|
+
name: meta["toolName"] ?? "",
|
|
1814
|
+
roundIndex: e.roundIndex,
|
|
1815
|
+
contextId: typeof meta["contextId"] === "string" ? meta["contextId"] : undefined,
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
else if (e.type === "tool_result") {
|
|
1819
|
+
resolvedToolCallIds.add(e.meta["toolCallId"]);
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
for (const tc of pendingToolCalls) {
|
|
1823
|
+
if (resolvedToolCallIds.has(tc.id))
|
|
1824
|
+
continue;
|
|
1825
|
+
if (!tc.id)
|
|
1826
|
+
continue;
|
|
1827
|
+
this._appendEntry(createToolResultEntry(this._nextLogId("tool_result"), this._turnCount, tc.roundIndex ?? this._computeNextRoundIndex(), {
|
|
1828
|
+
toolCallId: tc.id,
|
|
1829
|
+
toolName: tc.name,
|
|
1830
|
+
content: interruptedContent,
|
|
1831
|
+
toolSummary: interruptedContent,
|
|
1832
|
+
}, { isError: false, contextId: tc.contextId }), false);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
_getLastSendableRole() {
|
|
1836
|
+
let importantLog = "";
|
|
1837
|
+
try {
|
|
1838
|
+
importantLog = this._readImportantLog();
|
|
1839
|
+
}
|
|
1840
|
+
catch {
|
|
1841
|
+
importantLog = "";
|
|
1842
|
+
}
|
|
1843
|
+
const agentsMd = this._readAgentsMd();
|
|
1844
|
+
const messages = projectToApiMessages(this._log, {
|
|
1845
|
+
resolveImageRef: (refPath) => this._resolveImageRef(refPath),
|
|
1846
|
+
importantLog,
|
|
1847
|
+
agentsMd,
|
|
1848
|
+
requiresAlternatingRoles: this.primaryAgent._provider.requiresAlternatingRoles,
|
|
1849
|
+
});
|
|
1850
|
+
if (messages.length === 0)
|
|
1851
|
+
return null;
|
|
1852
|
+
const role = messages[messages.length - 1]["role"];
|
|
1853
|
+
return typeof role === "string" ? role : null;
|
|
1854
|
+
}
|
|
1855
|
+
_isUserSideProtocolRole(role) {
|
|
1856
|
+
if (!role)
|
|
1857
|
+
return true;
|
|
1858
|
+
if (role === "assistant")
|
|
1859
|
+
return false;
|
|
1860
|
+
return true;
|
|
1861
|
+
}
|
|
1862
|
+
// ==================================================================
|
|
1863
|
+
// Activation
|
|
1864
|
+
// ==================================================================
|
|
1865
|
+
async _runActivation(signal, textAccumulator, reasoningAccumulator, suppressStreaming) {
|
|
1866
|
+
const baseRoundIndex = this._computeNextRoundIndex();
|
|
1867
|
+
const streamedAssistantEntries = new Map();
|
|
1868
|
+
const streamedReasoningEntries = new Map();
|
|
1869
|
+
const textBuffers = new Map();
|
|
1870
|
+
const roundContextIds = new Map();
|
|
1871
|
+
const getRoundContextId = (roundIndex) => {
|
|
1872
|
+
let contextId = roundContextIds.get(roundIndex);
|
|
1873
|
+
if (!contextId) {
|
|
1874
|
+
contextId = this._allocateContextId();
|
|
1875
|
+
roundContextIds.set(roundIndex, contextId);
|
|
1876
|
+
}
|
|
1877
|
+
return contextId;
|
|
1878
|
+
};
|
|
1879
|
+
let onTextChunk;
|
|
1880
|
+
let onReasoningChunk;
|
|
1881
|
+
if (suppressStreaming) {
|
|
1882
|
+
// During compact phase: accumulate text but don't stream to TUI
|
|
1883
|
+
if (textAccumulator) {
|
|
1884
|
+
const stripBuf = new ContextTagStripBuffer((chunk) => {
|
|
1885
|
+
textAccumulator.text += chunk;
|
|
1886
|
+
});
|
|
1887
|
+
const buf = new NoReplyStreamBuffer((chunk) => stripBuf.feed(chunk));
|
|
1888
|
+
onTextChunk = (_roundIndex, chunk) => {
|
|
1889
|
+
buf.feed(chunk);
|
|
1890
|
+
return false;
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
if (reasoningAccumulator) {
|
|
1894
|
+
onReasoningChunk = (_roundIndex, chunk) => {
|
|
1895
|
+
reasoningAccumulator.text += chunk;
|
|
1896
|
+
return false;
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
else {
|
|
1901
|
+
const agentName = this.primaryAgent.name;
|
|
1902
|
+
const progress = this._progress;
|
|
1903
|
+
onTextChunk = (roundIndex, chunk) => {
|
|
1904
|
+
let roundBuffer = textBuffers.get(roundIndex);
|
|
1905
|
+
if (!roundBuffer) {
|
|
1906
|
+
const stripBuf = new ContextTagStripBuffer((cleanChunk) => {
|
|
1907
|
+
if (textAccumulator)
|
|
1908
|
+
textAccumulator.text += cleanChunk;
|
|
1909
|
+
if (progress)
|
|
1910
|
+
progress.onTextChunk(agentName, cleanChunk);
|
|
1911
|
+
const entry = streamedAssistantEntries.get(roundIndex);
|
|
1912
|
+
if (!entry) {
|
|
1913
|
+
const nextEntry = createAssistantText(this._nextLogId("assistant_text"), this._turnCount, roundIndex, cleanChunk, cleanChunk, getRoundContextId(roundIndex));
|
|
1914
|
+
this._appendEntry(nextEntry, false);
|
|
1915
|
+
streamedAssistantEntries.set(roundIndex, nextEntry);
|
|
1916
|
+
}
|
|
1917
|
+
else {
|
|
1918
|
+
entry.display += cleanChunk;
|
|
1919
|
+
entry.content = String(entry.content ?? "") + cleanChunk;
|
|
1920
|
+
this._touchLog();
|
|
1921
|
+
}
|
|
1922
|
+
});
|
|
1923
|
+
roundBuffer = new NoReplyStreamBuffer((cleanChunk) => stripBuf.feed(cleanChunk));
|
|
1924
|
+
textBuffers.set(roundIndex, roundBuffer);
|
|
1925
|
+
}
|
|
1926
|
+
roundBuffer.feed(chunk);
|
|
1927
|
+
// Check if the streaming callback actually created/updated a log entry
|
|
1928
|
+
return streamedAssistantEntries.has(roundIndex);
|
|
1929
|
+
};
|
|
1930
|
+
onReasoningChunk = (roundIndex, chunk) => {
|
|
1931
|
+
if (reasoningAccumulator)
|
|
1932
|
+
reasoningAccumulator.text += chunk;
|
|
1933
|
+
if (progress)
|
|
1934
|
+
progress.onReasoningChunk(agentName, chunk);
|
|
1935
|
+
const entry = streamedReasoningEntries.get(roundIndex);
|
|
1936
|
+
if (!entry) {
|
|
1937
|
+
const nextEntry = createReasoning(this._nextLogId("reasoning"), this._turnCount, roundIndex, chunk, chunk, undefined, getRoundContextId(roundIndex));
|
|
1938
|
+
this._appendEntry(nextEntry, false);
|
|
1939
|
+
streamedReasoningEntries.set(roundIndex, nextEntry);
|
|
1940
|
+
}
|
|
1941
|
+
else {
|
|
1942
|
+
entry.display += chunk;
|
|
1943
|
+
entry.content = String(entry.content ?? "") + chunk;
|
|
1944
|
+
this._touchLog();
|
|
1945
|
+
}
|
|
1946
|
+
return true;
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
let onToolCall;
|
|
1950
|
+
if (this._progress) {
|
|
1951
|
+
const step = this._turnCount;
|
|
1952
|
+
const progress = this._progress;
|
|
1953
|
+
onToolCall = (name, tool, args, summary) => {
|
|
1954
|
+
progress.onToolCall(step, name, tool, args, summary);
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
// Token update callback: update _lastInputTokens after each provider call
|
|
1958
|
+
// so the TUI can display real-time context usage.
|
|
1959
|
+
const onTokenUpdate = (inputTokens, usage) => {
|
|
1960
|
+
this._lastInputTokens = inputTokens;
|
|
1961
|
+
this._lastTotalTokens = usage?.totalTokens ?? inputTokens;
|
|
1962
|
+
this._lastCacheReadTokens = usage?.cacheReadTokens ?? 0;
|
|
1963
|
+
this._appendEntry(createTokenUpdate(this._nextLogId("token_update"), this._turnCount, inputTokens, usage?.cacheReadTokens, usage?.cacheCreationTokens, usage?.totalTokens), false);
|
|
1964
|
+
if (this._progress) {
|
|
1965
|
+
const extra = { input_tokens: inputTokens };
|
|
1966
|
+
if (usage) {
|
|
1967
|
+
if (usage.cacheReadTokens > 0)
|
|
1968
|
+
extra["cache_read_tokens"] = usage.cacheReadTokens;
|
|
1969
|
+
if (usage.cacheCreationTokens > 0)
|
|
1970
|
+
extra["cache_creation_tokens"] = usage.cacheCreationTokens;
|
|
1971
|
+
}
|
|
1972
|
+
this._progress.emit({
|
|
1973
|
+
step: this._turnCount,
|
|
1974
|
+
agent: this.primaryAgent.name,
|
|
1975
|
+
action: "token_update",
|
|
1976
|
+
message: "",
|
|
1977
|
+
level: "quiet",
|
|
1978
|
+
timestamp: Date.now() / 1000,
|
|
1979
|
+
usage: { input_tokens: inputTokens },
|
|
1980
|
+
extra,
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
};
|
|
1984
|
+
const agentName = this.primaryAgent.name;
|
|
1985
|
+
const emitRetryAttempt = (attempt, max, delaySec, errMsg) => {
|
|
1986
|
+
if (!this._compactInProgress) {
|
|
1987
|
+
this._appendEntry(createStatus(this._nextLogId("status"), this._turnCount, `[Network retry ${attempt}/${max}] waiting ${delaySec}s: ${errMsg}`, "retry_attempt"), false);
|
|
1988
|
+
}
|
|
1989
|
+
this._progress?.onRetryAttempt(agentName, attempt, max, delaySec, errMsg);
|
|
1990
|
+
};
|
|
1991
|
+
const emitRetrySuccess = (attempt) => {
|
|
1992
|
+
if (!this._compactInProgress) {
|
|
1993
|
+
this._appendEntry(createStatus(this._nextLogId("status"), this._turnCount, `[Network retry succeeded] attempt ${attempt}`, "retry_success"), false);
|
|
1994
|
+
}
|
|
1995
|
+
this._progress?.onRetrySuccess(agentName, attempt);
|
|
1996
|
+
};
|
|
1997
|
+
const emitRetryExhausted = (max, errMsg) => {
|
|
1998
|
+
if (!this._compactInProgress) {
|
|
1999
|
+
this._appendEntry(createErrorEntry(this._nextLogId("error"), this._turnCount, `[Network retry exhausted after ${max} attempts] ${errMsg}`, "retry_exhausted"), false);
|
|
2000
|
+
}
|
|
2001
|
+
this._progress?.onRetryExhausted(agentName, max, errMsg);
|
|
2002
|
+
};
|
|
2003
|
+
// v2: callback-based message management
|
|
2004
|
+
// getMessages projects from _log via projectToApiMessages
|
|
2005
|
+
const getMessages = () => {
|
|
2006
|
+
const showAnnotations = this._showContextRoundsRemaining > 0
|
|
2007
|
+
? this._showContextAnnotations ?? undefined
|
|
2008
|
+
: undefined;
|
|
2009
|
+
let importantLog = this._readImportantLog();
|
|
2010
|
+
// Inject active plan content alongside important log
|
|
2011
|
+
if (this._activePlanFile) {
|
|
2012
|
+
try {
|
|
2013
|
+
const planContent = readFileSync(this._activePlanFile, "utf-8");
|
|
2014
|
+
if (planContent) {
|
|
2015
|
+
importantLog += `\n\n---\n## Active Plan\n${planContent}`;
|
|
2016
|
+
// Detect checkpoint changes from file edits and emit update
|
|
2017
|
+
const { checkpoints, checked } = this._parsePlanCheckpoints(planContent);
|
|
2018
|
+
if (checkpoints.length !== this._activePlanCheckpoints.length ||
|
|
2019
|
+
checkpoints.some((t, i) => t !== this._activePlanCheckpoints[i]) ||
|
|
2020
|
+
checked.some((c, i) => c !== this._activePlanChecked[i])) {
|
|
2021
|
+
this._activePlanCheckpoints = checkpoints;
|
|
2022
|
+
this._activePlanChecked = checked;
|
|
2023
|
+
this._emitPlanProgress("plan_update");
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
catch {
|
|
2028
|
+
// Plan file may have been deleted externally — ignore
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
const agentsMd = this._readAgentsMd();
|
|
2032
|
+
return projectToApiMessages(this._log, {
|
|
2033
|
+
resolveImageRef: (refPath) => this._resolveImageRef(refPath),
|
|
2034
|
+
importantLog,
|
|
2035
|
+
agentsMd,
|
|
2036
|
+
requiresAlternatingRoles: this.primaryAgent._provider.requiresAlternatingRoles,
|
|
2037
|
+
showContextAnnotations: showAnnotations ?? undefined,
|
|
2038
|
+
});
|
|
2039
|
+
};
|
|
2040
|
+
const appendEntry = (entry) => {
|
|
2041
|
+
if (this._compactInProgress) {
|
|
2042
|
+
entry.tuiVisible = false;
|
|
2043
|
+
entry.displayKind = null;
|
|
2044
|
+
entry.meta["compactPhase"] = true;
|
|
2045
|
+
}
|
|
2046
|
+
this._appendEntry(entry, false);
|
|
2047
|
+
};
|
|
2048
|
+
const allocId = (type) => {
|
|
2049
|
+
return this._nextLogId(type);
|
|
2050
|
+
};
|
|
2051
|
+
return this.primaryAgent.asyncRunWithMessages(getMessages, appendEntry, allocId, this._turnCount, baseRoundIndex, this._toolExecutors, onToolCall, onTextChunk, onReasoningChunk, signal, (roundIndex) => getRoundContextId(roundIndex), this._buildCompactCheck(), onTokenUpdate, this._thinkingLevel === "default" ? undefined : this._thinkingLevel, this._cacheHitEnabled, this._compactInProgress ? undefined : (() => this.onSaveRequest?.()), this._beforeToolExecute, () => this._buildNotificationSummary(), !suppressStreaming, emitRetryAttempt, emitRetrySuccess, emitRetryExhausted);
|
|
2052
|
+
}
|
|
2053
|
+
// ==================================================================
|
|
2054
|
+
// Tool argument helpers
|
|
2055
|
+
// ==================================================================
|
|
2056
|
+
_toolArgError(toolName, message) {
|
|
2057
|
+
return new ToolResult({ content: `Error: invalid arguments for ${toolName}: ${message}` });
|
|
2058
|
+
}
|
|
2059
|
+
_argOptionalString(toolName, args, key) {
|
|
2060
|
+
const value = args[key];
|
|
2061
|
+
if (value == null)
|
|
2062
|
+
return undefined;
|
|
2063
|
+
if (typeof value !== "string") {
|
|
2064
|
+
return this._toolArgError(toolName, `'${key}' must be a string.`);
|
|
2065
|
+
}
|
|
2066
|
+
return value;
|
|
2067
|
+
}
|
|
2068
|
+
_argRequiredString(toolName, args, key, opts) {
|
|
2069
|
+
const value = args[key];
|
|
2070
|
+
if (typeof value !== "string") {
|
|
2071
|
+
return this._toolArgError(toolName, `'${key}' must be a string.`);
|
|
2072
|
+
}
|
|
2073
|
+
if (opts?.nonEmpty && !value.trim()) {
|
|
2074
|
+
return this._toolArgError(toolName, `'${key}' must be a non-empty string.`);
|
|
2075
|
+
}
|
|
2076
|
+
return value;
|
|
2077
|
+
}
|
|
2078
|
+
_argRequiredStringArray(toolName, args, key) {
|
|
2079
|
+
const value = args[key];
|
|
2080
|
+
if (!Array.isArray(value)) {
|
|
2081
|
+
return this._toolArgError(toolName, `'${key}' must be an array of strings.`);
|
|
2082
|
+
}
|
|
2083
|
+
for (let i = 0; i < value.length; i++) {
|
|
2084
|
+
if (typeof value[i] !== "string") {
|
|
2085
|
+
return this._toolArgError(toolName, `'${key}[${i}]' must be a string.`);
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
return value;
|
|
2089
|
+
}
|
|
2090
|
+
_argOptionalInteger(toolName, args, key) {
|
|
2091
|
+
const value = args[key];
|
|
2092
|
+
if (value == null)
|
|
2093
|
+
return undefined;
|
|
2094
|
+
if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value)) {
|
|
2095
|
+
return this._toolArgError(toolName, `'${key}' must be an integer.`);
|
|
2096
|
+
}
|
|
2097
|
+
return value;
|
|
2098
|
+
}
|
|
2099
|
+
// ==================================================================
|
|
2100
|
+
// Ask tool
|
|
2101
|
+
// ==================================================================
|
|
2102
|
+
_execAsk(args) {
|
|
2103
|
+
// Validate args
|
|
2104
|
+
const questions = args["questions"];
|
|
2105
|
+
if (!Array.isArray(questions) || questions.length === 0 || questions.length > 4) {
|
|
2106
|
+
return new ToolResult({
|
|
2107
|
+
content: "Error: 'questions' must be an array of 1-4 items.",
|
|
2108
|
+
});
|
|
2109
|
+
}
|
|
2110
|
+
const parsedQuestions = [];
|
|
2111
|
+
for (let i = 0; i < questions.length; i++) {
|
|
2112
|
+
const q = questions[i];
|
|
2113
|
+
if (!q || typeof q["question"] !== "string") {
|
|
2114
|
+
return new ToolResult({
|
|
2115
|
+
content: `Error: questions[${i}].question must be a string.`,
|
|
2116
|
+
});
|
|
2117
|
+
}
|
|
2118
|
+
const opts = q["options"];
|
|
2119
|
+
if (!Array.isArray(opts) || opts.length === 0 || opts.length > 4) {
|
|
2120
|
+
return new ToolResult({
|
|
2121
|
+
content: `Error: questions[${i}].options must be an array of 1-4 items.`,
|
|
2122
|
+
});
|
|
2123
|
+
}
|
|
2124
|
+
const parsedOpts = [];
|
|
2125
|
+
for (let j = 0; j < opts.length; j++) {
|
|
2126
|
+
const o = opts[j];
|
|
2127
|
+
if (!o || typeof o["label"] !== "string") {
|
|
2128
|
+
return new ToolResult({
|
|
2129
|
+
content: `Error: questions[${i}].options[${j}].label must be a string.`,
|
|
2130
|
+
});
|
|
2131
|
+
}
|
|
2132
|
+
parsedOpts.push({
|
|
2133
|
+
label: o["label"],
|
|
2134
|
+
description: typeof o["description"] === "string" ? o["description"] : undefined,
|
|
2135
|
+
kind: "normal",
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
parsedOpts.push({
|
|
2139
|
+
label: ASK_CUSTOM_OPTION_LABEL,
|
|
2140
|
+
kind: "custom_input",
|
|
2141
|
+
systemAdded: true,
|
|
2142
|
+
});
|
|
2143
|
+
parsedOpts.push({
|
|
2144
|
+
label: ASK_DISCUSS_OPTION_LABEL,
|
|
2145
|
+
kind: "discuss_further",
|
|
2146
|
+
systemAdded: true,
|
|
2147
|
+
});
|
|
2148
|
+
parsedQuestions.push({
|
|
2149
|
+
question: q["question"],
|
|
2150
|
+
options: parsedOpts,
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
const ask = {
|
|
2154
|
+
id: randomUUID(),
|
|
2155
|
+
kind: "agent_question",
|
|
2156
|
+
createdAt: new Date().toISOString(),
|
|
2157
|
+
source: {
|
|
2158
|
+
agentId: this.primaryAgent.name,
|
|
2159
|
+
agentName: this.primaryAgent.name,
|
|
2160
|
+
toolName: "ask",
|
|
2161
|
+
},
|
|
2162
|
+
roundIndex: undefined,
|
|
2163
|
+
summary: `Agent asking: ${parsedQuestions[0].question}${parsedQuestions.length > 1 ? ` (+${parsedQuestions.length - 1} more)` : ""}`,
|
|
2164
|
+
payload: { questions: parsedQuestions, toolCallId: "" },
|
|
2165
|
+
options: [], // per-question options are in payload
|
|
2166
|
+
};
|
|
2167
|
+
throw new AskPendingError(ask);
|
|
2168
|
+
}
|
|
2169
|
+
_buildAgentQuestionToolResult(questions, decision) {
|
|
2170
|
+
const lines = [];
|
|
2171
|
+
let hasDiscussFurther = false;
|
|
2172
|
+
for (let i = 0; i < questions.length; i++) {
|
|
2173
|
+
const q = questions[i];
|
|
2174
|
+
const answer = decision.answers.find((a) => a.questionIndex === i);
|
|
2175
|
+
lines.push(`Question ${i + 1}: "${q.question}"`);
|
|
2176
|
+
if (!answer) {
|
|
2177
|
+
lines.push("Answer: [missing]");
|
|
2178
|
+
}
|
|
2179
|
+
else {
|
|
2180
|
+
lines.push(`Answer: ${answer.answerText}`);
|
|
2181
|
+
const selected = q.options[answer.selectedOptionIndex];
|
|
2182
|
+
if (selected?.kind === "discuss_further") {
|
|
2183
|
+
hasDiscussFurther = true;
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
if (answer?.note) {
|
|
2187
|
+
lines.push(`User note: ${answer.note}`);
|
|
2188
|
+
}
|
|
2189
|
+
lines.push("");
|
|
2190
|
+
}
|
|
2191
|
+
if (hasDiscussFurther) {
|
|
2192
|
+
lines.push(ASK_DISCUSS_FURTHER_GUIDANCE);
|
|
2193
|
+
}
|
|
2194
|
+
return new ToolResult({ content: lines.join("\n").trim() });
|
|
2195
|
+
}
|
|
2196
|
+
_buildAgentQuestionPreview(questions, decision) {
|
|
2197
|
+
const lines = [];
|
|
2198
|
+
for (let i = 0; i < questions.length; i++) {
|
|
2199
|
+
const q = questions[i];
|
|
2200
|
+
const answer = decision.answers.find((a) => a.questionIndex === i);
|
|
2201
|
+
// Show question with all options, marking the selected one
|
|
2202
|
+
lines.push(`Q${questions.length > 1 ? i + 1 : ""}: ${q.question}`);
|
|
2203
|
+
for (let j = 0; j < q.options.length; j++) {
|
|
2204
|
+
const opt = q.options[j];
|
|
2205
|
+
const isSelected = answer?.selectedOptionIndex === j;
|
|
2206
|
+
const marker = isSelected ? "●" : "○";
|
|
2207
|
+
const desc = opt.description ? ` — ${opt.description}` : "";
|
|
2208
|
+
lines.push(` ${marker} ${opt.label}${desc}`);
|
|
2209
|
+
}
|
|
2210
|
+
if (answer && q.options[answer.selectedOptionIndex]?.kind === "custom_input") {
|
|
2211
|
+
lines.push(` ✎ ${answer.answerText}`);
|
|
2212
|
+
}
|
|
2213
|
+
if (answer?.note) {
|
|
2214
|
+
lines.push(` 📝 ${answer.note}`);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
return lines.join("\n");
|
|
2218
|
+
}
|
|
2219
|
+
resolveAgentQuestionAsk(askId, decision) {
|
|
2220
|
+
const ask = this._activeAsk;
|
|
2221
|
+
if (!ask) {
|
|
2222
|
+
throw new Error("No active ask to resolve.");
|
|
2223
|
+
}
|
|
2224
|
+
if (ask.id !== askId) {
|
|
2225
|
+
throw new Error(`Ask id mismatch (active=${ask.id}, got=${askId}).`);
|
|
2226
|
+
}
|
|
2227
|
+
if (ask.kind !== "agent_question") {
|
|
2228
|
+
throw new Error(`Ask kind mismatch (active=${ask.kind}, expected=agent_question).`);
|
|
2229
|
+
}
|
|
2230
|
+
// Create ask_resolution entry in log
|
|
2231
|
+
this._appendEntry(createAskResolution(this._nextLogId("ask_resolution"), this._turnCount, { answers: decision.answers }, askId, "agent_question"), false);
|
|
2232
|
+
const toolResult = this._buildAgentQuestionToolResult(ask.payload.questions, decision);
|
|
2233
|
+
const previewText = this._buildAgentQuestionPreview(ask.payload.questions, decision);
|
|
2234
|
+
const toolCallId = ask.payload.toolCallId || "ask";
|
|
2235
|
+
const toolResultContextId = this._findToolCallContextId(toolCallId, ask.roundIndex)
|
|
2236
|
+
?? this._allocateContextId();
|
|
2237
|
+
this._appendEntry(createToolResultEntry(this._nextLogId("tool_result"), this._turnCount, ask.roundIndex ?? this._computeNextRoundIndex(), {
|
|
2238
|
+
toolCallId,
|
|
2239
|
+
toolName: "ask",
|
|
2240
|
+
content: toolResult.content,
|
|
2241
|
+
toolSummary: "ask resolved",
|
|
2242
|
+
}, {
|
|
2243
|
+
isError: false,
|
|
2244
|
+
contextId: toolResultContextId,
|
|
2245
|
+
previewText,
|
|
2246
|
+
}), false);
|
|
2247
|
+
this._askHistory.push({
|
|
2248
|
+
askId: ask.id,
|
|
2249
|
+
kind: ask.kind,
|
|
2250
|
+
summary: ask.summary,
|
|
2251
|
+
decidedAt: new Date().toISOString(),
|
|
2252
|
+
decision: "answered",
|
|
2253
|
+
source: ask.source,
|
|
2254
|
+
});
|
|
2255
|
+
if (this._askHistory.length > 100) {
|
|
2256
|
+
this._askHistory = this._askHistory.slice(-100);
|
|
2257
|
+
}
|
|
2258
|
+
this._activeAsk = null;
|
|
2259
|
+
this._emitAskResolvedProgress(askId, "answered", "agent_question");
|
|
2260
|
+
this._pendingTurnState = { stage: "activation" };
|
|
2261
|
+
this.onSaveRequest?.();
|
|
2262
|
+
}
|
|
2263
|
+
_execShowContext(args) {
|
|
2264
|
+
// Handle dismiss mode: clear annotations without generating new ones
|
|
2265
|
+
if (args["dismiss"]) {
|
|
2266
|
+
this._showContextRoundsRemaining = 0;
|
|
2267
|
+
this._showContextAnnotations = null;
|
|
2268
|
+
return new ToolResult({ content: "Context annotations dismissed." });
|
|
2269
|
+
}
|
|
2270
|
+
const mc = this.primaryAgent.modelConfig;
|
|
2271
|
+
const provider = this.primaryAgent._provider;
|
|
2272
|
+
const effectiveMax = this._effectiveMaxTokens(mc);
|
|
2273
|
+
const budget = provider.budgetCalcMode === "full_context"
|
|
2274
|
+
? mc.contextLength : mc.contextLength - effectiveMax;
|
|
2275
|
+
const result = generateShowContext(this._log, this._lastInputTokens, budget);
|
|
2276
|
+
this._showContextRoundsRemaining = 1;
|
|
2277
|
+
this._showContextAnnotations = result.annotations;
|
|
2278
|
+
return new ToolResult({ content: result.contextMap });
|
|
2279
|
+
}
|
|
2280
|
+
_execSummarizeContext(args) {
|
|
2281
|
+
const fileMode = typeof args.file === "string";
|
|
2282
|
+
let effectiveArgs = args;
|
|
2283
|
+
if (fileMode) {
|
|
2284
|
+
const fileRel = args.file.trim();
|
|
2285
|
+
if (!fileRel) {
|
|
2286
|
+
return new ToolResult({ content: "Error: 'file' parameter must be a non-empty string." });
|
|
2287
|
+
}
|
|
2288
|
+
const artifactsDir = this._resolveSessionArtifacts();
|
|
2289
|
+
let filePath;
|
|
2290
|
+
try {
|
|
2291
|
+
filePath = safePath({
|
|
2292
|
+
baseDir: artifactsDir,
|
|
2293
|
+
requestedPath: fileRel,
|
|
2294
|
+
cwd: artifactsDir,
|
|
2295
|
+
mustExist: true,
|
|
2296
|
+
expectFile: true,
|
|
2297
|
+
accessKind: "read",
|
|
2298
|
+
}).safePath;
|
|
2299
|
+
}
|
|
2300
|
+
catch (e) {
|
|
2301
|
+
if (e instanceof SafePathError) {
|
|
2302
|
+
const candidatePath = e.details?.resolvedPath || join(artifactsDir, fileRel);
|
|
2303
|
+
return new ToolResult({
|
|
2304
|
+
content: `Error: summary file not found or not accessible at ${candidatePath}\n` +
|
|
2305
|
+
`The 'file' parameter is resolved relative to SESSION_ARTIFACTS (${artifactsDir}).`,
|
|
2306
|
+
});
|
|
2307
|
+
}
|
|
2308
|
+
throw e;
|
|
2309
|
+
}
|
|
2310
|
+
let parsed;
|
|
2311
|
+
try {
|
|
2312
|
+
parsed = yaml.load(readFileSync(filePath, "utf-8"));
|
|
2313
|
+
}
|
|
2314
|
+
catch (e) {
|
|
2315
|
+
return new ToolResult({ content: `Error: failed to parse summary file: ${e}` });
|
|
2316
|
+
}
|
|
2317
|
+
if (!parsed || typeof parsed !== "object") {
|
|
2318
|
+
return new ToolResult({ content: "Error: summary file must be a YAML mapping." });
|
|
2319
|
+
}
|
|
2320
|
+
const operations = parsed["operations"];
|
|
2321
|
+
if (!Array.isArray(operations)) {
|
|
2322
|
+
return new ToolResult({ content: "Error: summary file must contain an 'operations' array." });
|
|
2323
|
+
}
|
|
2324
|
+
effectiveArgs = { operations };
|
|
2325
|
+
}
|
|
2326
|
+
const result = execSummarizeContextOnLog(effectiveArgs, this._log, () => this._allocateContextId(), () => this._nextLogId("summary"), this._turnCount);
|
|
2327
|
+
if (result.results.some((r) => r.success)) {
|
|
2328
|
+
// Don't reset hint state here — wait for next API call's actual inputTokens
|
|
2329
|
+
// to determine the real state (via _updateHintStateAfterApiCall)
|
|
2330
|
+
}
|
|
2331
|
+
this._annotateLatestSummarizeToolCall(result.results);
|
|
2332
|
+
// In file mode, compress intermediate decision-process entries
|
|
2333
|
+
if (fileMode && result.results.some((r) => r.success)) {
|
|
2334
|
+
this._compressFileModeSummarizeSteps(args.file);
|
|
2335
|
+
}
|
|
2336
|
+
this._touchLog();
|
|
2337
|
+
// Auto-dismiss show_context annotations after a successful summarize
|
|
2338
|
+
if (result.results.some((r) => r.success)) {
|
|
2339
|
+
this._showContextRoundsRemaining = 0;
|
|
2340
|
+
this._showContextAnnotations = null;
|
|
2341
|
+
}
|
|
2342
|
+
return new ToolResult({ content: result.output });
|
|
2343
|
+
}
|
|
2344
|
+
// ==================================================================
|
|
2345
|
+
// Plan tool
|
|
2346
|
+
// ==================================================================
|
|
2347
|
+
_execPlan(args) {
|
|
2348
|
+
const action = args["action"];
|
|
2349
|
+
if (typeof action !== "string" || !["submit", "check", "finish"].includes(action)) {
|
|
2350
|
+
return this._toolArgError("plan", "'action' must be one of: submit, check, finish.");
|
|
2351
|
+
}
|
|
2352
|
+
if (action === "submit") {
|
|
2353
|
+
const fileArg = this._argRequiredString("plan", args, "file", { nonEmpty: true });
|
|
2354
|
+
if (fileArg instanceof ToolResult)
|
|
2355
|
+
return fileArg;
|
|
2356
|
+
const fileRel = fileArg.trim();
|
|
2357
|
+
const artifactsDir = this._resolveSessionArtifacts();
|
|
2358
|
+
let filePath;
|
|
2359
|
+
try {
|
|
2360
|
+
filePath = safePath({
|
|
2361
|
+
baseDir: artifactsDir,
|
|
2362
|
+
requestedPath: fileRel,
|
|
2363
|
+
cwd: artifactsDir,
|
|
2364
|
+
mustExist: true,
|
|
2365
|
+
expectFile: true,
|
|
2366
|
+
accessKind: "read",
|
|
2367
|
+
}).safePath;
|
|
2368
|
+
}
|
|
2369
|
+
catch (e) {
|
|
2370
|
+
if (e instanceof SafePathError) {
|
|
2371
|
+
if (e.code === "PATH_NOT_FOUND" || e.code === "PATH_NOT_FILE") {
|
|
2372
|
+
const candidatePath = e.details.resolvedPath || join(artifactsDir, fileRel);
|
|
2373
|
+
return new ToolResult({
|
|
2374
|
+
content: `Error: plan file not found at ${candidatePath}\n` +
|
|
2375
|
+
`The 'file' parameter is resolved relative to SESSION_ARTIFACTS (${artifactsDir}).\n` +
|
|
2376
|
+
`Make sure you wrote the plan file to this directory using write_file(path="${join(artifactsDir, fileRel)}").`,
|
|
2377
|
+
});
|
|
2378
|
+
}
|
|
2379
|
+
return new ToolResult({ content: `Error: invalid plan file path: ${e.message}` });
|
|
2380
|
+
}
|
|
2381
|
+
throw e;
|
|
2382
|
+
}
|
|
2383
|
+
let content;
|
|
2384
|
+
try {
|
|
2385
|
+
content = readFileSync(filePath, "utf-8");
|
|
2386
|
+
}
|
|
2387
|
+
catch (e) {
|
|
2388
|
+
return new ToolResult({
|
|
2389
|
+
content: `Error: could not read plan file: ${e instanceof Error ? e.message : String(e)}`,
|
|
2390
|
+
});
|
|
2391
|
+
}
|
|
2392
|
+
const { checkpoints, checked } = this._parsePlanCheckpoints(content);
|
|
2393
|
+
if (checkpoints.length === 0) {
|
|
2394
|
+
return new ToolResult({
|
|
2395
|
+
content: "Error: no checkpoints found in plan file. " +
|
|
2396
|
+
"Expected a '## Checkpoints' section with items like '- [ ] Do something'.",
|
|
2397
|
+
});
|
|
2398
|
+
}
|
|
2399
|
+
this._activePlanFile = filePath;
|
|
2400
|
+
this._activePlanCheckpoints = checkpoints;
|
|
2401
|
+
this._activePlanChecked = checked;
|
|
2402
|
+
this._emitPlanProgress("plan_submit");
|
|
2403
|
+
return new ToolResult({
|
|
2404
|
+
content: `Plan submitted with ${checkpoints.length} checkpoints.`,
|
|
2405
|
+
});
|
|
2406
|
+
}
|
|
2407
|
+
if (action === "check") {
|
|
2408
|
+
if (!this._activePlanFile) {
|
|
2409
|
+
return new ToolResult({ content: "Error: no active plan. Use action='submit' first." });
|
|
2410
|
+
}
|
|
2411
|
+
const item = args["item"];
|
|
2412
|
+
if (typeof item !== "number" || !Number.isInteger(item)) {
|
|
2413
|
+
return this._toolArgError("plan", "'item' must be an integer index.");
|
|
2414
|
+
}
|
|
2415
|
+
// Re-parse checkpoints from file to handle edits since submit
|
|
2416
|
+
let currentContent;
|
|
2417
|
+
try {
|
|
2418
|
+
currentContent = readFileSync(this._activePlanFile, "utf-8");
|
|
2419
|
+
}
|
|
2420
|
+
catch {
|
|
2421
|
+
return new ToolResult({ content: "Error: could not read plan file." });
|
|
2422
|
+
}
|
|
2423
|
+
const { checkpoints, checked } = this._parsePlanCheckpoints(currentContent);
|
|
2424
|
+
if (checkpoints.length === 0) {
|
|
2425
|
+
return new ToolResult({ content: "Error: no checkpoints found in plan file." });
|
|
2426
|
+
}
|
|
2427
|
+
this._activePlanCheckpoints = checkpoints;
|
|
2428
|
+
this._activePlanChecked = checked;
|
|
2429
|
+
if (item < 0 || item >= checkpoints.length) {
|
|
2430
|
+
return new ToolResult({
|
|
2431
|
+
content: `Error: 'item' index ${item} is out of range (0..${checkpoints.length - 1}).`,
|
|
2432
|
+
});
|
|
2433
|
+
}
|
|
2434
|
+
this._activePlanChecked[item] = true;
|
|
2435
|
+
// Update the file on disk: replace the matching unchecked item with checked
|
|
2436
|
+
try {
|
|
2437
|
+
const lines = currentContent.split("\n");
|
|
2438
|
+
let checkpointIndex = 0;
|
|
2439
|
+
let inCheckpointsSection = false;
|
|
2440
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2441
|
+
if (/^## Checkpoints\b/.test(lines[i])) {
|
|
2442
|
+
inCheckpointsSection = true;
|
|
2443
|
+
continue;
|
|
2444
|
+
}
|
|
2445
|
+
if (inCheckpointsSection && /^## /.test(lines[i]))
|
|
2446
|
+
break;
|
|
2447
|
+
if (inCheckpointsSection && /^- \[[ x]\] .+$/.test(lines[i])) {
|
|
2448
|
+
if (checkpointIndex === item) {
|
|
2449
|
+
lines[i] = lines[i].replace(/^- \[ \]/, "- [x]");
|
|
2450
|
+
break;
|
|
2451
|
+
}
|
|
2452
|
+
checkpointIndex++;
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
writeFileSync(this._activePlanFile, lines.join("\n"), "utf-8");
|
|
2456
|
+
}
|
|
2457
|
+
catch {
|
|
2458
|
+
// File write failure is non-fatal — in-memory state is still updated
|
|
2459
|
+
}
|
|
2460
|
+
this._emitPlanProgress("plan_update");
|
|
2461
|
+
return new ToolResult({
|
|
2462
|
+
content: `Checkpoint ${item} marked as done: ${checkpoints[item]}`,
|
|
2463
|
+
});
|
|
2464
|
+
}
|
|
2465
|
+
// action === "finish"
|
|
2466
|
+
this._activePlanFile = null;
|
|
2467
|
+
this._activePlanCheckpoints = [];
|
|
2468
|
+
this._activePlanChecked = [];
|
|
2469
|
+
this._emitPlanProgress("plan_finish");
|
|
2470
|
+
return new ToolResult({ content: "Plan finished and dismissed." });
|
|
2471
|
+
}
|
|
2472
|
+
_parsePlanCheckpoints(content) {
|
|
2473
|
+
const checkpoints = [];
|
|
2474
|
+
const checked = [];
|
|
2475
|
+
let inCheckpointsSection = false;
|
|
2476
|
+
for (const line of content.split("\n")) {
|
|
2477
|
+
if (/^## Checkpoints\b/.test(line)) {
|
|
2478
|
+
inCheckpointsSection = true;
|
|
2479
|
+
continue;
|
|
2480
|
+
}
|
|
2481
|
+
if (inCheckpointsSection && /^## /.test(line))
|
|
2482
|
+
break;
|
|
2483
|
+
if (inCheckpointsSection) {
|
|
2484
|
+
const match = line.match(/^- \[([x ])\] (.+)$/);
|
|
2485
|
+
if (match) {
|
|
2486
|
+
checkpoints.push(match[2]);
|
|
2487
|
+
checked.push(match[1] === "x");
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
return { checkpoints, checked };
|
|
2492
|
+
}
|
|
2493
|
+
_emitPlanProgress(action) {
|
|
2494
|
+
if (!this._progress)
|
|
2495
|
+
return;
|
|
2496
|
+
const checkpoints = this._activePlanCheckpoints.map((text, i) => ({
|
|
2497
|
+
text,
|
|
2498
|
+
checked: this._activePlanChecked[i] ?? false,
|
|
2499
|
+
}));
|
|
2500
|
+
this._progress.emit({
|
|
2501
|
+
step: this._turnCount,
|
|
2502
|
+
agent: this.primaryAgent.name,
|
|
2503
|
+
action,
|
|
2504
|
+
message: "",
|
|
2505
|
+
level: "normal",
|
|
2506
|
+
timestamp: Date.now() / 1000,
|
|
2507
|
+
usage: {},
|
|
2508
|
+
extra: { checkpoints },
|
|
2509
|
+
});
|
|
2510
|
+
}
|
|
2511
|
+
_annotateLatestSummarizeToolCall(results) {
|
|
2512
|
+
const resolvedToolCallIds = new Set();
|
|
2513
|
+
let summarizeEntry = null;
|
|
2514
|
+
for (let i = this._log.length - 1; i >= 0; i--) {
|
|
2515
|
+
const entry = this._log[i];
|
|
2516
|
+
if (entry.discarded)
|
|
2517
|
+
continue;
|
|
2518
|
+
if (entry.type === "tool_result") {
|
|
2519
|
+
const toolCallId = entry.meta["toolCallId"];
|
|
2520
|
+
if (toolCallId)
|
|
2521
|
+
resolvedToolCallIds.add(String(toolCallId));
|
|
2522
|
+
continue;
|
|
2523
|
+
}
|
|
2524
|
+
if (entry.type !== "tool_call")
|
|
2525
|
+
continue;
|
|
2526
|
+
const toolCallId = String(entry.meta["toolCallId"] ?? "");
|
|
2527
|
+
if (resolvedToolCallIds.has(toolCallId))
|
|
2528
|
+
continue;
|
|
2529
|
+
if (entry.meta["toolName"] !== "summarize_context")
|
|
2530
|
+
continue;
|
|
2531
|
+
summarizeEntry = entry;
|
|
2532
|
+
break;
|
|
2533
|
+
}
|
|
2534
|
+
if (!summarizeEntry)
|
|
2535
|
+
return;
|
|
2536
|
+
const content = summarizeEntry.content;
|
|
2537
|
+
const args = content["arguments"] ?? {};
|
|
2538
|
+
const operations = (args["operations"] ?? []).map((op) => ({ ...op }));
|
|
2539
|
+
for (let i = 0; i < operations.length && i < results.length; i++) {
|
|
2540
|
+
if (!results[i].success || !results[i].newContextId)
|
|
2541
|
+
continue;
|
|
2542
|
+
operations[i]["_result_context_id"] = results[i].newContextId;
|
|
2543
|
+
}
|
|
2544
|
+
summarizeEntry.content = {
|
|
2545
|
+
...content,
|
|
2546
|
+
arguments: {
|
|
2547
|
+
...args,
|
|
2548
|
+
operations,
|
|
2549
|
+
},
|
|
2550
|
+
};
|
|
2551
|
+
}
|
|
2552
|
+
/**
|
|
2553
|
+
* Compress intermediate tool calls (read_file, write_file, edit_file) between the
|
|
2554
|
+
* last show_context and the current summarize_context when file mode was used.
|
|
2555
|
+
* These entries represent the "decision process" of building the summary file.
|
|
2556
|
+
*/
|
|
2557
|
+
_compressFileModeSummarizeSteps(filePath) {
|
|
2558
|
+
// Resolve the full file path for matching against tool call arguments
|
|
2559
|
+
const artifactsDir = this._resolveSessionArtifacts();
|
|
2560
|
+
const resolvedFilePath = resolve(artifactsDir, filePath.trim());
|
|
2561
|
+
// Find the current summarize_context tool_call (most recent unresolved one)
|
|
2562
|
+
let summarizeIdx = -1;
|
|
2563
|
+
const resolvedToolCallIds = new Set();
|
|
2564
|
+
for (let i = this._log.length - 1; i >= 0; i--) {
|
|
2565
|
+
const entry = this._log[i];
|
|
2566
|
+
if (entry.discarded)
|
|
2567
|
+
continue;
|
|
2568
|
+
if (entry.type === "tool_result") {
|
|
2569
|
+
const toolCallId = entry.meta["toolCallId"];
|
|
2570
|
+
if (toolCallId)
|
|
2571
|
+
resolvedToolCallIds.add(String(toolCallId));
|
|
2572
|
+
continue;
|
|
2573
|
+
}
|
|
2574
|
+
if (entry.type !== "tool_call")
|
|
2575
|
+
continue;
|
|
2576
|
+
const toolCallId = String(entry.meta["toolCallId"] ?? "");
|
|
2577
|
+
if (resolvedToolCallIds.has(toolCallId))
|
|
2578
|
+
continue;
|
|
2579
|
+
if (entry.meta["toolName"] !== "summarize_context")
|
|
2580
|
+
continue;
|
|
2581
|
+
summarizeIdx = i;
|
|
2582
|
+
break;
|
|
2583
|
+
}
|
|
2584
|
+
if (summarizeIdx < 0)
|
|
2585
|
+
return;
|
|
2586
|
+
// Find the most recent show_context tool_call before the summarize_context
|
|
2587
|
+
let showContextIdx = -1;
|
|
2588
|
+
for (let i = summarizeIdx - 1; i >= 0; i--) {
|
|
2589
|
+
const entry = this._log[i];
|
|
2590
|
+
if (entry.discarded || entry.summarized)
|
|
2591
|
+
continue;
|
|
2592
|
+
if (entry.type !== "tool_call")
|
|
2593
|
+
continue;
|
|
2594
|
+
if (entry.meta["toolName"] === "show_context") {
|
|
2595
|
+
showContextIdx = i;
|
|
2596
|
+
break;
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
if (showContextIdx < 0)
|
|
2600
|
+
return;
|
|
2601
|
+
// Collect all entries between show_context and summarize_context (exclusive on both ends)
|
|
2602
|
+
const FILE_TOOLS = new Set(["read_file", "write_file", "edit_file"]);
|
|
2603
|
+
const candidateIndices = [];
|
|
2604
|
+
let allQualify = true;
|
|
2605
|
+
for (let i = showContextIdx + 1; i < summarizeIdx; i++) {
|
|
2606
|
+
const entry = this._log[i];
|
|
2607
|
+
if (entry.discarded || entry.summarized)
|
|
2608
|
+
continue;
|
|
2609
|
+
if (entry.type === "tool_call") {
|
|
2610
|
+
const toolName = entry.meta["toolName"];
|
|
2611
|
+
if (!FILE_TOOLS.has(String(toolName))) {
|
|
2612
|
+
allQualify = false;
|
|
2613
|
+
break;
|
|
2614
|
+
}
|
|
2615
|
+
// Check that the tool operates on the summary file
|
|
2616
|
+
const content = entry.content;
|
|
2617
|
+
const toolArgs = content["arguments"] ?? {};
|
|
2618
|
+
const toolPath = String(toolArgs["path"] ?? "");
|
|
2619
|
+
const resolvedToolPath = resolve(artifactsDir, toolPath);
|
|
2620
|
+
if (resolvedToolPath !== resolvedFilePath) {
|
|
2621
|
+
allQualify = false;
|
|
2622
|
+
break;
|
|
2623
|
+
}
|
|
2624
|
+
candidateIndices.push(i);
|
|
2625
|
+
}
|
|
2626
|
+
else if (entry.type === "tool_result" || entry.type === "assistant_text" || entry.type === "reasoning") {
|
|
2627
|
+
candidateIndices.push(i);
|
|
2628
|
+
}
|
|
2629
|
+
else {
|
|
2630
|
+
// Other entry types (e.g. user_message) — don't compress
|
|
2631
|
+
allQualify = false;
|
|
2632
|
+
break;
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
if (!allQualify || candidateIndices.length === 0)
|
|
2636
|
+
return;
|
|
2637
|
+
// Mark all intermediate entries as summarized
|
|
2638
|
+
const summarizedEntryIds = [];
|
|
2639
|
+
for (const idx of candidateIndices) {
|
|
2640
|
+
this._log[idx].summarized = true;
|
|
2641
|
+
summarizedEntryIds.push(this._log[idx].id);
|
|
2642
|
+
}
|
|
2643
|
+
// Insert a synthetic summary entry right before the summarize_context tool_call
|
|
2644
|
+
const newCtxId = this._allocateContextId();
|
|
2645
|
+
const summaryContent = "[Summary (decision process)] summarize_context decision process between show_context and import — omitted.";
|
|
2646
|
+
const summaryEntry = createSummary(this._nextLogId("summary"), this._turnCount, summaryContent, summaryContent, newCtxId, summarizedEntryIds, 1);
|
|
2647
|
+
this._log.splice(summarizeIdx, 0, summaryEntry);
|
|
2648
|
+
}
|
|
2649
|
+
/**
|
|
2650
|
+
* After execSummarizeContext mutates the projected messages array,
|
|
2651
|
+
* mirror changes back to _log: mark entries as summarized and create summary LogEntries.
|
|
2652
|
+
*/
|
|
2653
|
+
_syncSummarizeToLog(messages) {
|
|
2654
|
+
// 1. Build set of contextIds marked as summarized
|
|
2655
|
+
const summarizedCtxIds = new Set();
|
|
2656
|
+
const summarizedByMap = new Map();
|
|
2657
|
+
for (const msg of messages) {
|
|
2658
|
+
if (msg["_is_summarized"] !== true)
|
|
2659
|
+
continue;
|
|
2660
|
+
const ctxId = msg["_context_id"];
|
|
2661
|
+
if (ctxId === undefined || ctxId === null)
|
|
2662
|
+
continue;
|
|
2663
|
+
summarizedCtxIds.add(String(ctxId));
|
|
2664
|
+
const by = msg["_summarized_by"];
|
|
2665
|
+
if (by !== undefined && by !== null) {
|
|
2666
|
+
summarizedByMap.set(String(ctxId), String(by));
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
// 2. Mark corresponding _log entries
|
|
2670
|
+
for (const entry of this._log) {
|
|
2671
|
+
if (entry.summarized)
|
|
2672
|
+
continue;
|
|
2673
|
+
const ctxId = entry.meta["contextId"];
|
|
2674
|
+
if (ctxId && summarizedCtxIds.has(String(ctxId))) {
|
|
2675
|
+
entry.summarized = true;
|
|
2676
|
+
const by = summarizedByMap.get(String(ctxId));
|
|
2677
|
+
if (by)
|
|
2678
|
+
entry.summarizedBy = by;
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
// 3. Find new summary messages and create LogEntries
|
|
2682
|
+
const existingSummaryCtxIds = new Set();
|
|
2683
|
+
for (const entry of this._log) {
|
|
2684
|
+
if (entry.type === "summary") {
|
|
2685
|
+
const ctxId = entry.meta["contextId"];
|
|
2686
|
+
if (ctxId)
|
|
2687
|
+
existingSummaryCtxIds.add(String(ctxId));
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
for (const msg of messages) {
|
|
2691
|
+
if (msg["_is_summary"] !== true)
|
|
2692
|
+
continue;
|
|
2693
|
+
const ctxId = msg["_context_id"];
|
|
2694
|
+
if (!ctxId || existingSummaryCtxIds.has(String(ctxId)))
|
|
2695
|
+
continue;
|
|
2696
|
+
const summarizedIds = msg["_summarized_ids"] ?? [];
|
|
2697
|
+
const depth = msg["_summary_depth"] ?? 1;
|
|
2698
|
+
const content = typeof msg["content"] === "string" ? msg["content"] : "";
|
|
2699
|
+
// Find splice position: before the first log entry summarized by this summary
|
|
2700
|
+
let spliceIdx = this._log.length;
|
|
2701
|
+
for (let i = 0; i < this._log.length; i++) {
|
|
2702
|
+
if (this._log[i].summarizedBy === String(ctxId)) {
|
|
2703
|
+
spliceIdx = i;
|
|
2704
|
+
break;
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
const summaryEntry = createSummary(this._nextLogId("summary"), this._turnCount, content, content, String(ctxId), summarizedIds.map(String), depth);
|
|
2708
|
+
this._log.splice(spliceIdx, 0, summaryEntry);
|
|
2709
|
+
existingSummaryCtxIds.add(String(ctxId));
|
|
2710
|
+
}
|
|
2711
|
+
this._notifyLogListeners();
|
|
2712
|
+
}
|
|
2713
|
+
// ==================================================================
|
|
2714
|
+
// Important log
|
|
2715
|
+
// ==================================================================
|
|
2716
|
+
_readImportantLog() {
|
|
2717
|
+
const path = this._getImportantLogPath();
|
|
2718
|
+
if (existsSync(path)) {
|
|
2719
|
+
const content = readFileSync(path, "utf-8").trim();
|
|
2720
|
+
return content || "(empty file)";
|
|
2721
|
+
}
|
|
2722
|
+
return "(empty file)";
|
|
2723
|
+
}
|
|
2724
|
+
_getImportantLogPath() {
|
|
2725
|
+
return join(this._getArtifactsDir(), "important-log.md");
|
|
2726
|
+
}
|
|
2727
|
+
// ==================================================================
|
|
2728
|
+
// AGENTS.md persistent memory
|
|
2729
|
+
// ==================================================================
|
|
2730
|
+
/**
|
|
2731
|
+
* Read AGENTS.md from user home (~/) and project root, concatenating both.
|
|
2732
|
+
* Global file comes first, project file second.
|
|
2733
|
+
*/
|
|
2734
|
+
_readAgentsMd() {
|
|
2735
|
+
const parts = [];
|
|
2736
|
+
// 1. Global: ~/AGENTS.md
|
|
2737
|
+
const globalPath = join(homedir(), "AGENTS.md");
|
|
2738
|
+
if (existsSync(globalPath)) {
|
|
2739
|
+
try {
|
|
2740
|
+
const content = readFileSync(globalPath, "utf-8").trim();
|
|
2741
|
+
parts.push(content
|
|
2742
|
+
? `## Global Memory (~/AGENTS.md)\n\n${content}`
|
|
2743
|
+
: `## Global Memory (~/AGENTS.md)\n\n(empty file)`);
|
|
2744
|
+
}
|
|
2745
|
+
catch {
|
|
2746
|
+
// Ignore read errors
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
// 2. Project: {PROJECT_ROOT}/AGENTS.md
|
|
2750
|
+
const projectPath = join(this._projectRoot, "AGENTS.md");
|
|
2751
|
+
if (existsSync(projectPath)) {
|
|
2752
|
+
try {
|
|
2753
|
+
const content = readFileSync(projectPath, "utf-8").trim();
|
|
2754
|
+
parts.push(content
|
|
2755
|
+
? `## Project Memory (AGENTS.md)\n\n${content}`
|
|
2756
|
+
: `## Project Memory (AGENTS.md)\n\n(empty file)`);
|
|
2757
|
+
}
|
|
2758
|
+
catch {
|
|
2759
|
+
// Ignore read errors
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
return parts.join("\n\n---\n\n");
|
|
2763
|
+
}
|
|
2764
|
+
_getArtifactsDirIfAvailable() {
|
|
2765
|
+
if (!this._store)
|
|
2766
|
+
return undefined;
|
|
2767
|
+
const d = this._store.artifactsDir;
|
|
2768
|
+
if (d)
|
|
2769
|
+
return d;
|
|
2770
|
+
return undefined;
|
|
2771
|
+
}
|
|
2772
|
+
_createMissingSessionDirOrThrow() {
|
|
2773
|
+
if (!this._store)
|
|
2774
|
+
return;
|
|
2775
|
+
if (this._store.sessionDir)
|
|
2776
|
+
return;
|
|
2777
|
+
if (typeof this._store.createSession !== "function") {
|
|
2778
|
+
throw new Error("Session artifacts directory is unavailable. " +
|
|
2779
|
+
"No session directory is active and the attached SessionStore " +
|
|
2780
|
+
"cannot create one.");
|
|
2781
|
+
}
|
|
2782
|
+
try {
|
|
2783
|
+
this._store.createSession();
|
|
2784
|
+
}
|
|
2785
|
+
catch (e) {
|
|
2786
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
2787
|
+
throw new Error("Failed to create session storage before running this turn. " +
|
|
2788
|
+
`Reason: ${reason}`);
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
_ensureSessionStorageReady() {
|
|
2792
|
+
if (this._sessionArtifactsOverride) {
|
|
2793
|
+
this._refreshSystemPromptPaths();
|
|
2794
|
+
return;
|
|
2795
|
+
}
|
|
2796
|
+
if (!this._store) {
|
|
2797
|
+
throw new Error("Session artifacts directory is unavailable. " +
|
|
2798
|
+
"No SessionStore is attached and no paths.session_artifacts override is configured.");
|
|
2799
|
+
}
|
|
2800
|
+
if (!this._store.sessionDir) {
|
|
2801
|
+
this._createMissingSessionDirOrThrow();
|
|
2802
|
+
}
|
|
2803
|
+
const artifacts = this._getArtifactsDirIfAvailable();
|
|
2804
|
+
if (!artifacts) {
|
|
2805
|
+
throw new Error("Session artifacts directory is unavailable after session initialization. " +
|
|
2806
|
+
"Possible causes: (1) ~/.longeragent/ is not writable, (2) disk is full, " +
|
|
2807
|
+
"(3) permission issues creating the artifacts directory.");
|
|
2808
|
+
}
|
|
2809
|
+
this._refreshSystemPromptPaths();
|
|
2810
|
+
// Auto-create important-log.md if it doesn't exist (starts empty)
|
|
2811
|
+
const logPath = this._getImportantLogPath();
|
|
2812
|
+
if (!existsSync(logPath))
|
|
2813
|
+
writeFileSync(logPath, "");
|
|
2814
|
+
}
|
|
2815
|
+
_getArtifactsDir() {
|
|
2816
|
+
if (this._sessionArtifactsOverride)
|
|
2817
|
+
return this._sessionArtifactsOverride;
|
|
2818
|
+
const d = this._getArtifactsDirIfAvailable();
|
|
2819
|
+
if (d)
|
|
2820
|
+
return d;
|
|
2821
|
+
throw new Error("Session artifacts directory is unavailable. " +
|
|
2822
|
+
"This usually means no active session directory exists yet, or session " +
|
|
2823
|
+
"persistence failed to initialize. " +
|
|
2824
|
+
"Possible causes: (1) ~/.longeragent/ is not writable, (2) disk is full, " +
|
|
2825
|
+
"(3) SessionStore is missing or not ready.");
|
|
2826
|
+
}
|
|
2827
|
+
// ==================================================================
|
|
2828
|
+
// Path variable resolution
|
|
2829
|
+
// ==================================================================
|
|
2830
|
+
_resolveSessionArtifacts(options) {
|
|
2831
|
+
if (this._sessionArtifactsOverride)
|
|
2832
|
+
return this._sessionArtifactsOverride;
|
|
2833
|
+
const d = this._getArtifactsDirIfAvailable();
|
|
2834
|
+
if (d)
|
|
2835
|
+
return d;
|
|
2836
|
+
if (options?.allowUnresolved)
|
|
2837
|
+
return "{SESSION_ARTIFACTS}";
|
|
2838
|
+
return this._getArtifactsDir();
|
|
2839
|
+
}
|
|
2840
|
+
_resolveSystemData(options) {
|
|
2841
|
+
if (this._systemData)
|
|
2842
|
+
return this._systemData;
|
|
2843
|
+
if (this._store?.projectDir)
|
|
2844
|
+
return this._store.projectDir;
|
|
2845
|
+
if (options?.allowUnresolved)
|
|
2846
|
+
return "{SYSTEM_DATA}";
|
|
2847
|
+
const artifacts = this._getArtifactsDir();
|
|
2848
|
+
return join(artifacts, "..");
|
|
2849
|
+
}
|
|
2850
|
+
_renderSystemPrompt(rawPrompt) {
|
|
2851
|
+
return rawPrompt
|
|
2852
|
+
.replace(/\{PROJECT_ROOT\}/g, this._projectRoot)
|
|
2853
|
+
.replace(/\{SESSION_ARTIFACTS\}/g, this._resolveSessionArtifacts({ allowUnresolved: true }))
|
|
2854
|
+
.replace(/\{SYSTEM_DATA\}/g, this._resolveSystemData({ allowUnresolved: true }));
|
|
2855
|
+
}
|
|
2856
|
+
/**
|
|
2857
|
+
* Update the system message in the conversation with re-rendered paths.
|
|
2858
|
+
* Called by setStore() to fix paths after the store is linked.
|
|
2859
|
+
*/
|
|
2860
|
+
_refreshSystemPromptPaths() {
|
|
2861
|
+
const rendered = this._renderSystemPrompt(this.primaryAgent.systemPrompt);
|
|
2862
|
+
// Update the system_prompt entry in _log
|
|
2863
|
+
for (const e of this._log) {
|
|
2864
|
+
if (e.type === "system_prompt" && !e.discarded) {
|
|
2865
|
+
e.content = rendered;
|
|
2866
|
+
break;
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
// ==================================================================
|
|
2871
|
+
// Auto-compact
|
|
2872
|
+
// ==================================================================
|
|
2873
|
+
_buildCompactCheck() {
|
|
2874
|
+
if (this._compactInProgress)
|
|
2875
|
+
return undefined;
|
|
2876
|
+
const mc = this.primaryAgent.modelConfig;
|
|
2877
|
+
const provider = this.primaryAgent._provider;
|
|
2878
|
+
const effectiveMax = this._effectiveMaxTokens(mc);
|
|
2879
|
+
const budget = provider.budgetCalcMode === "full_context"
|
|
2880
|
+
? mc.contextLength
|
|
2881
|
+
: mc.contextLength - effectiveMax;
|
|
2882
|
+
if (budget <= 0)
|
|
2883
|
+
return undefined;
|
|
2884
|
+
const compactOutputRatio = this._thresholds.compact_output / 100;
|
|
2885
|
+
const compactToolcallRatio = this._thresholds.compact_toolcall / 100;
|
|
2886
|
+
return (inputTokens, outputTokens, hasToolCalls) => {
|
|
2887
|
+
const tokensToCheck = provider.budgetCalcMode === "full_context"
|
|
2888
|
+
? inputTokens // full_context mode: only check input
|
|
2889
|
+
: inputTokens + outputTokens;
|
|
2890
|
+
const threshold = hasToolCalls ? compactToolcallRatio : compactOutputRatio;
|
|
2891
|
+
if (tokensToCheck > threshold * budget) {
|
|
2892
|
+
return { compactNeeded: true, scenario: hasToolCalls ? "toolcall" : "output" };
|
|
2893
|
+
}
|
|
2894
|
+
return { compactNeeded: false };
|
|
2895
|
+
};
|
|
2896
|
+
}
|
|
2897
|
+
/**
|
|
2898
|
+
* Run the compact phase: inject compact prompt, let the Agent produce
|
|
2899
|
+
* a continuation prompt (possibly using tools), then return it.
|
|
2900
|
+
*/
|
|
2901
|
+
async _runCompactPhase(scenario, promptOverride, signal) {
|
|
2902
|
+
this._compactInProgress = true;
|
|
2903
|
+
// Emit compact_start event
|
|
2904
|
+
if (this._progress) {
|
|
2905
|
+
this._progress.onCompactStart(this.primaryAgent.name, scenario);
|
|
2906
|
+
}
|
|
2907
|
+
// Inject compact prompt as user_message entry (compactPhase, invisible in TUI)
|
|
2908
|
+
const prompt = promptOverride ?? (scenario === "output" ? COMPACT_PROMPT_OUTPUT : COMPACT_PROMPT_TOOLCALL);
|
|
2909
|
+
const compactPromptEntry = createUserMessageEntry(this._nextLogId("user_message"), this._turnCount, "", // not visible in TUI
|
|
2910
|
+
prompt, this._allocateContextId());
|
|
2911
|
+
compactPromptEntry.tuiVisible = false;
|
|
2912
|
+
compactPromptEntry.meta["compactPhase"] = true;
|
|
2913
|
+
this._appendEntry(compactPromptEntry, false);
|
|
2914
|
+
let continuationPrompt = "";
|
|
2915
|
+
try {
|
|
2916
|
+
for (let i = 0; i < MAX_COMPACT_PHASE_ROUNDS; i++) {
|
|
2917
|
+
if (signal?.aborted)
|
|
2918
|
+
break;
|
|
2919
|
+
const result = await this._runActivation(signal, undefined, undefined, true);
|
|
2920
|
+
if (signal?.aborted)
|
|
2921
|
+
break;
|
|
2922
|
+
if (result.text) {
|
|
2923
|
+
// Agent produced text → this is the continuation prompt
|
|
2924
|
+
const compactRound = this._computeNextRoundIndex();
|
|
2925
|
+
const compactContextId = this._allocateContextId();
|
|
2926
|
+
if (result.reasoningContent) {
|
|
2927
|
+
const compactReasoningEntry = createReasoning(this._nextLogId("reasoning"), this._turnCount, compactRound, "", result.reasoningContent, result.reasoningState, compactContextId);
|
|
2928
|
+
compactReasoningEntry.tuiVisible = false;
|
|
2929
|
+
compactReasoningEntry.displayKind = null;
|
|
2930
|
+
compactReasoningEntry.meta["compactPhase"] = true;
|
|
2931
|
+
this._appendEntry(compactReasoningEntry, false);
|
|
2932
|
+
}
|
|
2933
|
+
const compactReplyEntry = createAssistantText(this._nextLogId("assistant_text"), this._turnCount, compactRound, "", // not visible in TUI
|
|
2934
|
+
result.text, compactContextId);
|
|
2935
|
+
compactReplyEntry.tuiVisible = false;
|
|
2936
|
+
compactReplyEntry.meta["compactPhase"] = true;
|
|
2937
|
+
this._appendEntry(compactReplyEntry, false);
|
|
2938
|
+
continuationPrompt = result.text;
|
|
2939
|
+
break;
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
if (!continuationPrompt) {
|
|
2943
|
+
continuationPrompt = "[Compact phase did not produce a continuation prompt.]";
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
finally {
|
|
2947
|
+
this._compactInProgress = false;
|
|
2948
|
+
}
|
|
2949
|
+
return continuationPrompt;
|
|
2950
|
+
}
|
|
2951
|
+
/**
|
|
2952
|
+
* Execute auto-compact: run compact phase, then reconstruct conversation
|
|
2953
|
+
* with marker + system prompt + continuation prompt.
|
|
2954
|
+
*/
|
|
2955
|
+
async _doAutoCompact(scenario, signal, promptOverride) {
|
|
2956
|
+
const originalTokens = this._lastTotalTokens;
|
|
2957
|
+
// Run compact phase
|
|
2958
|
+
const continuationPrompt = await this._runCompactPhase(scenario, promptOverride, signal);
|
|
2959
|
+
const contCtxId = this._allocateContextId();
|
|
2960
|
+
this._compactCount += 1;
|
|
2961
|
+
// v2 log: compact_marker + compact_context entries (source of truth)
|
|
2962
|
+
this._appendEntry(createCompactMarker(this._nextLogId("compact_marker"), this._turnCount, this._compactCount - 1, originalTokens, 0), false);
|
|
2963
|
+
const currentMarkerIdx = this._log.length - 1;
|
|
2964
|
+
const contContent = `${continuationPrompt}\n\n[Contexts before this point have been compacted.]`;
|
|
2965
|
+
this._appendEntry(createCompactContext(this._nextLogId("compact_context"), this._turnCount, contContent, contCtxId, this._compactCount - 1), false);
|
|
2966
|
+
const sessionDir = this._store?.sessionDir;
|
|
2967
|
+
if (sessionDir) {
|
|
2968
|
+
let previousMarkerIdx = -1;
|
|
2969
|
+
for (let i = currentMarkerIdx - 1; i >= 0; i--) {
|
|
2970
|
+
if (this._log[i].type === "compact_marker" && !this._log[i].discarded) {
|
|
2971
|
+
previousMarkerIdx = i;
|
|
2972
|
+
break;
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
const archiveStartIdx = previousMarkerIdx >= 0 ? previousMarkerIdx + 1 : 1;
|
|
2976
|
+
const archiveEndIdx = currentMarkerIdx - 1;
|
|
2977
|
+
if (archiveEndIdx >= archiveStartIdx) {
|
|
2978
|
+
archiveWindow(sessionDir, this._compactCount - 1, this._log, archiveStartIdx, archiveEndIdx);
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
// Emit compact_end event
|
|
2982
|
+
if (this._progress) {
|
|
2983
|
+
this._progress.onCompactEnd(this.primaryAgent.name, scenario, originalTokens);
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
/**
|
|
2987
|
+
* Check and inject hint compression prompt if thresholds are met.
|
|
2988
|
+
* Two-tier: level 1 and level 2, configurable via settings.json.
|
|
2989
|
+
*/
|
|
2990
|
+
_checkAndInjectHint(_result) {
|
|
2991
|
+
if (this._compactInProgress)
|
|
2992
|
+
return;
|
|
2993
|
+
const mc = this.primaryAgent.modelConfig;
|
|
2994
|
+
const provider = this.primaryAgent._provider;
|
|
2995
|
+
const effectiveMax = this._effectiveMaxTokens(mc);
|
|
2996
|
+
const budget = provider.budgetCalcMode === "full_context"
|
|
2997
|
+
? mc.contextLength : mc.contextLength - effectiveMax;
|
|
2998
|
+
if (budget <= 0)
|
|
2999
|
+
return;
|
|
3000
|
+
const ratio = this._lastInputTokens / budget;
|
|
3001
|
+
const pct = `${Math.round(ratio * 100)}%`;
|
|
3002
|
+
const level2Ratio = this._thresholds.summarize_hint_level2 / 100;
|
|
3003
|
+
const level1Ratio = this._thresholds.summarize_hint_level1 / 100;
|
|
3004
|
+
if (ratio >= level2Ratio && this._hintState !== "level2_sent") {
|
|
3005
|
+
this._deliverMessage("system", HINT_LEVEL2_PROMPT(pct));
|
|
3006
|
+
this._hintState = "level2_sent";
|
|
3007
|
+
}
|
|
3008
|
+
else if (ratio >= level1Ratio && this._hintState === "none") {
|
|
3009
|
+
this._deliverMessage("system", HINT_LEVEL1_PROMPT(pct));
|
|
3010
|
+
this._hintState = "level1_sent";
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
/**
|
|
3014
|
+
* Update hint state based on actual inputTokens from the latest API call.
|
|
3015
|
+
* Implements hysteresis to prevent oscillation.
|
|
3016
|
+
* Reset thresholds are auto-derived from trigger thresholds.
|
|
3017
|
+
*/
|
|
3018
|
+
_updateHintStateAfterApiCall() {
|
|
3019
|
+
const mc = this.primaryAgent.modelConfig;
|
|
3020
|
+
const provider = this.primaryAgent._provider;
|
|
3021
|
+
const effectiveMax = this._effectiveMaxTokens(mc);
|
|
3022
|
+
const budget = provider.budgetCalcMode === "full_context"
|
|
3023
|
+
? mc.contextLength : mc.contextLength - effectiveMax;
|
|
3024
|
+
if (budget <= 0)
|
|
3025
|
+
return;
|
|
3026
|
+
const ratio = this._lastInputTokens / budget;
|
|
3027
|
+
if (ratio < this._hintResetNone) {
|
|
3028
|
+
this._hintState = "none";
|
|
3029
|
+
}
|
|
3030
|
+
else if (ratio < this._hintResetLevel1) {
|
|
3031
|
+
this._hintState = "level1_sent";
|
|
3032
|
+
}
|
|
3033
|
+
// ratio >= HINT_RESET_LEVEL1: keep current state (don't downgrade)
|
|
3034
|
+
}
|
|
3035
|
+
// ==================================================================
|
|
3036
|
+
// Background shell tools
|
|
3037
|
+
// ==================================================================
|
|
3038
|
+
_resolveShellCwd(toolName, requested) {
|
|
3039
|
+
const trimmed = (requested ?? "").trim();
|
|
3040
|
+
if (!trimmed) {
|
|
3041
|
+
return this._projectRoot;
|
|
3042
|
+
}
|
|
3043
|
+
try {
|
|
3044
|
+
return safePath({
|
|
3045
|
+
baseDir: this._projectRoot,
|
|
3046
|
+
requestedPath: trimmed,
|
|
3047
|
+
cwd: this._projectRoot,
|
|
3048
|
+
mustExist: true,
|
|
3049
|
+
expectDirectory: true,
|
|
3050
|
+
accessKind: "list",
|
|
3051
|
+
}).safePath;
|
|
3052
|
+
}
|
|
3053
|
+
catch (err) {
|
|
3054
|
+
if (!(err instanceof SafePathError))
|
|
3055
|
+
throw err;
|
|
3056
|
+
try {
|
|
3057
|
+
return safePath({
|
|
3058
|
+
baseDir: this._resolveSessionArtifacts(),
|
|
3059
|
+
requestedPath: trimmed,
|
|
3060
|
+
cwd: this._resolveSessionArtifacts(),
|
|
3061
|
+
mustExist: true,
|
|
3062
|
+
expectDirectory: true,
|
|
3063
|
+
accessKind: "list",
|
|
3064
|
+
}).safePath;
|
|
3065
|
+
}
|
|
3066
|
+
catch (inner) {
|
|
3067
|
+
if (inner instanceof SafePathError) {
|
|
3068
|
+
return new ToolResult({
|
|
3069
|
+
content: `Error: invalid arguments for ${toolName}: cwd must stay within the project root or SESSION_ARTIFACTS.`,
|
|
3070
|
+
});
|
|
3071
|
+
}
|
|
3072
|
+
throw inner;
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
_execBashBackground(args) {
|
|
3077
|
+
const commandArg = this._argRequiredString("bash_background", args, "command", { nonEmpty: true });
|
|
3078
|
+
if (commandArg instanceof ToolResult)
|
|
3079
|
+
return commandArg;
|
|
3080
|
+
const cwdArg = this._argOptionalString("bash_background", args, "cwd");
|
|
3081
|
+
if (cwdArg instanceof ToolResult)
|
|
3082
|
+
return cwdArg;
|
|
3083
|
+
const idArg = this._argOptionalString("bash_background", args, "id");
|
|
3084
|
+
if (idArg instanceof ToolResult)
|
|
3085
|
+
return idArg;
|
|
3086
|
+
const shellId = idArg
|
|
3087
|
+
? this._normalizeShellId(idArg)
|
|
3088
|
+
: `shell-${++this._shellCounter}`;
|
|
3089
|
+
if (!shellId) {
|
|
3090
|
+
return this._toolArgError("bash_background", "'id' must contain only letters, numbers, '.', '_' or '-'.");
|
|
3091
|
+
}
|
|
3092
|
+
if (this._activeShells.has(shellId)) {
|
|
3093
|
+
return new ToolResult({ content: `Error: shell '${shellId}' is already tracked.` });
|
|
3094
|
+
}
|
|
3095
|
+
const cwd = this._resolveShellCwd("bash_background", cwdArg);
|
|
3096
|
+
if (cwd instanceof ToolResult)
|
|
3097
|
+
return cwd;
|
|
3098
|
+
const logPath = join(this._getShellsDir(), `${shellId}.log`);
|
|
3099
|
+
writeFileSync(logPath, "", "utf-8");
|
|
3100
|
+
let child;
|
|
3101
|
+
try {
|
|
3102
|
+
child = spawn("sh", ["-lc", commandArg], {
|
|
3103
|
+
cwd,
|
|
3104
|
+
env: buildBashEnv(),
|
|
3105
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3106
|
+
});
|
|
3107
|
+
}
|
|
3108
|
+
catch (e) {
|
|
3109
|
+
return new ToolResult({ content: `Error: failed to start background shell: ${e}` });
|
|
3110
|
+
}
|
|
3111
|
+
const entry = {
|
|
3112
|
+
id: shellId,
|
|
3113
|
+
process: child,
|
|
3114
|
+
command: commandArg,
|
|
3115
|
+
cwd,
|
|
3116
|
+
logPath,
|
|
3117
|
+
startTime: performance.now(),
|
|
3118
|
+
status: "running",
|
|
3119
|
+
exitCode: null,
|
|
3120
|
+
signal: null,
|
|
3121
|
+
readOffset: 0,
|
|
3122
|
+
recentOutput: [],
|
|
3123
|
+
explicitKill: false,
|
|
3124
|
+
};
|
|
3125
|
+
this._activeShells.set(shellId, entry);
|
|
3126
|
+
child.stdout?.on("data", (chunk) => {
|
|
3127
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
3128
|
+
this._recordShellChunk(entry, text);
|
|
3129
|
+
});
|
|
3130
|
+
child.stderr?.on("data", (chunk) => {
|
|
3131
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
3132
|
+
this._recordShellChunk(entry, text);
|
|
3133
|
+
});
|
|
3134
|
+
child.on("error", (error) => {
|
|
3135
|
+
entry.status = "failed";
|
|
3136
|
+
entry.exitCode = 1;
|
|
3137
|
+
entry.signal = null;
|
|
3138
|
+
this._deliverMessage("system", `Background shell '${shellId}' failed to start: ${error}. Use \`bash_output(id="${shellId}")\` to inspect ${logPath}.`);
|
|
3139
|
+
});
|
|
3140
|
+
child.on("close", (code, signal) => {
|
|
3141
|
+
entry.exitCode = code;
|
|
3142
|
+
entry.signal = signal;
|
|
3143
|
+
if (entry.explicitKill) {
|
|
3144
|
+
entry.status = "killed";
|
|
3145
|
+
}
|
|
3146
|
+
else if (code === 0) {
|
|
3147
|
+
entry.status = "exited";
|
|
3148
|
+
}
|
|
3149
|
+
else {
|
|
3150
|
+
entry.status = "failed";
|
|
3151
|
+
}
|
|
3152
|
+
const statusText = entry.status === "killed"
|
|
3153
|
+
? `was killed (${signal ?? "TERM"})`
|
|
3154
|
+
: entry.status === "exited"
|
|
3155
|
+
? "completed successfully"
|
|
3156
|
+
: `failed (exit ${code ?? 1})`;
|
|
3157
|
+
this._deliverMessage("system", `Background shell '${shellId}' ${statusText}. Use \`bash_output(id="${shellId}")\` to inspect logs at ${logPath}.`);
|
|
3158
|
+
});
|
|
3159
|
+
return new ToolResult({
|
|
3160
|
+
content: `Started background shell '${shellId}'.\n` +
|
|
3161
|
+
`cwd: ${cwd}\n` +
|
|
3162
|
+
`log: ${logPath}\n` +
|
|
3163
|
+
`Use \`bash_output(id="${shellId}")\` to inspect logs and \`wait(shell="${shellId}", seconds=60)\` to wait for exit.`,
|
|
3164
|
+
});
|
|
3165
|
+
}
|
|
3166
|
+
_execBashOutput(args) {
|
|
3167
|
+
const idArg = this._argRequiredString("bash_output", args, "id", { nonEmpty: true });
|
|
3168
|
+
if (idArg instanceof ToolResult)
|
|
3169
|
+
return idArg;
|
|
3170
|
+
const tailLinesArg = this._argOptionalInteger("bash_output", args, "tail_lines");
|
|
3171
|
+
if (tailLinesArg instanceof ToolResult)
|
|
3172
|
+
return tailLinesArg;
|
|
3173
|
+
const maxCharsArg = this._argOptionalInteger("bash_output", args, "max_chars");
|
|
3174
|
+
if (maxCharsArg instanceof ToolResult)
|
|
3175
|
+
return maxCharsArg;
|
|
3176
|
+
const entry = this._activeShells.get(idArg);
|
|
3177
|
+
if (!entry) {
|
|
3178
|
+
return new ToolResult({ content: `Error: shell '${idArg}' not found.` });
|
|
3179
|
+
}
|
|
3180
|
+
const maxChars = Math.max(500, Math.min(50_000, maxCharsArg ?? 8_000));
|
|
3181
|
+
const fullText = existsSync(entry.logPath) ? readFileSync(entry.logPath, "utf-8") : "";
|
|
3182
|
+
let body = "";
|
|
3183
|
+
if (tailLinesArg !== undefined) {
|
|
3184
|
+
const lines = fullText.split("\n");
|
|
3185
|
+
body = lines.slice(-Math.max(1, tailLinesArg)).join("\n").trimEnd();
|
|
3186
|
+
}
|
|
3187
|
+
else {
|
|
3188
|
+
const fullBuffer = Buffer.from(fullText, "utf-8");
|
|
3189
|
+
const unread = fullBuffer.subarray(entry.readOffset).toString("utf-8");
|
|
3190
|
+
entry.readOffset = fullBuffer.length;
|
|
3191
|
+
if (!unread.trim()) {
|
|
3192
|
+
body = "(No new output since the last read.)";
|
|
3193
|
+
}
|
|
3194
|
+
else if (unread.length > maxChars) {
|
|
3195
|
+
const visible = unread.slice(0, maxChars);
|
|
3196
|
+
const omittedChars = unread.length - visible.length;
|
|
3197
|
+
const omittedLines = unread.slice(visible.length).split("\n").filter(Boolean).length;
|
|
3198
|
+
body =
|
|
3199
|
+
`${visible.trimEnd()}\n\n` +
|
|
3200
|
+
`[Truncated here because unread output exceeded ${maxChars} chars; skipped ${omittedChars.toLocaleString()} chars` +
|
|
3201
|
+
(omittedLines > 0 ? ` / ${omittedLines.toLocaleString()} lines` : "") +
|
|
3202
|
+
`. Full log: ${entry.logPath}]`;
|
|
3203
|
+
}
|
|
3204
|
+
else {
|
|
3205
|
+
body = unread.trimEnd();
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
3208
|
+
return new ToolResult({
|
|
3209
|
+
content: `# Shell Output\n` +
|
|
3210
|
+
`id: ${entry.id}\n` +
|
|
3211
|
+
`status: ${entry.status}\n` +
|
|
3212
|
+
`log: ${entry.logPath}\n\n` +
|
|
3213
|
+
`${body || "(No output yet.)"}`,
|
|
3214
|
+
});
|
|
3215
|
+
}
|
|
3216
|
+
_execKillShell(args) {
|
|
3217
|
+
const idsArg = this._argRequiredStringArray("kill_shell", args, "ids");
|
|
3218
|
+
if (idsArg instanceof ToolResult)
|
|
3219
|
+
return idsArg;
|
|
3220
|
+
const signalArg = this._argOptionalString("kill_shell", args, "signal");
|
|
3221
|
+
if (signalArg instanceof ToolResult)
|
|
3222
|
+
return signalArg;
|
|
3223
|
+
const rawSignal = (signalArg?.trim() || "SIGTERM").toUpperCase();
|
|
3224
|
+
const signal = (rawSignal.startsWith("SIG") ? rawSignal : `SIG${rawSignal}`);
|
|
3225
|
+
const parts = [];
|
|
3226
|
+
for (const id of idsArg) {
|
|
3227
|
+
const entry = this._activeShells.get(id);
|
|
3228
|
+
if (!entry) {
|
|
3229
|
+
parts.push(`'${id}': not found.`);
|
|
3230
|
+
continue;
|
|
3231
|
+
}
|
|
3232
|
+
if (entry.status !== "running") {
|
|
3233
|
+
parts.push(`'${id}': already ${entry.status}.`);
|
|
3234
|
+
continue;
|
|
3235
|
+
}
|
|
3236
|
+
entry.explicitKill = true;
|
|
3237
|
+
try {
|
|
3238
|
+
entry.process.kill(signal);
|
|
3239
|
+
parts.push(`'${id}': sent ${signal}.`);
|
|
3240
|
+
}
|
|
3241
|
+
catch (e) {
|
|
3242
|
+
parts.push(`'${id}': failed to send ${signal} (${e}).`);
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
return new ToolResult({ content: parts.join(" ") || "No shells specified." });
|
|
3246
|
+
}
|
|
3247
|
+
// ==================================================================
|
|
3248
|
+
// Sub-agent spawn / cancel / lifecycle
|
|
3249
|
+
// ==================================================================
|
|
3250
|
+
async _execSpawnAgents(args) {
|
|
3251
|
+
const fileArg = this._argRequiredString("spawn_agent", args, "file", { nonEmpty: true });
|
|
3252
|
+
if (fileArg instanceof ToolResult)
|
|
3253
|
+
return fileArg;
|
|
3254
|
+
const fileRel = fileArg.trim();
|
|
3255
|
+
if (!fileRel) {
|
|
3256
|
+
return new ToolResult({ content: "Error: 'file' parameter is required." });
|
|
3257
|
+
}
|
|
3258
|
+
const artifactsDir = this._resolveSessionArtifacts();
|
|
3259
|
+
let filePath;
|
|
3260
|
+
try {
|
|
3261
|
+
filePath = safePath({
|
|
3262
|
+
baseDir: artifactsDir,
|
|
3263
|
+
requestedPath: fileRel,
|
|
3264
|
+
cwd: artifactsDir,
|
|
3265
|
+
mustExist: true,
|
|
3266
|
+
expectFile: true,
|
|
3267
|
+
accessKind: "spawn_call_file",
|
|
3268
|
+
}).safePath;
|
|
3269
|
+
}
|
|
3270
|
+
catch (e) {
|
|
3271
|
+
if (e instanceof SafePathError) {
|
|
3272
|
+
if (e.code === "PATH_OUTSIDE_SCOPE") {
|
|
3273
|
+
return new ToolResult({
|
|
3274
|
+
content: "Error: call file path must be within SESSION_ARTIFACTS.\n" +
|
|
3275
|
+
`The 'file' parameter is resolved relative to SESSION_ARTIFACTS (${artifactsDir}).`,
|
|
3276
|
+
});
|
|
3277
|
+
}
|
|
3278
|
+
if (e.code === "PATH_SYMLINK_ESCAPES_SCOPE") {
|
|
3279
|
+
return new ToolResult({
|
|
3280
|
+
content: "Error: call file path escapes SESSION_ARTIFACTS via a symbolic link.\n" +
|
|
3281
|
+
`The 'file' parameter is resolved relative to SESSION_ARTIFACTS (${artifactsDir}).`,
|
|
3282
|
+
});
|
|
3283
|
+
}
|
|
3284
|
+
if (e.code === "PATH_NOT_FOUND" || e.code === "PATH_NOT_FILE") {
|
|
3285
|
+
const candidatePath = e.details.resolvedPath || join(artifactsDir, fileRel);
|
|
3286
|
+
return new ToolResult({
|
|
3287
|
+
content: `Error: call file not found at ${candidatePath}\n` +
|
|
3288
|
+
`The 'file' parameter is resolved relative to SESSION_ARTIFACTS (${artifactsDir}).\n` +
|
|
3289
|
+
`Make sure you wrote the call file to this directory using write_file(path="${join(artifactsDir, fileRel)}").`,
|
|
3290
|
+
});
|
|
3291
|
+
}
|
|
3292
|
+
return new ToolResult({ content: `Error: invalid call file path: ${e.message}` });
|
|
3293
|
+
}
|
|
3294
|
+
throw e;
|
|
3295
|
+
}
|
|
3296
|
+
let callFile;
|
|
3297
|
+
try {
|
|
3298
|
+
callFile = yaml.load(readFileSync(filePath, "utf-8"));
|
|
3299
|
+
}
|
|
3300
|
+
catch (e) {
|
|
3301
|
+
return new ToolResult({ content: `Error: failed to parse call file: ${e}` });
|
|
3302
|
+
}
|
|
3303
|
+
if (!callFile || typeof callFile !== "object") {
|
|
3304
|
+
return new ToolResult({ content: "Error: call file must be a YAML mapping." });
|
|
3305
|
+
}
|
|
3306
|
+
// Warn about deprecated inline templates section
|
|
3307
|
+
if (callFile["templates"]) {
|
|
3308
|
+
console.warn("spawn_agent: 'templates:' section in call files is deprecated. " +
|
|
3309
|
+
"Use 'template:' (pre-defined) or 'template_path:' (custom) per task instead.");
|
|
3310
|
+
}
|
|
3311
|
+
const tasksSpec = callFile["tasks"] ?? [];
|
|
3312
|
+
if (!tasksSpec.length) {
|
|
3313
|
+
return new ToolResult({ content: "Error: call file has no 'tasks' section." });
|
|
3314
|
+
}
|
|
3315
|
+
const spawned = [];
|
|
3316
|
+
const spawnedInfo = [];
|
|
3317
|
+
const errors = [];
|
|
3318
|
+
for (const spec of tasksSpec) {
|
|
3319
|
+
const taskId = (spec["id"] ?? "").trim();
|
|
3320
|
+
const templateName = (spec["template"] ?? "").trim();
|
|
3321
|
+
const templatePath = (spec["template_path"] ?? "").trim();
|
|
3322
|
+
const taskDesc = (spec["task"] ?? "").trim();
|
|
3323
|
+
const includeLog = spec["include_important_log"] !== false;
|
|
3324
|
+
if (!taskId || !taskDesc) {
|
|
3325
|
+
errors.push("Skipped entry: missing 'id' or 'task'.");
|
|
3326
|
+
continue;
|
|
3327
|
+
}
|
|
3328
|
+
if (!templateName && !templatePath) {
|
|
3329
|
+
errors.push(`'${taskId}': must specify either 'template' or 'template_path'.`);
|
|
3330
|
+
continue;
|
|
3331
|
+
}
|
|
3332
|
+
if (templateName && templatePath) {
|
|
3333
|
+
errors.push(`'${taskId}': cannot specify both 'template' and 'template_path'.`);
|
|
3334
|
+
continue;
|
|
3335
|
+
}
|
|
3336
|
+
if (this._activeAgents.has(taskId)) {
|
|
3337
|
+
errors.push(`'${taskId}': already running.`);
|
|
3338
|
+
continue;
|
|
3339
|
+
}
|
|
3340
|
+
let agent;
|
|
3341
|
+
let templateLabel;
|
|
3342
|
+
try {
|
|
3343
|
+
if (templateName) {
|
|
3344
|
+
agent = this._createSubAgentFromPredefined(templateName, taskId);
|
|
3345
|
+
templateLabel = templateName;
|
|
3346
|
+
}
|
|
3347
|
+
else {
|
|
3348
|
+
const resolvedPath = this._resolveTemplatePath(templatePath);
|
|
3349
|
+
agent = this._createSubAgentFromPath(resolvedPath, taskId);
|
|
3350
|
+
templateLabel = templatePath;
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
catch (e) {
|
|
3354
|
+
errors.push(`'${taskId}': ${e}`);
|
|
3355
|
+
continue;
|
|
3356
|
+
}
|
|
3357
|
+
const extraMessages = this._buildSubAgentContext(includeLog);
|
|
3358
|
+
this._subAgentCounter += 1;
|
|
3359
|
+
const numericId = this._subAgentCounter;
|
|
3360
|
+
const abortController = new AbortController();
|
|
3361
|
+
const promise = this._runSubAgent(taskId, agent, taskDesc, numericId, extraMessages, abortController.signal);
|
|
3362
|
+
this._activeAgents.set(taskId, {
|
|
3363
|
+
promise,
|
|
3364
|
+
abortController,
|
|
3365
|
+
numericId,
|
|
3366
|
+
template: templateLabel,
|
|
3367
|
+
startTime: performance.now(),
|
|
3368
|
+
status: "working",
|
|
3369
|
+
resultText: "",
|
|
3370
|
+
elapsed: 0,
|
|
3371
|
+
delivered: false,
|
|
3372
|
+
phase: "idle",
|
|
3373
|
+
recentActivity: [],
|
|
3374
|
+
toolCallCount: 0,
|
|
3375
|
+
});
|
|
3376
|
+
spawned.push(taskId);
|
|
3377
|
+
spawnedInfo.push({ numericId, taskId, template: templateLabel, task: taskDesc });
|
|
3378
|
+
if (this._progress) {
|
|
3379
|
+
this._progress.onAgentStart(this._turnCount, taskId, { sub_agent_id: numericId, template: templateLabel });
|
|
3380
|
+
}
|
|
3381
|
+
// v2 log: sub_agent_start
|
|
3382
|
+
this._appendEntry(createSubAgentStart(this._nextLogId("sub_agent_start"), this._turnCount, `Sub-agent #${numericId} (${taskId}) started`, numericId, taskId, taskDesc), false);
|
|
3383
|
+
}
|
|
3384
|
+
const parts = [];
|
|
3385
|
+
if (spawned.length) {
|
|
3386
|
+
parts.push(`Spawned ${spawned.length} sub-agent(s): ${spawned.join(", ")}. ` +
|
|
3387
|
+
"Results will be delivered as each agent completes.");
|
|
3388
|
+
}
|
|
3389
|
+
if (errors.length) {
|
|
3390
|
+
parts.push("Errors: " + errors.join(" | "));
|
|
3391
|
+
}
|
|
3392
|
+
// Build TUI preview: list each sub-agent with truncated task
|
|
3393
|
+
let previewText;
|
|
3394
|
+
if (spawnedInfo.length) {
|
|
3395
|
+
const maxTaskLen = 60;
|
|
3396
|
+
const lines = spawnedInfo.map((info) => {
|
|
3397
|
+
const taskOneLine = info.task.replace(/\s+/g, " ");
|
|
3398
|
+
const taskTrunc = taskOneLine.length > maxTaskLen
|
|
3399
|
+
? taskOneLine.slice(0, maxTaskLen - 1) + "…"
|
|
3400
|
+
: taskOneLine;
|
|
3401
|
+
return ` #${info.numericId} ${info.taskId} [${info.template}] — ${taskTrunc}`;
|
|
3402
|
+
});
|
|
3403
|
+
previewText = `Spawned ${spawnedInfo.length} sub-agent(s):\n${lines.join("\n")}`;
|
|
3404
|
+
}
|
|
3405
|
+
return new ToolResult({
|
|
3406
|
+
content: parts.join("\n") || "No agents spawned.",
|
|
3407
|
+
metadata: previewText ? { tui_preview: { text: previewText, dim: true } } : undefined,
|
|
3408
|
+
});
|
|
3409
|
+
}
|
|
3410
|
+
_execKillAgent(args) {
|
|
3411
|
+
const idsArg = this._argRequiredStringArray("kill_agent", args, "ids");
|
|
3412
|
+
if (idsArg instanceof ToolResult)
|
|
3413
|
+
return idsArg;
|
|
3414
|
+
const ids = idsArg;
|
|
3415
|
+
if (!ids.length) {
|
|
3416
|
+
return new ToolResult({ content: "No agent IDs specified." });
|
|
3417
|
+
}
|
|
3418
|
+
const killed = [];
|
|
3419
|
+
const notFound = [];
|
|
3420
|
+
for (const name of ids) {
|
|
3421
|
+
const entry = this._activeAgents.get(name);
|
|
3422
|
+
if (!entry) {
|
|
3423
|
+
notFound.push(name);
|
|
3424
|
+
continue;
|
|
3425
|
+
}
|
|
3426
|
+
this._activeAgents.delete(name);
|
|
3427
|
+
entry.abortController.abort();
|
|
3428
|
+
killed.push(name);
|
|
3429
|
+
if (this._progress) {
|
|
3430
|
+
this._progress.emit({
|
|
3431
|
+
step: this._turnCount,
|
|
3432
|
+
agent: name,
|
|
3433
|
+
action: "agent_killed",
|
|
3434
|
+
message: ` [#${entry.numericId} ${name}] killed`,
|
|
3435
|
+
level: "normal",
|
|
3436
|
+
timestamp: Date.now() / 1000,
|
|
3437
|
+
usage: {},
|
|
3438
|
+
extra: { sub_agent_id: entry.numericId },
|
|
3439
|
+
});
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
const parts = [];
|
|
3443
|
+
if (killed.length)
|
|
3444
|
+
parts.push(`Killed: ${killed.join(", ")}.`);
|
|
3445
|
+
if (notFound.length)
|
|
3446
|
+
parts.push(`Not found (may have already completed): ${notFound.join(", ")}.`);
|
|
3447
|
+
return new ToolResult({ content: parts.join(" ") });
|
|
3448
|
+
}
|
|
3449
|
+
async _execCheckStatus(_args) {
|
|
3450
|
+
// Non-blocking sweep: check if any working agents have settled
|
|
3451
|
+
this._sweepSettledAgents();
|
|
3452
|
+
// Unified delivery: drain queue + build agent report (marks delivered)
|
|
3453
|
+
const content = this._buildDeliveryContent();
|
|
3454
|
+
return new ToolResult({ content });
|
|
3455
|
+
}
|
|
3456
|
+
/**
|
|
3457
|
+
* Non-blocking sweep: check if any working agents have settled.
|
|
3458
|
+
*/
|
|
3459
|
+
_sweepSettledAgents() {
|
|
3460
|
+
for (const [, entry] of this._activeAgents) {
|
|
3461
|
+
if (entry.status !== "working")
|
|
3462
|
+
continue;
|
|
3463
|
+
// Zero-delay race to check if promise has settled
|
|
3464
|
+
const settled = Promise.race([
|
|
3465
|
+
entry.promise.then((result) => ({ result, error: undefined }), (error) => ({ result: undefined, error })),
|
|
3466
|
+
new Promise((resolve) => setTimeout(() => resolve("pending"), 0)),
|
|
3467
|
+
]);
|
|
3468
|
+
// Note: this is async but we fire-and-forget since the status update
|
|
3469
|
+
// will be visible on next check. For immediate results, use _execWait.
|
|
3470
|
+
void settled.then((r) => {
|
|
3471
|
+
if (r === "pending")
|
|
3472
|
+
return;
|
|
3473
|
+
const res = r;
|
|
3474
|
+
entry.elapsed = this._getElapsed(entry);
|
|
3475
|
+
if (res.result) {
|
|
3476
|
+
entry.status = res.result.status === "completed" ? "finished" : res.result.status;
|
|
3477
|
+
entry.resultText = res.result.text;
|
|
3478
|
+
}
|
|
3479
|
+
else {
|
|
3480
|
+
entry.status = "error";
|
|
3481
|
+
entry.resultText = `Sub-agent error: ${res.error}`;
|
|
3482
|
+
}
|
|
3483
|
+
});
|
|
3484
|
+
}
|
|
3485
|
+
}
|
|
3486
|
+
// ------------------------------------------------------------------
|
|
3487
|
+
// wait — blocking wait for sub-agent completion or new messages
|
|
3488
|
+
// ------------------------------------------------------------------
|
|
3489
|
+
async _execWait(args) {
|
|
3490
|
+
const secondsRaw = args["seconds"];
|
|
3491
|
+
if (typeof secondsRaw !== "number" || isNaN(secondsRaw)) {
|
|
3492
|
+
return new ToolResult({ content: "Error: 'seconds' must be a number." });
|
|
3493
|
+
}
|
|
3494
|
+
const seconds = Math.max(15, secondsRaw);
|
|
3495
|
+
const agentFilter = typeof args["agent"] === "string" ? args["agent"].trim() : null;
|
|
3496
|
+
const shellFilter = typeof args["shell"] === "string" ? args["shell"].trim() : null;
|
|
3497
|
+
if (agentFilter && shellFilter) {
|
|
3498
|
+
return new ToolResult({ content: "Error: wait accepts either 'agent' or 'shell', not both." });
|
|
3499
|
+
}
|
|
3500
|
+
this._agentState = "waiting";
|
|
3501
|
+
const abortPromise = this._makeAbortPromise(this._currentTurnSignal);
|
|
3502
|
+
const throwIfTurnAborted = () => {
|
|
3503
|
+
this._waitResolver = null;
|
|
3504
|
+
this._agentState = "working";
|
|
3505
|
+
throw new DOMException("The operation was aborted.", "AbortError");
|
|
3506
|
+
};
|
|
3507
|
+
if (this._currentTurnSignal?.aborted) {
|
|
3508
|
+
throwIfTurnAborted();
|
|
3509
|
+
}
|
|
3510
|
+
if (this._activeAgents.size === 0 && !this._hasTrackedShells() && !this._hasQueuedMessages()) {
|
|
3511
|
+
this._agentState = "working";
|
|
3512
|
+
return new ToolResult({ content: "No tracked workers and no messages queued." });
|
|
3513
|
+
}
|
|
3514
|
+
// Validate agent filter if specified
|
|
3515
|
+
if (agentFilter) {
|
|
3516
|
+
const targetEntry = this._activeAgents.get(agentFilter);
|
|
3517
|
+
if (!targetEntry) {
|
|
3518
|
+
this._agentState = "working";
|
|
3519
|
+
return new ToolResult({
|
|
3520
|
+
content: `Error: agent '${agentFilter}' not found. Use check_status to see current agents.`,
|
|
3521
|
+
});
|
|
3522
|
+
}
|
|
3523
|
+
if (targetEntry.status !== "working") {
|
|
3524
|
+
// Agent already done — return status immediately
|
|
3525
|
+
this._agentState = "working";
|
|
3526
|
+
const content = this._buildDeliveryContent();
|
|
3527
|
+
return new ToolResult({ content });
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
if (shellFilter) {
|
|
3531
|
+
const targetShell = this._activeShells.get(shellFilter);
|
|
3532
|
+
if (!targetShell) {
|
|
3533
|
+
this._agentState = "working";
|
|
3534
|
+
return new ToolResult({
|
|
3535
|
+
content: `Error: shell '${shellFilter}' not found. Use check_status to see current shells.`,
|
|
3536
|
+
});
|
|
3537
|
+
}
|
|
3538
|
+
if (targetShell.status !== "running") {
|
|
3539
|
+
this._agentState = "working";
|
|
3540
|
+
const content = this._buildDeliveryContent();
|
|
3541
|
+
return new ToolResult({ content });
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
// Collect working agents
|
|
3545
|
+
const working = [];
|
|
3546
|
+
for (const [n, entry] of this._activeAgents) {
|
|
3547
|
+
if (entry.status === "working") {
|
|
3548
|
+
working.push({ name: n, entry });
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
const hasRunningShells = this._hasRunningShells();
|
|
3552
|
+
if (!working.length && !hasRunningShells) {
|
|
3553
|
+
this._agentState = "working";
|
|
3554
|
+
const content = this._buildDeliveryContent();
|
|
3555
|
+
return new ToolResult({ content });
|
|
3556
|
+
}
|
|
3557
|
+
// Helper: settle one entry from its promise result
|
|
3558
|
+
const settleEntry = (entryName, result, error) => {
|
|
3559
|
+
const entry = this._activeAgents.get(entryName);
|
|
3560
|
+
if (!entry)
|
|
3561
|
+
return;
|
|
3562
|
+
entry.elapsed = this._getElapsed(entry);
|
|
3563
|
+
if (result) {
|
|
3564
|
+
entry.status = result.status === "completed" ? "finished" : result.status;
|
|
3565
|
+
entry.resultText = result.text;
|
|
3566
|
+
}
|
|
3567
|
+
else {
|
|
3568
|
+
entry.status = "error";
|
|
3569
|
+
entry.resultText = `Sub-agent error: ${error}`;
|
|
3570
|
+
}
|
|
3571
|
+
// v2 log: sub_agent_end
|
|
3572
|
+
const elapsedSec = entry.elapsed;
|
|
3573
|
+
const statusStr = entry.status === "finished" ? "completed" : "errored";
|
|
3574
|
+
this._appendEntry(createSubAgentEnd(this._nextLogId("sub_agent_end"), this._turnCount, `Sub-agent #${entry.numericId} (${entryName}) ${statusStr} (${elapsedSec.toFixed(1)}s, ${entry.toolCallCount} tool calls)`, entry.numericId, entryName, elapsedSec, entry.toolCallCount), false);
|
|
3575
|
+
};
|
|
3576
|
+
// Create message wake-up promise
|
|
3577
|
+
const messageWake = new Promise((resolve) => {
|
|
3578
|
+
this._waitResolver = () => resolve("message");
|
|
3579
|
+
});
|
|
3580
|
+
let wakeReason = "timeout";
|
|
3581
|
+
const activeAgentRacers = () => working
|
|
3582
|
+
.filter((w) => {
|
|
3583
|
+
const e = this._activeAgents.get(w.name);
|
|
3584
|
+
return e && e.status === "working";
|
|
3585
|
+
})
|
|
3586
|
+
.map(({ name: n, entry: ent }) => ent.promise.then((result) => ({ name: n, result, error: undefined }), (error) => ({ name: n, result: undefined, error })));
|
|
3587
|
+
if (agentFilter) {
|
|
3588
|
+
// Work-time mode: poll until target agent accumulates enough wall time
|
|
3589
|
+
const POLL_INTERVAL = 1000;
|
|
3590
|
+
const timeoutMs = seconds * 1000;
|
|
3591
|
+
while (true) {
|
|
3592
|
+
const currentEntry = this._activeAgents.get(agentFilter);
|
|
3593
|
+
if (!currentEntry || currentEntry.status !== "working") {
|
|
3594
|
+
wakeReason = "agent";
|
|
3595
|
+
break;
|
|
3596
|
+
}
|
|
3597
|
+
const workMs = (performance.now() - currentEntry.startTime);
|
|
3598
|
+
if (workMs >= timeoutMs) {
|
|
3599
|
+
break; // Reached target work time
|
|
3600
|
+
}
|
|
3601
|
+
// Race all working promises against a short poll interval + message wake
|
|
3602
|
+
const racers = activeAgentRacers();
|
|
3603
|
+
if (!racers.length)
|
|
3604
|
+
break;
|
|
3605
|
+
const poll = new Promise((resolve) => setTimeout(() => resolve("poll"), POLL_INTERVAL));
|
|
3606
|
+
const winner = await Promise.race([
|
|
3607
|
+
...racers,
|
|
3608
|
+
poll,
|
|
3609
|
+
messageWake,
|
|
3610
|
+
...(abortPromise ? [abortPromise] : []),
|
|
3611
|
+
]);
|
|
3612
|
+
if (winner === "aborted") {
|
|
3613
|
+
throwIfTurnAborted();
|
|
3614
|
+
}
|
|
3615
|
+
if (winner === "message") {
|
|
3616
|
+
wakeReason = "message";
|
|
3617
|
+
break;
|
|
3618
|
+
}
|
|
3619
|
+
if (winner !== "poll") {
|
|
3620
|
+
const settled = winner;
|
|
3621
|
+
settleEntry(settled.name, settled.result, settled.error);
|
|
3622
|
+
wakeReason = "agent";
|
|
3623
|
+
break;
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
else if (shellFilter) {
|
|
3628
|
+
const POLL_INTERVAL = 1000;
|
|
3629
|
+
const timeoutMs = seconds * 1000;
|
|
3630
|
+
const started = performance.now();
|
|
3631
|
+
while (true) {
|
|
3632
|
+
const currentShell = this._activeShells.get(shellFilter);
|
|
3633
|
+
if (!currentShell || currentShell.status !== "running") {
|
|
3634
|
+
wakeReason = "shell";
|
|
3635
|
+
break;
|
|
3636
|
+
}
|
|
3637
|
+
if ((performance.now() - started) >= timeoutMs) {
|
|
3638
|
+
break;
|
|
3639
|
+
}
|
|
3640
|
+
const poll = new Promise((resolve) => setTimeout(() => resolve("poll"), POLL_INTERVAL));
|
|
3641
|
+
const winner = await Promise.race([
|
|
3642
|
+
...activeAgentRacers(),
|
|
3643
|
+
poll,
|
|
3644
|
+
messageWake,
|
|
3645
|
+
...(abortPromise ? [abortPromise] : []),
|
|
3646
|
+
]);
|
|
3647
|
+
if (winner === "aborted") {
|
|
3648
|
+
throwIfTurnAborted();
|
|
3649
|
+
}
|
|
3650
|
+
if (winner === "message") {
|
|
3651
|
+
const currentShell = this._activeShells.get(shellFilter);
|
|
3652
|
+
wakeReason = !currentShell || currentShell.status !== "running" ? "shell" : "message";
|
|
3653
|
+
break;
|
|
3654
|
+
}
|
|
3655
|
+
if (winner !== "poll") {
|
|
3656
|
+
const settled = winner;
|
|
3657
|
+
settleEntry(settled.name, settled.result, settled.error);
|
|
3658
|
+
wakeReason = "agent";
|
|
3659
|
+
break;
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
else {
|
|
3664
|
+
// Wall-clock mode: simple race with sleep + message wake
|
|
3665
|
+
const timeout = new Promise((resolve) => setTimeout(() => resolve("timeout"), seconds * 1000));
|
|
3666
|
+
const racers = activeAgentRacers();
|
|
3667
|
+
const winner = await Promise.race([
|
|
3668
|
+
...racers,
|
|
3669
|
+
timeout,
|
|
3670
|
+
messageWake,
|
|
3671
|
+
...(abortPromise ? [abortPromise] : []),
|
|
3672
|
+
]);
|
|
3673
|
+
if (winner === "aborted") {
|
|
3674
|
+
throwIfTurnAborted();
|
|
3675
|
+
}
|
|
3676
|
+
if (winner === "message") {
|
|
3677
|
+
wakeReason = "message";
|
|
3678
|
+
}
|
|
3679
|
+
else if (winner !== "timeout") {
|
|
3680
|
+
const settled = winner;
|
|
3681
|
+
settleEntry(settled.name, settled.result, settled.error);
|
|
3682
|
+
wakeReason = "agent";
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
// Cleanup wait resolver
|
|
3686
|
+
this._waitResolver = null;
|
|
3687
|
+
// Non-blocking sweep: check if other working agents have also settled
|
|
3688
|
+
for (const [, entry] of this._activeAgents) {
|
|
3689
|
+
if (entry.status !== "working")
|
|
3690
|
+
continue;
|
|
3691
|
+
const zeroTimeout = new Promise((resolve) => setTimeout(() => resolve("pending"), 0));
|
|
3692
|
+
const check = entry.promise.then((result) => ({ result, error: undefined }), (error) => ({ result: undefined, error }));
|
|
3693
|
+
const r = await Promise.race([check, zeroTimeout]);
|
|
3694
|
+
if (r !== "pending") {
|
|
3695
|
+
const res = r;
|
|
3696
|
+
entry.elapsed = this._getElapsed(entry);
|
|
3697
|
+
if (res.result) {
|
|
3698
|
+
entry.status = res.result.status === "completed" ? "finished" : res.result.status;
|
|
3699
|
+
entry.resultText = res.result.text;
|
|
3700
|
+
}
|
|
3701
|
+
else {
|
|
3702
|
+
entry.status = "error";
|
|
3703
|
+
entry.resultText = `Sub-agent error: ${res.error}`;
|
|
3704
|
+
}
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
this._agentState = "working";
|
|
3708
|
+
// Build return value with unified delivery content
|
|
3709
|
+
const hasNewContent = this._hasQueuedMessages() || this._hasUndeliveredAgentResults();
|
|
3710
|
+
let header;
|
|
3711
|
+
if (wakeReason === "message") {
|
|
3712
|
+
header = `Waited — new message arrived.`;
|
|
3713
|
+
}
|
|
3714
|
+
else if (wakeReason === "agent") {
|
|
3715
|
+
header = `Waited — agent completed.`;
|
|
3716
|
+
}
|
|
3717
|
+
else if (wakeReason === "shell") {
|
|
3718
|
+
header = `Waited — shell exited.`;
|
|
3719
|
+
}
|
|
3720
|
+
else if (hasNewContent) {
|
|
3721
|
+
header = `Waited ${seconds}s. New event arrived during wait.`;
|
|
3722
|
+
}
|
|
3723
|
+
else {
|
|
3724
|
+
header = `Waited ${seconds}s. No new event arrived during this period.`;
|
|
3725
|
+
}
|
|
3726
|
+
const deliveryContent = this._buildDeliveryContent();
|
|
3727
|
+
return new ToolResult({ content: header + "\n\n" + deliveryContent });
|
|
3728
|
+
}
|
|
3729
|
+
// ------------------------------------------------------------------
|
|
3730
|
+
// Elapsed helpers
|
|
3731
|
+
// ------------------------------------------------------------------
|
|
3732
|
+
_getElapsed(entry) {
|
|
3733
|
+
return (performance.now() - entry.startTime) / 1000;
|
|
3734
|
+
}
|
|
3735
|
+
// ------------------------------------------------------------------
|
|
3736
|
+
// Agent report — built at consumption time (check_status, wait,
|
|
3737
|
+
// activation boundary injection). Sets delivered=true only here.
|
|
3738
|
+
// ------------------------------------------------------------------
|
|
3739
|
+
_buildAgentReport() {
|
|
3740
|
+
const statusLines = [];
|
|
3741
|
+
const newResultParts = [];
|
|
3742
|
+
for (const [name, entry] of this._activeAgents) {
|
|
3743
|
+
if (entry.status === "working") {
|
|
3744
|
+
const workSec = this._getElapsed(entry);
|
|
3745
|
+
let line = `- [#${entry.numericId} ${name}] (${entry.template}): working (${workSec.toFixed(1)}s)`;
|
|
3746
|
+
line += ` | ${entry.toolCallCount} tools called`;
|
|
3747
|
+
if (entry.recentActivity.length > 0) {
|
|
3748
|
+
line += "\n recent: " + entry.recentActivity.join(" → ");
|
|
3749
|
+
}
|
|
3750
|
+
statusLines.push(line);
|
|
3751
|
+
}
|
|
3752
|
+
else if ((entry.status === "finished" || entry.status === "error") &&
|
|
3753
|
+
!entry.delivered) {
|
|
3754
|
+
statusLines.push(`- [#${entry.numericId} ${name}] (${entry.template}): ${entry.status} (took ${entry.elapsed.toFixed(1)}s) [result below]`);
|
|
3755
|
+
const resultDict = {
|
|
3756
|
+
name,
|
|
3757
|
+
status: entry.status,
|
|
3758
|
+
text: entry.resultText,
|
|
3759
|
+
elapsed: entry.elapsed,
|
|
3760
|
+
};
|
|
3761
|
+
newResultParts.push(this._formatAgentOutput(resultDict));
|
|
3762
|
+
entry.delivered = true; // ★ marked at consumption time
|
|
3763
|
+
}
|
|
3764
|
+
else if (entry.delivered) {
|
|
3765
|
+
statusLines.push(`- [#${entry.numericId} ${name}] (${entry.template}): ${entry.status} (took ${entry.elapsed.toFixed(1)}s) [result already consumed]`);
|
|
3766
|
+
}
|
|
3767
|
+
else if (entry.status === "killed") {
|
|
3768
|
+
statusLines.push(`- [#${entry.numericId} ${name}] (${entry.template}): killed`);
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
let output = "## Agent Status\n" + statusLines.join("\n");
|
|
3772
|
+
if (newResultParts.length > 0) {
|
|
3773
|
+
output += "\n\n## New Results (" + newResultParts.length + ")\n\n" + newResultParts.join("\n\n---\n\n");
|
|
3774
|
+
}
|
|
3775
|
+
// Hint about still-working agents
|
|
3776
|
+
let workingCount = 0;
|
|
3777
|
+
for (const entry of this._activeAgents.values()) {
|
|
3778
|
+
if (entry.status === "working")
|
|
3779
|
+
workingCount++;
|
|
3780
|
+
}
|
|
3781
|
+
if (workingCount > 0) {
|
|
3782
|
+
output +=
|
|
3783
|
+
`\n\n(${workingCount} agent(s) still working. ` +
|
|
3784
|
+
"Use wait to wait efficiently, or continue working with tools.)";
|
|
3785
|
+
}
|
|
3786
|
+
return output;
|
|
3787
|
+
}
|
|
3788
|
+
_hasActiveAgents() {
|
|
3789
|
+
for (const entry of this._activeAgents.values()) {
|
|
3790
|
+
if (entry.status === "working")
|
|
3791
|
+
return true;
|
|
3792
|
+
}
|
|
3793
|
+
return false;
|
|
3794
|
+
}
|
|
3795
|
+
_forceKillAllAgents() {
|
|
3796
|
+
for (const [name, entry] of this._activeAgents) {
|
|
3797
|
+
if (entry.status === "working") {
|
|
3798
|
+
entry.abortController.abort();
|
|
3799
|
+
if (this._progress) {
|
|
3800
|
+
this._progress.emit({
|
|
3801
|
+
step: this._turnCount,
|
|
3802
|
+
agent: name,
|
|
3803
|
+
action: "agent_killed",
|
|
3804
|
+
message: ` [#${entry.numericId} ${name}] killed`,
|
|
3805
|
+
level: "normal",
|
|
3806
|
+
timestamp: Date.now() / 1000,
|
|
3807
|
+
usage: {},
|
|
3808
|
+
extra: { sub_agent_id: entry.numericId },
|
|
3809
|
+
});
|
|
3810
|
+
}
|
|
3811
|
+
}
|
|
3812
|
+
}
|
|
3813
|
+
this._activeAgents.clear();
|
|
3814
|
+
}
|
|
3815
|
+
_forceKillAllShells() {
|
|
3816
|
+
for (const entry of this._activeShells.values()) {
|
|
3817
|
+
if (entry.status === "running") {
|
|
3818
|
+
entry.explicitKill = true;
|
|
3819
|
+
try {
|
|
3820
|
+
entry.process.kill("SIGTERM");
|
|
3821
|
+
}
|
|
3822
|
+
catch {
|
|
3823
|
+
// Best-effort cleanup.
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3827
|
+
this._activeShells.clear();
|
|
3828
|
+
}
|
|
3829
|
+
_createSubAgentFromPredefined(templateName, taskId) {
|
|
3830
|
+
// Try exact match first, then case-insensitive fallback
|
|
3831
|
+
let templateAgent = this.agentTemplates[templateName];
|
|
3832
|
+
if (!templateAgent) {
|
|
3833
|
+
const lower = templateName.toLowerCase();
|
|
3834
|
+
for (const [key, agent] of Object.entries(this.agentTemplates)) {
|
|
3835
|
+
if (key.toLowerCase() === lower) {
|
|
3836
|
+
templateAgent = agent;
|
|
3837
|
+
break;
|
|
3838
|
+
}
|
|
3839
|
+
}
|
|
3840
|
+
}
|
|
3841
|
+
if (!templateAgent) {
|
|
3842
|
+
const available = Object.keys(this.agentTemplates).sort();
|
|
3843
|
+
throw new Error(`Unknown template '${templateName}'. Available: ${available.join(", ") || "(none)"}`);
|
|
3844
|
+
}
|
|
3845
|
+
const modelConfig = this._getSubAgentModelConfig();
|
|
3846
|
+
const tools = [...templateAgent.tools]; // Use template's tools, not primary agent's
|
|
3847
|
+
const agent = new Agent({
|
|
3848
|
+
name: taskId,
|
|
3849
|
+
modelConfig,
|
|
3850
|
+
systemPrompt: this._renderSystemPrompt(templateAgent.systemPrompt),
|
|
3851
|
+
tools,
|
|
3852
|
+
maxToolRounds: templateAgent.maxToolRounds,
|
|
3853
|
+
description: `Sub-agent '${taskId}' (${templateName})`,
|
|
3854
|
+
});
|
|
3855
|
+
this._applySubAgentConstraints(agent);
|
|
3856
|
+
return agent;
|
|
3857
|
+
}
|
|
3858
|
+
_createSubAgentFromPath(templateDir, taskId) {
|
|
3859
|
+
const templateAgent = loadTemplate(templateDir, this.config, taskId, this._mcpManager, this._promptsDirs);
|
|
3860
|
+
const modelConfig = this._getSubAgentModelConfig();
|
|
3861
|
+
const agent = new Agent({
|
|
3862
|
+
name: taskId,
|
|
3863
|
+
modelConfig,
|
|
3864
|
+
systemPrompt: this._renderSystemPrompt(templateAgent.systemPrompt),
|
|
3865
|
+
tools: [...templateAgent.tools],
|
|
3866
|
+
maxToolRounds: templateAgent.maxToolRounds,
|
|
3867
|
+
description: `Sub-agent '${taskId}' (custom)`,
|
|
3868
|
+
});
|
|
3869
|
+
this._applySubAgentConstraints(agent);
|
|
3870
|
+
return agent;
|
|
3871
|
+
}
|
|
3872
|
+
_resolveTemplatePath(relPath) {
|
|
3873
|
+
const artifactsDir = this._resolveSessionArtifacts();
|
|
3874
|
+
let absPath;
|
|
3875
|
+
try {
|
|
3876
|
+
absPath = safePath({
|
|
3877
|
+
baseDir: artifactsDir,
|
|
3878
|
+
requestedPath: relPath,
|
|
3879
|
+
cwd: artifactsDir,
|
|
3880
|
+
mustExist: true,
|
|
3881
|
+
expectDirectory: true,
|
|
3882
|
+
accessKind: "template",
|
|
3883
|
+
}).safePath;
|
|
3884
|
+
}
|
|
3885
|
+
catch (e) {
|
|
3886
|
+
if (e instanceof SafePathError) {
|
|
3887
|
+
if (e.code === "PATH_OUTSIDE_SCOPE") {
|
|
3888
|
+
throw new Error("Template path must be within SESSION_ARTIFACTS");
|
|
3889
|
+
}
|
|
3890
|
+
if (e.code === "PATH_SYMLINK_ESCAPES_SCOPE") {
|
|
3891
|
+
throw new Error("Template path escapes SESSION_ARTIFACTS via a symbolic link");
|
|
3892
|
+
}
|
|
3893
|
+
throw new Error(e.message);
|
|
3894
|
+
}
|
|
3895
|
+
throw e;
|
|
3896
|
+
}
|
|
3897
|
+
const validationError = validateTemplate(absPath);
|
|
3898
|
+
if (validationError) {
|
|
3899
|
+
throw new Error(`Template validation failed: ${validationError}`);
|
|
3900
|
+
}
|
|
3901
|
+
return absPath;
|
|
3902
|
+
}
|
|
3903
|
+
_applySubAgentConstraints(agent) {
|
|
3904
|
+
agent.tools = agent.tools.filter((t) => !COMM_TOOL_NAMES.has(t.name));
|
|
3905
|
+
agent.systemPrompt +=
|
|
3906
|
+
"\n\n[SUB-AGENT CONSTRAINTS]\n" +
|
|
3907
|
+
"You are a sub-agent executing a bounded task. Rules:\n" +
|
|
3908
|
+
"- Focus on your assigned task and report findings clearly.\n" +
|
|
3909
|
+
"- Your final output message will be delivered to the primary agent " +
|
|
3910
|
+
"as your result.\n" +
|
|
3911
|
+
" Intermediate tool calls and their results will NOT be visible " +
|
|
3912
|
+
"to the primary agent.\n" +
|
|
3913
|
+
" Make sure your final output contains all relevant findings " +
|
|
3914
|
+
"and conclusions.";
|
|
3915
|
+
}
|
|
3916
|
+
_getSubAgentModelConfig() {
|
|
3917
|
+
const name = this.config.subAgentModelName;
|
|
3918
|
+
if (name)
|
|
3919
|
+
return this.config.getModel(name);
|
|
3920
|
+
return this.primaryAgent.modelConfig;
|
|
3921
|
+
}
|
|
3922
|
+
_buildSubAgentContext(includeImportantLog) {
|
|
3923
|
+
const extra = [];
|
|
3924
|
+
if (includeImportantLog) {
|
|
3925
|
+
const logContent = this._readImportantLog();
|
|
3926
|
+
if (logContent.trim()) {
|
|
3927
|
+
extra.push({
|
|
3928
|
+
role: "user",
|
|
3929
|
+
content: "[IMPORTANT LOG]\n" +
|
|
3930
|
+
"The following is the primary agent's engineering notebook. " +
|
|
3931
|
+
"Use it as background context for your task:\n\n" +
|
|
3932
|
+
logContent,
|
|
3933
|
+
});
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
// Always inject AGENTS.md for sub-agents (persistent memory is always relevant)
|
|
3937
|
+
const agentsMdContent = this._readAgentsMd();
|
|
3938
|
+
if (agentsMdContent.trim()) {
|
|
3939
|
+
extra.push({
|
|
3940
|
+
role: "user",
|
|
3941
|
+
content: "[AGENTS.MD — PERSISTENT MEMORY]\n" +
|
|
3942
|
+
"The following is persistent memory across sessions. " +
|
|
3943
|
+
"Use it as background context for your task:\n\n" +
|
|
3944
|
+
agentsMdContent,
|
|
3945
|
+
});
|
|
3946
|
+
}
|
|
3947
|
+
return extra;
|
|
3948
|
+
}
|
|
3949
|
+
async _runSubAgent(name, agent, task, agentId, extraMessages, signal) {
|
|
3950
|
+
const subExtra = { sub_agent_id: agentId };
|
|
3951
|
+
const MAX_SUB_AGENT_ACTIVATIONS = 15;
|
|
3952
|
+
let onToolCall;
|
|
3953
|
+
let onSubTextChunk;
|
|
3954
|
+
let onSubReasoningChunk;
|
|
3955
|
+
// Track whether phase-signal has been emitted this activation.
|
|
3956
|
+
// Reset on tool_call so subsequent thinking/generating is detected.
|
|
3957
|
+
let reasoningSignalEmitted = false;
|
|
3958
|
+
let textSignalEmitted = false;
|
|
3959
|
+
if (this._progress) {
|
|
3960
|
+
const progress = this._progress;
|
|
3961
|
+
const step = this._turnCount;
|
|
3962
|
+
let subToolCallCount = 0;
|
|
3963
|
+
onToolCall = (agentName, tool, args, summary) => {
|
|
3964
|
+
progress.onToolCall(step, agentName, tool, args, summary, { sub_agent_id: agentId });
|
|
3965
|
+
// Reset phase-signal flags so next thinking/generating is detected
|
|
3966
|
+
reasoningSignalEmitted = false;
|
|
3967
|
+
textSignalEmitted = false;
|
|
3968
|
+
// v2 log: sub_agent_tool_call
|
|
3969
|
+
subToolCallCount++;
|
|
3970
|
+
this._appendEntry(createSubAgentToolCall(this._nextLogId("sub_agent_tool_call"), this._turnCount, `[#${agentId} ${name}] (${subToolCallCount} tool called) -> ${summary}`, agentId, name, tool, subToolCallCount), false);
|
|
3971
|
+
};
|
|
3972
|
+
// Lightweight signal-only callbacks (empty chunk, once per phase)
|
|
3973
|
+
onSubReasoningChunk = (_roundIndex, _chunk) => {
|
|
3974
|
+
if (!reasoningSignalEmitted) {
|
|
3975
|
+
reasoningSignalEmitted = true;
|
|
3976
|
+
textSignalEmitted = false;
|
|
3977
|
+
progress.emit({
|
|
3978
|
+
step, agent: name, action: "reasoning_chunk",
|
|
3979
|
+
message: "", level: "quiet",
|
|
3980
|
+
timestamp: Date.now() / 1000,
|
|
3981
|
+
usage: {}, extra: { chunk: "", sub_agent_id: agentId },
|
|
3982
|
+
});
|
|
3983
|
+
}
|
|
3984
|
+
return false;
|
|
3985
|
+
};
|
|
3986
|
+
onSubTextChunk = (_roundIndex, _chunk) => {
|
|
3987
|
+
if (!textSignalEmitted) {
|
|
3988
|
+
textSignalEmitted = true;
|
|
3989
|
+
reasoningSignalEmitted = false;
|
|
3990
|
+
progress.emit({
|
|
3991
|
+
step, agent: name, action: "text_chunk",
|
|
3992
|
+
message: "", level: "quiet",
|
|
3993
|
+
timestamp: Date.now() / 1000,
|
|
3994
|
+
usage: {}, extra: { chunk: "", sub_agent_id: agentId },
|
|
3995
|
+
});
|
|
3996
|
+
}
|
|
3997
|
+
return false;
|
|
3998
|
+
};
|
|
3999
|
+
}
|
|
4000
|
+
// Wrap callbacks to write back live state to AgentEntry unconditionally
|
|
4001
|
+
// (works even when this._progress is null)
|
|
4002
|
+
const getEntry = () => this._activeAgents.get(name);
|
|
4003
|
+
const origOnToolCall = onToolCall;
|
|
4004
|
+
onToolCall = (ag, tool, args, summary) => {
|
|
4005
|
+
origOnToolCall?.(ag, tool, args, summary);
|
|
4006
|
+
const e = getEntry();
|
|
4007
|
+
if (e) {
|
|
4008
|
+
e.phase = "tool_calling";
|
|
4009
|
+
e.recentActivity.push(summary);
|
|
4010
|
+
if (e.recentActivity.length > 3)
|
|
4011
|
+
e.recentActivity.shift();
|
|
4012
|
+
e.toolCallCount++;
|
|
4013
|
+
}
|
|
4014
|
+
};
|
|
4015
|
+
const origOnReasoningChunk = onSubReasoningChunk;
|
|
4016
|
+
onSubReasoningChunk = (roundIndex, c) => {
|
|
4017
|
+
origOnReasoningChunk?.(roundIndex, c);
|
|
4018
|
+
const e = getEntry();
|
|
4019
|
+
if (e)
|
|
4020
|
+
e.phase = "thinking";
|
|
4021
|
+
return false;
|
|
4022
|
+
};
|
|
4023
|
+
const origOnTextChunk = onSubTextChunk;
|
|
4024
|
+
onSubTextChunk = (roundIndex, c) => {
|
|
4025
|
+
origOnTextChunk?.(roundIndex, c);
|
|
4026
|
+
const e = getEntry();
|
|
4027
|
+
if (e)
|
|
4028
|
+
e.phase = "generating";
|
|
4029
|
+
return false;
|
|
4030
|
+
};
|
|
4031
|
+
const t0 = performance.now();
|
|
4032
|
+
try {
|
|
4033
|
+
// Check abort before starting
|
|
4034
|
+
if (signal?.aborted) {
|
|
4035
|
+
throw new DOMException("Aborted", "AbortError");
|
|
4036
|
+
}
|
|
4037
|
+
// Build sub-agent ephemeral log state
|
|
4038
|
+
const initialMessages = [
|
|
4039
|
+
{ role: "system", content: agent.systemPrompt },
|
|
4040
|
+
];
|
|
4041
|
+
if (extraMessages) {
|
|
4042
|
+
initialMessages.push(...extraMessages);
|
|
4043
|
+
}
|
|
4044
|
+
initialMessages.push({ role: "user", content: task });
|
|
4045
|
+
const runtime = createEphemeralLogState(initialMessages, {
|
|
4046
|
+
requiresAlternatingRoles: agent._provider.requiresAlternatingRoles,
|
|
4047
|
+
});
|
|
4048
|
+
const roundContextIds = new Map();
|
|
4049
|
+
const getSubAgentRoundContextId = (roundIndex) => {
|
|
4050
|
+
let contextId = roundContextIds.get(roundIndex);
|
|
4051
|
+
if (!contextId) {
|
|
4052
|
+
contextId = runtime.allocateContextId();
|
|
4053
|
+
roundContextIds.set(roundIndex, contextId);
|
|
4054
|
+
}
|
|
4055
|
+
return contextId;
|
|
4056
|
+
};
|
|
4057
|
+
// Build sub-agent compact check
|
|
4058
|
+
const compactCheck = this._buildSubAgentCompactCheck(agent);
|
|
4059
|
+
// Pass a small, explicit subset of Session-scoped executors to sub-agents.
|
|
4060
|
+
// This preserves project-root path enforcement for file tools while keeping
|
|
4061
|
+
// comm tools unavailable.
|
|
4062
|
+
const subExecutors = {};
|
|
4063
|
+
for (const toolName of [
|
|
4064
|
+
"$web_search",
|
|
4065
|
+
"read_file",
|
|
4066
|
+
"list_dir",
|
|
4067
|
+
"glob",
|
|
4068
|
+
"grep",
|
|
4069
|
+
"edit_file",
|
|
4070
|
+
"write_file",
|
|
4071
|
+
"diff",
|
|
4072
|
+
"web_fetch",
|
|
4073
|
+
]) {
|
|
4074
|
+
if (this._toolExecutors[toolName]) {
|
|
4075
|
+
subExecutors[toolName] = this._toolExecutors[toolName];
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
let totalUsage = { inputTokens: 0, outputTokens: 0 };
|
|
4079
|
+
let finalText = "";
|
|
4080
|
+
let compactCount = 0;
|
|
4081
|
+
// Activation loop with compact support
|
|
4082
|
+
for (let i = 0; i < MAX_SUB_AGENT_ACTIVATIONS; i++) {
|
|
4083
|
+
if (signal?.aborted) {
|
|
4084
|
+
throw new DOMException("Aborted", "AbortError");
|
|
4085
|
+
}
|
|
4086
|
+
// Reset phase-signal flags at the start of each activation
|
|
4087
|
+
reasoningSignalEmitted = false;
|
|
4088
|
+
textSignalEmitted = false;
|
|
4089
|
+
const subAgentName = agent.name;
|
|
4090
|
+
const result = await agent.asyncRunWithMessages(runtime.getMessages, runtime.appendEntry, runtime.allocId, 0, runtime.computeNextRoundIndex(), subExecutors, onToolCall, onSubTextChunk, onSubReasoningChunk, signal, getSubAgentRoundContextId, compactCheck, undefined, undefined, undefined, undefined, undefined, undefined, false, (attempt, max, delaySec, errMsg) => this._progress?.onRetryAttempt(subAgentName, attempt, max, delaySec, errMsg), (attempt) => this._progress?.onRetrySuccess(subAgentName, attempt), (max, errMsg) => this._progress?.onRetryExhausted(subAgentName, max, errMsg));
|
|
4091
|
+
totalUsage.inputTokens += result.totalUsage.inputTokens;
|
|
4092
|
+
totalUsage.outputTokens += result.totalUsage.outputTokens;
|
|
4093
|
+
if (!result.compactNeeded) {
|
|
4094
|
+
// Normal completion
|
|
4095
|
+
this._appendEphemeralAgentOutput(runtime, result, getSubAgentRoundContextId);
|
|
4096
|
+
if (result.text)
|
|
4097
|
+
finalText = stripContextTags(result.text);
|
|
4098
|
+
break;
|
|
4099
|
+
}
|
|
4100
|
+
// Compact triggered — run compact phase for sub-agent
|
|
4101
|
+
if (result.compactScenario === "output") {
|
|
4102
|
+
this._appendEphemeralAgentOutput(runtime, result, getSubAgentRoundContextId);
|
|
4103
|
+
}
|
|
4104
|
+
const continuation = await this._runSubAgentCompactPhase(agent, runtime, subExecutors, getSubAgentRoundContextId, result.compactScenario ?? "output", onToolCall, signal);
|
|
4105
|
+
// Insert compact marker and reconstruct context
|
|
4106
|
+
compactCount += 1;
|
|
4107
|
+
runtime.appendEntry(createCompactMarker(runtime.allocId("compact_marker"), 0, compactCount - 1, result.lastTotalTokens ?? 0, 0));
|
|
4108
|
+
runtime.appendEntry(createCompactContext(runtime.allocId("compact_context"), 0, continuation + "\n\nContinue from where you left off.", runtime.allocateContextId(), compactCount - 1));
|
|
4109
|
+
}
|
|
4110
|
+
const elapsed = (performance.now() - t0) / 1000;
|
|
4111
|
+
if (signal?.aborted) {
|
|
4112
|
+
throw new DOMException("Aborted", "AbortError");
|
|
4113
|
+
}
|
|
4114
|
+
if (this._progress) {
|
|
4115
|
+
this._progress.onAgentEnd(this._turnCount, name, elapsed, totalUsage, subExtra);
|
|
4116
|
+
}
|
|
4117
|
+
return { name, status: "completed", text: finalText, usage: totalUsage, elapsed };
|
|
4118
|
+
}
|
|
4119
|
+
catch (e) {
|
|
4120
|
+
const elapsed = (performance.now() - t0) / 1000;
|
|
4121
|
+
if (e?.name === "AbortError" || signal?.aborted) {
|
|
4122
|
+
if (this._progress) {
|
|
4123
|
+
this._progress.emit({
|
|
4124
|
+
step: this._turnCount,
|
|
4125
|
+
agent: name,
|
|
4126
|
+
action: "agent_killed",
|
|
4127
|
+
message: ` [#${agentId} ${name}] killed`,
|
|
4128
|
+
level: "normal",
|
|
4129
|
+
timestamp: Date.now() / 1000,
|
|
4130
|
+
usage: {},
|
|
4131
|
+
extra: subExtra,
|
|
4132
|
+
});
|
|
4133
|
+
}
|
|
4134
|
+
return { name, status: "killed", text: "(killed)", usage: {}, elapsed };
|
|
4135
|
+
}
|
|
4136
|
+
console.error(`Sub-agent '${name}' failed:`, e);
|
|
4137
|
+
if (this._progress) {
|
|
4138
|
+
this._progress.emit({
|
|
4139
|
+
step: this._turnCount,
|
|
4140
|
+
agent: name,
|
|
4141
|
+
action: "agent_error",
|
|
4142
|
+
message: ` [#${agentId} ${name}] error: ${e}`,
|
|
4143
|
+
level: "normal",
|
|
4144
|
+
timestamp: Date.now() / 1000,
|
|
4145
|
+
usage: {},
|
|
4146
|
+
extra: subExtra,
|
|
4147
|
+
});
|
|
4148
|
+
}
|
|
4149
|
+
return { name, status: "error", text: `Sub-agent error: ${e}`, usage: {}, elapsed };
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
_appendEphemeralAgentOutput(runtime, result, getRoundContextId) {
|
|
4153
|
+
if (!result.text && !result.reasoningContent)
|
|
4154
|
+
return;
|
|
4155
|
+
const roundIndex = (result.textHandledInLog || result.reasoningHandledInLog)
|
|
4156
|
+
? Math.max(0, runtime.computeNextRoundIndex() - 1)
|
|
4157
|
+
: runtime.computeNextRoundIndex();
|
|
4158
|
+
// Check if this round has tool_call entries (i.e., is NOT text-only).
|
|
4159
|
+
// Text-only final rounds inherit the preceding user-side contextId.
|
|
4160
|
+
const hasToolCallsInRound = runtime.entries.some((e) => e.roundIndex === roundIndex && e.type === "tool_call" && !e.discarded);
|
|
4161
|
+
let contextId;
|
|
4162
|
+
if (hasToolCallsInRound) {
|
|
4163
|
+
contextId = getRoundContextId(roundIndex);
|
|
4164
|
+
}
|
|
4165
|
+
else {
|
|
4166
|
+
// Inherit: find the most recent user-side contextId in the ephemeral log
|
|
4167
|
+
let inherited;
|
|
4168
|
+
for (let i = runtime.entries.length - 1; i >= 0; i--) {
|
|
4169
|
+
const e = runtime.entries[i];
|
|
4170
|
+
if (e.discarded || e.summarized)
|
|
4171
|
+
continue;
|
|
4172
|
+
if (e.apiRole === "user" || e.apiRole === "tool_result") {
|
|
4173
|
+
const cid = e.meta["contextId"];
|
|
4174
|
+
if (typeof cid === "string" && cid.trim()) {
|
|
4175
|
+
inherited = cid;
|
|
4176
|
+
break;
|
|
4177
|
+
}
|
|
4178
|
+
}
|
|
4179
|
+
}
|
|
4180
|
+
contextId = inherited ?? getRoundContextId(roundIndex);
|
|
4181
|
+
}
|
|
4182
|
+
if (result.reasoningContent && !result.reasoningHandledInLog) {
|
|
4183
|
+
runtime.appendEntry(createReasoning(runtime.allocId("reasoning"), 0, roundIndex, result.reasoningContent, result.reasoningContent, result.reasoningState, contextId));
|
|
4184
|
+
}
|
|
4185
|
+
if (!result.text || result.textHandledInLog)
|
|
4186
|
+
return;
|
|
4187
|
+
const trimmedText = result.text.trimEnd();
|
|
4188
|
+
const hasNoReply = isNoReply(result.text) || trimmedText.endsWith(NO_REPLY_MARKER);
|
|
4189
|
+
if (hasNoReply) {
|
|
4190
|
+
const precedingText = trimmedText
|
|
4191
|
+
.slice(0, trimmedText.length - NO_REPLY_MARKER.length)
|
|
4192
|
+
.trim();
|
|
4193
|
+
runtime.appendEntry(createNoReply(runtime.allocId("no_reply"), 0, roundIndex, precedingText || "<NO_REPLY>", contextId));
|
|
4194
|
+
return;
|
|
4195
|
+
}
|
|
4196
|
+
const cleanText = stripContextTags(result.text);
|
|
4197
|
+
runtime.appendEntry(createAssistantText(runtime.allocId("assistant_text"), 0, roundIndex, cleanText, cleanText, contextId));
|
|
4198
|
+
}
|
|
4199
|
+
/**
|
|
4200
|
+
* Build a compact check callback for sub-agents.
|
|
4201
|
+
* Similar to _buildCompactCheck but without sub-agent deferral logic.
|
|
4202
|
+
*/
|
|
4203
|
+
_buildSubAgentCompactCheck(agent) {
|
|
4204
|
+
const mc = agent.modelConfig;
|
|
4205
|
+
const provider = agent._provider;
|
|
4206
|
+
const effectiveMax = this._effectiveMaxTokens(mc);
|
|
4207
|
+
const budget = provider.budgetCalcMode === "full_context"
|
|
4208
|
+
? mc.contextLength
|
|
4209
|
+
: mc.contextLength - effectiveMax;
|
|
4210
|
+
if (budget <= 0)
|
|
4211
|
+
return undefined;
|
|
4212
|
+
const compactOutputRatio = this._thresholds.compact_output / 100;
|
|
4213
|
+
const compactToolcallRatio = this._thresholds.compact_toolcall / 100;
|
|
4214
|
+
return (inputTokens, outputTokens, hasToolCalls) => {
|
|
4215
|
+
const tokensToCheck = provider.budgetCalcMode === "full_context"
|
|
4216
|
+
? inputTokens
|
|
4217
|
+
: inputTokens + outputTokens;
|
|
4218
|
+
const threshold = hasToolCalls ? compactToolcallRatio : compactOutputRatio;
|
|
4219
|
+
if (tokensToCheck > threshold * budget) {
|
|
4220
|
+
return { compactNeeded: true, scenario: hasToolCalls ? "toolcall" : "output" };
|
|
4221
|
+
}
|
|
4222
|
+
return { compactNeeded: false };
|
|
4223
|
+
};
|
|
4224
|
+
}
|
|
4225
|
+
/**
|
|
4226
|
+
* Run compact phase for a sub-agent: inject compact prompt, let agent produce
|
|
4227
|
+
* a continuation prompt (possibly using tools), then return it.
|
|
4228
|
+
* Simplified version — does not inject important log or phase plan.
|
|
4229
|
+
*/
|
|
4230
|
+
async _runSubAgentCompactPhase(agent, runtime, subExecutors, getRoundContextId, scenario, onToolCall, signal) {
|
|
4231
|
+
const prompt = scenario === "output" ? SUB_AGENT_COMPACT_PROMPT_OUTPUT : SUB_AGENT_COMPACT_PROMPT_TOOLCALL;
|
|
4232
|
+
runtime.appendEntry(createUserMessageEntry(runtime.allocId("user_message"), 0, "", prompt, runtime.allocateContextId()));
|
|
4233
|
+
let continuationPrompt = "";
|
|
4234
|
+
for (let i = 0; i < MAX_COMPACT_PHASE_ROUNDS; i++) {
|
|
4235
|
+
if (signal?.aborted)
|
|
4236
|
+
break;
|
|
4237
|
+
const compactAgentName = agent.name;
|
|
4238
|
+
const result = await agent.asyncRunWithMessages(runtime.getMessages, runtime.appendEntry, runtime.allocId, 0, runtime.computeNextRoundIndex(), subExecutors, onToolCall, undefined, undefined, signal, getRoundContextId, undefined, undefined, undefined, undefined, undefined, undefined, undefined, false, (attempt, max, delaySec, errMsg) => this._progress?.onRetryAttempt(compactAgentName, attempt, max, delaySec, errMsg), (attempt) => this._progress?.onRetrySuccess(compactAgentName, attempt), (max, errMsg) => this._progress?.onRetryExhausted(compactAgentName, max, errMsg));
|
|
4239
|
+
if (result.text) {
|
|
4240
|
+
this._appendEphemeralAgentOutput(runtime, result, getRoundContextId);
|
|
4241
|
+
continuationPrompt = stripContextTags(result.text);
|
|
4242
|
+
break;
|
|
4243
|
+
}
|
|
4244
|
+
}
|
|
4245
|
+
if (!continuationPrompt) {
|
|
4246
|
+
continuationPrompt = "[Compact phase did not produce a continuation prompt.]";
|
|
4247
|
+
}
|
|
4248
|
+
return continuationPrompt;
|
|
4249
|
+
}
|
|
4250
|
+
// -- Result collection & delivery --
|
|
4251
|
+
async _waitForAnyAgent(signal) {
|
|
4252
|
+
const working = [];
|
|
4253
|
+
for (const [name, entry] of this._activeAgents) {
|
|
4254
|
+
if (entry.status === "working") {
|
|
4255
|
+
working.push({ name, entry });
|
|
4256
|
+
}
|
|
4257
|
+
}
|
|
4258
|
+
if (!working.length)
|
|
4259
|
+
return;
|
|
4260
|
+
// Race all working promises + a timeout
|
|
4261
|
+
const timeout = new Promise((resolve) => setTimeout(() => resolve("timeout"), SUB_AGENT_TIMEOUT));
|
|
4262
|
+
// Wrap each promise to return its name on settle
|
|
4263
|
+
const racers = working.map(({ name, entry }) => entry.promise.then((result) => ({ name, result, error: undefined }), (error) => ({ name, result: undefined, error })));
|
|
4264
|
+
const abortPromise = this._makeAbortPromise(signal);
|
|
4265
|
+
const winner = await Promise.race([
|
|
4266
|
+
...racers,
|
|
4267
|
+
timeout,
|
|
4268
|
+
...(abortPromise ? [abortPromise] : []),
|
|
4269
|
+
]);
|
|
4270
|
+
if (winner === "timeout") {
|
|
4271
|
+
// Kill the agent with the most elapsed work time
|
|
4272
|
+
let candidate;
|
|
4273
|
+
for (const w of working) {
|
|
4274
|
+
const workTime = performance.now() - w.entry.startTime;
|
|
4275
|
+
if (!candidate || workTime > candidate.workTime) {
|
|
4276
|
+
candidate = { ...w, workTime };
|
|
4277
|
+
}
|
|
4278
|
+
}
|
|
4279
|
+
if (candidate && candidate.workTime > SUB_AGENT_TIMEOUT) {
|
|
4280
|
+
console.warn(`Sub-agent '${candidate.name}' killed after ${(candidate.workTime / 1000).toFixed(0)}s elapsed time`);
|
|
4281
|
+
this._execKillAgent({ ids: [candidate.name] });
|
|
4282
|
+
}
|
|
4283
|
+
return;
|
|
4284
|
+
}
|
|
4285
|
+
if (winner === "aborted") {
|
|
4286
|
+
return;
|
|
4287
|
+
}
|
|
4288
|
+
// Update the entry that just finished
|
|
4289
|
+
const settled = winner;
|
|
4290
|
+
const entry = this._activeAgents.get(settled.name);
|
|
4291
|
+
if (entry) {
|
|
4292
|
+
entry.elapsed = this._getElapsed(entry);
|
|
4293
|
+
if (settled.result) {
|
|
4294
|
+
entry.status = settled.result.status === "completed" ? "finished" : settled.result.status;
|
|
4295
|
+
entry.resultText = settled.result.text;
|
|
4296
|
+
}
|
|
4297
|
+
else if (settled.error) {
|
|
4298
|
+
entry.status = "error";
|
|
4299
|
+
entry.resultText = `Sub-agent error: ${settled.error}`;
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4302
|
+
// Also check if other agents have settled
|
|
4303
|
+
for (const w of working) {
|
|
4304
|
+
if (w.name === settled.name)
|
|
4305
|
+
continue;
|
|
4306
|
+
const e = this._activeAgents.get(w.name);
|
|
4307
|
+
if (!e || e.status !== "working")
|
|
4308
|
+
continue;
|
|
4309
|
+
// Check with a zero-delay race
|
|
4310
|
+
const zeroTimeout = new Promise((resolve) => setTimeout(() => resolve("pending"), 0));
|
|
4311
|
+
const check = e.promise.then((result) => ({ result, error: undefined }), (error) => ({ result: undefined, error }));
|
|
4312
|
+
const r = await Promise.race([check, zeroTimeout]);
|
|
4313
|
+
if (r !== "pending") {
|
|
4314
|
+
const res = r;
|
|
4315
|
+
e.elapsed = this._getElapsed(e);
|
|
4316
|
+
if (res.result) {
|
|
4317
|
+
e.status = res.result.status === "completed" ? "finished" : res.result.status;
|
|
4318
|
+
e.resultText = res.result.text;
|
|
4319
|
+
}
|
|
4320
|
+
else {
|
|
4321
|
+
e.status = "error";
|
|
4322
|
+
e.resultText = `Sub-agent error: ${res.error}`;
|
|
4323
|
+
}
|
|
4324
|
+
}
|
|
4325
|
+
}
|
|
4326
|
+
}
|
|
4327
|
+
_formatAgentOutput(result) {
|
|
4328
|
+
const name = result["name"];
|
|
4329
|
+
const status = result["status"];
|
|
4330
|
+
const text = result["text"] ?? "";
|
|
4331
|
+
const elapsed = result["elapsed"] ?? 0;
|
|
4332
|
+
const header = `**${name}** [${status}, ${elapsed.toFixed(1)}s]`;
|
|
4333
|
+
if (status !== "finished") {
|
|
4334
|
+
return `${header}\n${text}`;
|
|
4335
|
+
}
|
|
4336
|
+
if (text.length > SUB_AGENT_OUTPUT_LIMIT) {
|
|
4337
|
+
const outputDir = join(this._getArtifactsDir(), "agent-outputs");
|
|
4338
|
+
mkdirSync(outputDir, { recursive: true });
|
|
4339
|
+
const outputPath = join(outputDir, `${name}.md`);
|
|
4340
|
+
writeFileSync(outputPath, text);
|
|
4341
|
+
const truncated = text.slice(0, SUB_AGENT_OUTPUT_LIMIT);
|
|
4342
|
+
const truncatedAtLine = truncated.split("\n").length;
|
|
4343
|
+
return (`${header}\n` +
|
|
4344
|
+
`(Output truncated at ${SUB_AGENT_OUTPUT_LIMIT.toLocaleString()} chars ` +
|
|
4345
|
+
`(line ${truncatedAtLine}). Full output: artifacts/agent-outputs/${name}.md. ` +
|
|
4346
|
+
`Continue reading from line ${truncatedAtLine} with \`read_file(start_line=${truncatedAtLine})\`; ` +
|
|
4347
|
+
`do not reread the portion already received.)\n\n` +
|
|
4348
|
+
truncated);
|
|
4349
|
+
}
|
|
4350
|
+
return `${header}\n${text}`;
|
|
4351
|
+
}
|
|
4352
|
+
// ==================================================================
|
|
4353
|
+
// Image file storage (v2 — image_ref)
|
|
4354
|
+
// ==================================================================
|
|
4355
|
+
_imageCounter = 0;
|
|
4356
|
+
/**
|
|
4357
|
+
* If content is a multimodal array, save inline base64 images to disk
|
|
4358
|
+
* and replace them with image_ref blocks for the log.
|
|
4359
|
+
* Returns the original content if no images, or if session dir is unavailable.
|
|
4360
|
+
*/
|
|
4361
|
+
_extractAndSaveImages(content) {
|
|
4362
|
+
if (typeof content === "string")
|
|
4363
|
+
return content;
|
|
4364
|
+
if (!Array.isArray(content))
|
|
4365
|
+
return content;
|
|
4366
|
+
let hasImage = false;
|
|
4367
|
+
for (const block of content) {
|
|
4368
|
+
if (block["type"] === "image" && block["data"]) {
|
|
4369
|
+
hasImage = true;
|
|
4370
|
+
break;
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
4373
|
+
if (!hasImage)
|
|
4374
|
+
return content;
|
|
4375
|
+
const sessionDir = this._store?.sessionDir;
|
|
4376
|
+
if (!sessionDir)
|
|
4377
|
+
return content; // Can't save without session dir
|
|
4378
|
+
const imagesDir = join(sessionDir, "images");
|
|
4379
|
+
try {
|
|
4380
|
+
mkdirSync(imagesDir, { recursive: true });
|
|
4381
|
+
}
|
|
4382
|
+
catch {
|
|
4383
|
+
return content; // Can't create images dir, keep inline
|
|
4384
|
+
}
|
|
4385
|
+
return content.map((block) => {
|
|
4386
|
+
if (block["type"] !== "image" || !block["data"])
|
|
4387
|
+
return block;
|
|
4388
|
+
const mediaType = block["media_type"] || "image/png";
|
|
4389
|
+
const ext = mediaType.split("/")[1]?.replace("jpeg", "jpg") || "png";
|
|
4390
|
+
let filename = "";
|
|
4391
|
+
let filePath = "";
|
|
4392
|
+
do {
|
|
4393
|
+
this._imageCounter += 1;
|
|
4394
|
+
filename = `img-${String(this._imageCounter).padStart(3, "0")}.${ext}`;
|
|
4395
|
+
filePath = join(imagesDir, filename);
|
|
4396
|
+
} while (existsSync(filePath));
|
|
4397
|
+
try {
|
|
4398
|
+
writeFileSync(filePath, Buffer.from(block["data"], "base64"));
|
|
4399
|
+
}
|
|
4400
|
+
catch {
|
|
4401
|
+
return block; // Write failed, keep inline
|
|
4402
|
+
}
|
|
4403
|
+
return {
|
|
4404
|
+
type: "image_ref",
|
|
4405
|
+
path: `images/${filename}`,
|
|
4406
|
+
media_type: mediaType,
|
|
4407
|
+
};
|
|
4408
|
+
});
|
|
4409
|
+
}
|
|
4410
|
+
/**
|
|
4411
|
+
* Resolve an image_ref path to base64 data for API consumption.
|
|
4412
|
+
* Used by projectToApiMessages to restore image data from files.
|
|
4413
|
+
*/
|
|
4414
|
+
_resolveImageRef(refPath) {
|
|
4415
|
+
const sessionDir = this._store?.sessionDir;
|
|
4416
|
+
if (!sessionDir)
|
|
4417
|
+
return null;
|
|
4418
|
+
const fullPath = join(sessionDir, refPath);
|
|
4419
|
+
try {
|
|
4420
|
+
const data = readFileSync(fullPath);
|
|
4421
|
+
const ext = refPath.split(".").pop() || "png";
|
|
4422
|
+
const mediaTypeMap = {
|
|
4423
|
+
png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg",
|
|
4424
|
+
gif: "image/gif", webp: "image/webp", bmp: "image/bmp",
|
|
4425
|
+
};
|
|
4426
|
+
return {
|
|
4427
|
+
data: data.toString("base64"),
|
|
4428
|
+
media_type: mediaTypeMap[ext] || "image/png",
|
|
4429
|
+
};
|
|
4430
|
+
}
|
|
4431
|
+
catch {
|
|
4432
|
+
return null;
|
|
4433
|
+
}
|
|
4434
|
+
}
|
|
4435
|
+
// ==================================================================
|
|
4436
|
+
// @file attachment processing
|
|
4437
|
+
// ==================================================================
|
|
4438
|
+
async _processFileAttachments(userInput) {
|
|
4439
|
+
const supportsMultimodal = this.primaryAgent.modelConfig.supportsMultimodal;
|
|
4440
|
+
const [, refs] = parseReferences(userInput);
|
|
4441
|
+
const explicitAttachmentRoots = new Set();
|
|
4442
|
+
for (const raw of refs) {
|
|
4443
|
+
if (!raw || typeof raw !== "string")
|
|
4444
|
+
continue;
|
|
4445
|
+
try {
|
|
4446
|
+
safePath({
|
|
4447
|
+
baseDir: this._projectRoot,
|
|
4448
|
+
requestedPath: raw,
|
|
4449
|
+
cwd: this._projectRoot,
|
|
4450
|
+
accessKind: "attach",
|
|
4451
|
+
allowCreate: true,
|
|
4452
|
+
});
|
|
4453
|
+
}
|
|
4454
|
+
catch (e) {
|
|
4455
|
+
if (!(e instanceof SafePathError))
|
|
4456
|
+
continue;
|
|
4457
|
+
if (e.code !== "PATH_OUTSIDE_SCOPE" && e.code !== "PATH_SYMLINK_ESCAPES_SCOPE")
|
|
4458
|
+
continue;
|
|
4459
|
+
const lexicalTarget = e.details.resolvedPath || resolve(this._projectRoot, raw);
|
|
4460
|
+
explicitAttachmentRoots.add(resolve(lexicalTarget));
|
|
4461
|
+
}
|
|
4462
|
+
}
|
|
4463
|
+
const externalRoots = [...explicitAttachmentRoots];
|
|
4464
|
+
const attachmentArtifactsDir = this._sessionArtifactsOverride ?? this._getArtifactsDirIfAvailable?.();
|
|
4465
|
+
try {
|
|
4466
|
+
const result = await processFileAttachments(userInput, undefined, supportsMultimodal, this._projectRoot, externalRoots, attachmentArtifactsDir);
|
|
4467
|
+
if (!fileAttachHasFiles(result))
|
|
4468
|
+
return userInput;
|
|
4469
|
+
if (fileAttachHasImages(result) && supportsMultimodal) {
|
|
4470
|
+
const contentParts = [];
|
|
4471
|
+
const cleaned = result.cleanedText.trim();
|
|
4472
|
+
if (cleaned) {
|
|
4473
|
+
contentParts.push({ type: "text", text: cleaned });
|
|
4474
|
+
}
|
|
4475
|
+
for (const f of result.files) {
|
|
4476
|
+
if (f.isImage && f.imageData) {
|
|
4477
|
+
contentParts.push({
|
|
4478
|
+
type: "image",
|
|
4479
|
+
media_type: f.imageMediaType,
|
|
4480
|
+
data: f.imageData,
|
|
4481
|
+
});
|
|
4482
|
+
}
|
|
4483
|
+
}
|
|
4484
|
+
if (result.contextStr) {
|
|
4485
|
+
contentParts.push({ type: "text", text: result.contextStr });
|
|
4486
|
+
}
|
|
4487
|
+
return contentParts;
|
|
4488
|
+
}
|
|
4489
|
+
let userContent = result.cleanedText;
|
|
4490
|
+
if (result.contextStr) {
|
|
4491
|
+
userContent += "\n\n" + result.contextStr;
|
|
4492
|
+
}
|
|
4493
|
+
return userContent;
|
|
4494
|
+
}
|
|
4495
|
+
catch (e) {
|
|
4496
|
+
console.warn(`File attachment processing failed; continuing without attachments: ${e instanceof Error ? e.message : String(e)}`);
|
|
4497
|
+
return userInput;
|
|
4498
|
+
}
|
|
4499
|
+
}
|
|
4500
|
+
// ==================================================================
|
|
4501
|
+
// MCP integration
|
|
4502
|
+
// ==================================================================
|
|
4503
|
+
async _ensureMcp() {
|
|
4504
|
+
if (!this._mcpManager)
|
|
4505
|
+
return;
|
|
4506
|
+
try {
|
|
4507
|
+
await this._mcpManager.connectAll();
|
|
4508
|
+
const mcpTools = this._mcpManager.getAllTools();
|
|
4509
|
+
for (const tool of mcpTools) {
|
|
4510
|
+
const toolName = tool.name;
|
|
4511
|
+
if (toolName in this._toolExecutors)
|
|
4512
|
+
continue;
|
|
4513
|
+
const capturedName = toolName;
|
|
4514
|
+
this._toolExecutors[toolName] = async (args) => {
|
|
4515
|
+
return this._mcpManager.callTool(capturedName, args);
|
|
4516
|
+
};
|
|
4517
|
+
}
|
|
4518
|
+
// Inject MCP tool defs into agents
|
|
4519
|
+
const agentsToPatch = [
|
|
4520
|
+
this.primaryAgent,
|
|
4521
|
+
...Object.values(this.agentTemplates),
|
|
4522
|
+
];
|
|
4523
|
+
const seenAgents = new Set();
|
|
4524
|
+
for (const agent of agentsToPatch) {
|
|
4525
|
+
if (seenAgents.has(agent))
|
|
4526
|
+
continue;
|
|
4527
|
+
seenAgents.add(agent);
|
|
4528
|
+
const spec = agent._mcpToolsSpec;
|
|
4529
|
+
if (!spec || spec === "none")
|
|
4530
|
+
continue;
|
|
4531
|
+
let selectedTools;
|
|
4532
|
+
if (spec === "all") {
|
|
4533
|
+
selectedTools = mcpTools;
|
|
4534
|
+
}
|
|
4535
|
+
else if (Array.isArray(spec)) {
|
|
4536
|
+
const prefixes = spec.map((s) => `mcp__${s}__`);
|
|
4537
|
+
selectedTools = mcpTools.filter((t) => prefixes.some((p) => t.name.startsWith(p)));
|
|
4538
|
+
}
|
|
4539
|
+
else {
|
|
4540
|
+
selectedTools = [];
|
|
4541
|
+
}
|
|
4542
|
+
if (!selectedTools.length)
|
|
4543
|
+
continue;
|
|
4544
|
+
const existingToolNames = new Set(agent.tools.map((t) => t.name));
|
|
4545
|
+
for (const tool of selectedTools) {
|
|
4546
|
+
if (existingToolNames.has(tool.name))
|
|
4547
|
+
continue;
|
|
4548
|
+
agent.tools.push(tool);
|
|
4549
|
+
existingToolNames.add(tool.name);
|
|
4550
|
+
}
|
|
4551
|
+
}
|
|
4552
|
+
this._mcpConnected = mcpTools.length > 0;
|
|
4553
|
+
}
|
|
4554
|
+
catch (e) {
|
|
4555
|
+
this._mcpConnected = false;
|
|
4556
|
+
console.error("Failed to connect MCP servers:", e);
|
|
4557
|
+
}
|
|
4558
|
+
}
|
|
4559
|
+
// ==================================================================
|
|
4560
|
+
// Persistence
|
|
4561
|
+
// ==================================================================
|
|
4562
|
+
// getStateForPersistence() and restoreFromPersistence() removed.
|
|
4563
|
+
// All persistence is now via getLogForPersistence() / restoreFromLog().
|
|
4564
|
+
_generateSummary() {
|
|
4565
|
+
for (const entry of this._log) {
|
|
4566
|
+
if (entry.type !== "user_message")
|
|
4567
|
+
continue;
|
|
4568
|
+
if (entry.discarded)
|
|
4569
|
+
continue;
|
|
4570
|
+
const display = entry.display;
|
|
4571
|
+
if (!display)
|
|
4572
|
+
continue;
|
|
4573
|
+
if (SYSTEM_PREFIXES.some((prefix) => display.startsWith(prefix)))
|
|
4574
|
+
continue;
|
|
4575
|
+
return stripContextTags(display).slice(0, 100).trim();
|
|
4576
|
+
}
|
|
4577
|
+
return "New session";
|
|
4578
|
+
}
|
|
4579
|
+
// ==================================================================
|
|
4580
|
+
// Resource cleanup
|
|
4581
|
+
// ==================================================================
|
|
4582
|
+
async close() {
|
|
4583
|
+
this._forceKillAllAgents();
|
|
4584
|
+
this._forceKillAllShells();
|
|
4585
|
+
if (this._mcpManager) {
|
|
4586
|
+
try {
|
|
4587
|
+
await this._mcpManager.closeAll();
|
|
4588
|
+
}
|
|
4589
|
+
catch (e) {
|
|
4590
|
+
console.warn("Error closing MCP connections:", e);
|
|
4591
|
+
}
|
|
4592
|
+
}
|
|
4593
|
+
}
|
|
4594
|
+
}
|
|
4595
|
+
//# sourceMappingURL=session.js.map
|