@oh-my-pi/pi-coding-agent 16.0.1 → 16.0.3
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/CHANGELOG.md +70 -0
- package/README.md +0 -1
- package/dist/cli.js +316 -371
- package/dist/types/advisor/advise-tool.d.ts +30 -1
- package/dist/types/commands/install.d.ts +1 -1
- package/dist/types/config/model-resolver.d.ts +22 -0
- package/dist/types/config/settings-schema.d.ts +0 -10
- package/dist/types/eval/js/shared/runtime.d.ts +1 -0
- package/dist/types/eval/js/worker-core.d.ts +1 -0
- package/dist/types/exec/non-interactive-env.d.ts +2 -0
- package/dist/types/extensibility/extensions/loader.d.ts +2 -2
- package/dist/types/goals/runtime.d.ts +0 -1
- package/dist/types/mcp/tool-bridge.d.ts +3 -0
- package/dist/types/modes/components/custom-editor.d.ts +14 -4
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +3 -2
- package/dist/types/modes/theme/mermaid-cache.d.ts +18 -1
- package/dist/types/modes/types.d.ts +1 -1
- package/dist/types/registry/agent-lifecycle.d.ts +16 -1
- package/dist/types/sdk.d.ts +8 -0
- package/dist/types/session/agent-session.d.ts +20 -8
- package/dist/types/session/messages.d.ts +3 -0
- package/dist/types/session/session-dump-format.d.ts +8 -2
- package/dist/types/session/session-entries.d.ts +4 -0
- package/dist/types/session/session-history-format.d.ts +2 -0
- package/dist/types/session/session-manager.d.ts +22 -0
- package/dist/types/stt/downloader.d.ts +5 -5
- package/dist/types/task/executor.d.ts +6 -0
- package/dist/types/task/persisted-revive.d.ts +36 -0
- package/dist/types/tiny/models.d.ts +8 -0
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/index.d.ts +0 -1
- package/dist/types/utils/markit.d.ts +8 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +156 -12
- package/src/advisor/advise-tool.ts +48 -6
- package/src/advisor/runtime.ts +10 -3
- package/src/auto-thinking/classifier.ts +12 -3
- package/src/cli/args.ts +1 -0
- package/src/cli.ts +2 -2
- package/src/commands/install.ts +3 -3
- package/src/config/model-resolver.ts +63 -12
- package/src/config/settings-schema.ts +0 -11
- package/src/discovery/github.ts +89 -1
- package/src/eval/agent-bridge.ts +2 -0
- package/src/eval/js/context-manager.ts +2 -1
- package/src/eval/js/shared/runtime.ts +189 -15
- package/src/eval/js/worker-core.ts +19 -0
- package/src/exec/bash-executor.ts +2 -2
- package/src/exec/non-interactive-env.ts +71 -0
- package/src/export/html/index.ts +1 -1
- package/src/export/html/tool-views.generated.js +34 -35
- package/src/extensibility/extensions/loader.ts +21 -9
- package/src/extensibility/extensions/runner.ts +17 -1
- package/src/extensibility/plugins/loader.ts +154 -21
- package/src/extensibility/plugins/manager.ts +40 -33
- package/src/goals/runtime.ts +1 -23
- package/src/internal-urls/docs-index.generated.ts +9 -11
- package/src/main.ts +20 -0
- package/src/mcp/render.ts +11 -1
- package/src/mcp/tool-bridge.ts +3 -0
- package/src/modes/components/custom-editor.test.ts +63 -18
- package/src/modes/components/custom-editor.ts +63 -15
- package/src/modes/controllers/command-controller.ts +2 -2
- package/src/modes/controllers/input-controller.ts +15 -9
- package/src/modes/controllers/selector-controller.ts +13 -8
- package/src/modes/controllers/tan-command-controller.ts +1 -0
- package/src/modes/interactive-mode.ts +4 -2
- package/src/modes/setup-wizard/wizard-overlay.ts +26 -4
- package/src/modes/theme/mermaid-cache.ts +74 -11
- package/src/modes/theme/theme.ts +14 -1
- package/src/modes/types.ts +1 -1
- package/src/prompts/system/system-prompt.md +2 -1
- package/src/registry/agent-lifecycle.ts +60 -8
- package/src/sdk.ts +20 -26
- package/src/session/agent-session.ts +381 -110
- package/src/session/artifacts.ts +19 -1
- package/src/session/messages.ts +1 -1
- package/src/session/session-dump-format.ts +167 -23
- package/src/session/session-entries.ts +4 -0
- package/src/session/session-history-format.ts +37 -3
- package/src/session/session-manager.ts +94 -4
- package/src/slash-commands/builtin-registry.ts +4 -7
- package/src/stt/asr-client.ts +6 -0
- package/src/stt/downloader.ts +13 -6
- package/src/stt/stt-controller.ts +52 -11
- package/src/system-prompt.ts +7 -1
- package/src/task/executor.ts +118 -6
- package/src/task/index.ts +2 -2
- package/src/task/persisted-revive.ts +128 -0
- package/src/tiny/models.ts +10 -0
- package/src/tiny/worker.ts +4 -3
- package/src/tools/builtin-names.ts +0 -1
- package/src/tools/index.ts +0 -4
- package/src/tools/output-meta.ts +17 -3
- package/src/utils/lang-from-path.ts +5 -0
- package/src/utils/markit.ts +24 -1
- package/src/utils/title-generator.ts +4 -4
- package/dist/types/tools/render-mermaid.d.ts +0 -38
- package/src/prompts/tools/render-mermaid.md +0 -9
- package/src/tools/render-mermaid.ts +0 -69
package/src/session/artifacts.ts
CHANGED
|
@@ -7,6 +7,24 @@
|
|
|
7
7
|
import * as fs from "node:fs/promises";
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Sanitize a tool name for safe use as the middle segment of the artifact
|
|
12
|
+
* filename (`${id}.${toolType}.log`). Built-in tool names are fixed, but MCP,
|
|
13
|
+
* extension, and RPC-host tool names are arbitrary and may contain path
|
|
14
|
+
* separators (`/`, `\`) or traversal sequences (`..`) that would otherwise let
|
|
15
|
+
* a spilled artifact escape the artifacts directory. Collapse everything
|
|
16
|
+
* outside `[A-Za-z0-9_-]` to `_`, and cap the length so an arbitrarily long
|
|
17
|
+
* name cannot overflow the filesystem's filename limit (ENAMETOOLONG). Fall
|
|
18
|
+
* back to `tool` when nothing survives.
|
|
19
|
+
*/
|
|
20
|
+
function sanitizeToolType(toolType: string): string {
|
|
21
|
+
const sanitized = toolType
|
|
22
|
+
.replace(/[^A-Za-z0-9_-]+/g, "_")
|
|
23
|
+
.slice(0, 64)
|
|
24
|
+
.replace(/^_+|_+$/g, "");
|
|
25
|
+
return sanitized.length > 0 ? sanitized : "tool";
|
|
26
|
+
}
|
|
27
|
+
|
|
10
28
|
/**
|
|
11
29
|
* Manages artifact storage for a session.
|
|
12
30
|
*
|
|
@@ -83,7 +101,7 @@ export class ArtifactManager {
|
|
|
83
101
|
async allocatePath(toolType: string): Promise<{ id: string; path: string }> {
|
|
84
102
|
await this.#ensureDir();
|
|
85
103
|
const id = String(this.allocateId());
|
|
86
|
-
const filename = `${id}.${toolType}.log`;
|
|
104
|
+
const filename = `${id}.${sanitizeToolType(toolType)}.log`;
|
|
87
105
|
return { id, path: path.join(this.#dir, filename) };
|
|
88
106
|
}
|
|
89
107
|
|
package/src/session/messages.ts
CHANGED
|
@@ -94,7 +94,7 @@ export function shouldRenderAbortReason(errorMessage: string | undefined): boole
|
|
|
94
94
|
|
|
95
95
|
/** Sentinel `errorMessage` the agent stamps on any abort that carried no custom
|
|
96
96
|
* reason (bare `abort()`). Renderers treat it as "no specific reason given". */
|
|
97
|
-
const GENERIC_ABORT_SENTINEL = "Request was aborted";
|
|
97
|
+
export const GENERIC_ABORT_SENTINEL = "Request was aborted";
|
|
98
98
|
|
|
99
99
|
/** Resolve the operator-facing label for an aborted assistant turn. A custom
|
|
100
100
|
* abort reason threaded onto `errorMessage` is returned verbatim; aborts with
|
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Plain-text / markdown session formatting
|
|
2
|
+
* Plain-text / markdown session formatting for `/dump` and `/advisor dump raw`.
|
|
3
|
+
*
|
|
4
|
+
* Renders a prelude (system prompt, model/thinking config, tool inventory)
|
|
5
|
+
* followed by the message history as per-message markdown headings: `## User`,
|
|
6
|
+
* `## Assistant` (with `<thinking>` blocks and `### Tool Call: <name>` + YAML
|
|
7
|
+
* args), `### Tool Result: <name>`, and the execution/summary sections.
|
|
3
8
|
*/
|
|
4
9
|
import type { AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
10
|
+
import { INTENT_FIELD } from "@oh-my-pi/pi-agent-core";
|
|
11
|
+
import type { AssistantMessage, Model, ToolExample, TSchema } from "@oh-my-pi/pi-ai";
|
|
12
|
+
import { renderDelimitedThinking, renderToolInventory } from "@oh-my-pi/pi-ai/dialect";
|
|
13
|
+
import { YAML } from "bun";
|
|
14
|
+
import { canonicalizeMessage } from "../utils/thinking-display";
|
|
15
|
+
import {
|
|
16
|
+
type BashExecutionMessage,
|
|
17
|
+
type BranchSummaryMessage,
|
|
18
|
+
bashExecutionToText,
|
|
19
|
+
type CompactionSummaryMessage,
|
|
20
|
+
type CustomMessage,
|
|
21
|
+
type FileMentionMessage,
|
|
22
|
+
type HookMessage,
|
|
23
|
+
type PythonExecutionMessage,
|
|
24
|
+
pythonExecutionToText,
|
|
25
|
+
} from "./messages";
|
|
9
26
|
|
|
10
27
|
/** Minimal tool shape for dump output (matches AgentTool fields used by formatSessionDumpText). */
|
|
11
28
|
export interface SessionDumpToolInfo {
|
|
@@ -23,12 +40,25 @@ export interface FormatSessionDumpTextOptions {
|
|
|
23
40
|
tools?: readonly SessionDumpToolInfo[];
|
|
24
41
|
}
|
|
25
42
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
43
|
+
interface InventoryTool {
|
|
44
|
+
name: string;
|
|
45
|
+
description: string;
|
|
46
|
+
parameters: TSchema;
|
|
47
|
+
examples?: readonly ToolExample[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function toInventoryTools(tools: readonly SessionDumpToolInfo[]): InventoryTool[] {
|
|
51
|
+
return tools.map(tool => ({
|
|
52
|
+
name: tool.name,
|
|
53
|
+
description: tool.description,
|
|
54
|
+
parameters: tool.parameters as TSchema,
|
|
55
|
+
examples: tool.examples,
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** System prompt + model/thinking config + tool inventory — shared by both transcript styles. */
|
|
60
|
+
function renderDumpHeader(options: FormatSessionDumpTextOptions, inventoryTools: readonly InventoryTool[]): string[] {
|
|
30
61
|
const lines: string[] = [];
|
|
31
|
-
const definition = getDialectDefinition(preferredDialect(options.model?.id ?? ""));
|
|
32
62
|
|
|
33
63
|
const systemPrompt = options.systemPrompt?.filter(prompt => prompt.length > 0) ?? [];
|
|
34
64
|
if (systemPrompt.length > 0) {
|
|
@@ -43,28 +73,142 @@ export function formatSessionDumpText(options: FormatSessionDumpTextOptions): st
|
|
|
43
73
|
}
|
|
44
74
|
|
|
45
75
|
const model = options.model;
|
|
46
|
-
const thinkingLevel = options.thinkingLevel;
|
|
47
76
|
lines.push("## Configuration\n");
|
|
48
77
|
lines.push(`Model: ${model ? `${model.provider}/${model.id}` : "(not selected)"}`);
|
|
49
|
-
lines.push(`Thinking Level: ${thinkingLevel ?? ""}`);
|
|
78
|
+
lines.push(`Thinking Level: ${options.thinkingLevel ?? ""}`);
|
|
50
79
|
lines.push("\n");
|
|
51
80
|
|
|
52
|
-
const tools = options.tools ?? [];
|
|
53
|
-
const inventoryTools = tools.map(tool => ({
|
|
54
|
-
name: tool.name,
|
|
55
|
-
description: tool.description,
|
|
56
|
-
parameters: tool.parameters as TSchema,
|
|
57
|
-
examples: tool.examples,
|
|
58
|
-
}));
|
|
59
81
|
if (inventoryTools.length > 0) {
|
|
60
82
|
lines.push("## Available Tools\n");
|
|
61
|
-
lines.push(renderToolInventory(inventoryTools,
|
|
83
|
+
lines.push(renderToolInventory(inventoryTools, model?.id ?? ""));
|
|
62
84
|
lines.push("\n");
|
|
63
85
|
}
|
|
64
86
|
|
|
65
|
-
lines
|
|
66
|
-
|
|
67
|
-
|
|
87
|
+
return lines;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Append the legacy per-message markdown-heading transcript (the pre-16.x `/dump` body). */
|
|
91
|
+
function appendMarkdownTranscript(lines: string[], messages: readonly AgentMessage[]): void {
|
|
92
|
+
for (const msg of messages) {
|
|
93
|
+
if (msg.role === "user" || msg.role === "developer") {
|
|
94
|
+
lines.push(msg.role === "developer" ? "## Developer\n" : "## User\n");
|
|
95
|
+
if (typeof msg.content === "string") {
|
|
96
|
+
lines.push(msg.content);
|
|
97
|
+
} else {
|
|
98
|
+
for (const c of msg.content) {
|
|
99
|
+
if (c.type === "text") lines.push(c.text);
|
|
100
|
+
else if (c.type === "image") lines.push("[Image]");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
lines.push("\n");
|
|
104
|
+
} else if (msg.role === "assistant") {
|
|
105
|
+
const assistantMsg = msg as AssistantMessage;
|
|
106
|
+
lines.push("## Assistant\n");
|
|
107
|
+
for (const c of assistantMsg.content) {
|
|
108
|
+
if (c.type === "text") {
|
|
109
|
+
lines.push(c.text);
|
|
110
|
+
} else if (c.type === "thinking") {
|
|
111
|
+
const thinking = canonicalizeMessage(c.thinking);
|
|
112
|
+
if (thinking.length === 0) continue;
|
|
113
|
+
// Unwrap any literal `<thinking>` envelope already present in the
|
|
114
|
+
// block (e.g. Opus 4.5 — issue #2700) so the dump never nests tags.
|
|
115
|
+
lines.push(`${renderDelimitedThinking("<thinking>", "</thinking>", thinking)}\n`);
|
|
116
|
+
} else if (c.type === "toolCall") {
|
|
117
|
+
lines.push(`### Tool Call: ${c.name}`);
|
|
118
|
+
const rawArgs = c.arguments as Record<string, unknown> | undefined;
|
|
119
|
+
if (rawArgs && typeof rawArgs === "object") {
|
|
120
|
+
const intent = rawArgs[INTENT_FIELD];
|
|
121
|
+
if (typeof intent === "string" && intent.trim().length > 0) {
|
|
122
|
+
for (const line of intent.split("\n")) lines.push(`// ${line}`);
|
|
123
|
+
}
|
|
124
|
+
const args: Record<string, unknown> = {};
|
|
125
|
+
let hasArgs = false;
|
|
126
|
+
for (const key in rawArgs) {
|
|
127
|
+
if (key === INTENT_FIELD) continue;
|
|
128
|
+
args[key] = rawArgs[key];
|
|
129
|
+
hasArgs = true;
|
|
130
|
+
}
|
|
131
|
+
if (hasArgs) {
|
|
132
|
+
lines.push("```yaml");
|
|
133
|
+
lines.push(YAML.stringify(args, null, 2).trimEnd());
|
|
134
|
+
lines.push("```\n");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
lines.push("");
|
|
140
|
+
} else if (msg.role === "toolResult") {
|
|
141
|
+
lines.push(`### Tool Result: ${msg.toolName}`);
|
|
142
|
+
if (msg.isError) lines.push("(error)");
|
|
143
|
+
for (const c of msg.content) {
|
|
144
|
+
if (c.type === "text") {
|
|
145
|
+
lines.push("```");
|
|
146
|
+
lines.push(c.text);
|
|
147
|
+
lines.push("```");
|
|
148
|
+
} else if (c.type === "image") {
|
|
149
|
+
lines.push("[Image output]");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
lines.push("");
|
|
153
|
+
} else if (msg.role === "bashExecution") {
|
|
154
|
+
const bashMsg = msg as BashExecutionMessage;
|
|
155
|
+
if (!bashMsg.excludeFromContext) {
|
|
156
|
+
lines.push("## Bash Execution\n");
|
|
157
|
+
lines.push(bashExecutionToText(bashMsg));
|
|
158
|
+
lines.push("\n");
|
|
159
|
+
}
|
|
160
|
+
} else if (msg.role === "pythonExecution") {
|
|
161
|
+
const pythonMsg = msg as PythonExecutionMessage;
|
|
162
|
+
if (!pythonMsg.excludeFromContext) {
|
|
163
|
+
lines.push("## Python Execution\n");
|
|
164
|
+
lines.push(pythonExecutionToText(pythonMsg));
|
|
165
|
+
lines.push("\n");
|
|
166
|
+
}
|
|
167
|
+
} else if (msg.role === "custom" || msg.role === "hookMessage") {
|
|
168
|
+
const customMsg = msg as CustomMessage | HookMessage;
|
|
169
|
+
lines.push(`## ${customMsg.customType}\n`);
|
|
170
|
+
if (typeof customMsg.content === "string") {
|
|
171
|
+
lines.push(customMsg.content);
|
|
172
|
+
} else {
|
|
173
|
+
for (const c of customMsg.content) {
|
|
174
|
+
if (c.type === "text") lines.push(c.text);
|
|
175
|
+
else if (c.type === "image") lines.push("[Image]");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
lines.push("\n");
|
|
179
|
+
} else if (msg.role === "branchSummary") {
|
|
180
|
+
const branchMsg = msg as BranchSummaryMessage;
|
|
181
|
+
lines.push("## Branch Summary\n");
|
|
182
|
+
lines.push(`(from branch: ${branchMsg.fromId})\n`);
|
|
183
|
+
lines.push(branchMsg.summary);
|
|
184
|
+
lines.push("\n");
|
|
185
|
+
} else if (msg.role === "compactionSummary") {
|
|
186
|
+
const compactMsg = msg as CompactionSummaryMessage;
|
|
187
|
+
lines.push("## Compaction Summary\n");
|
|
188
|
+
lines.push(`(${compactMsg.tokensBefore} tokens before compaction)\n`);
|
|
189
|
+
lines.push(compactMsg.summary);
|
|
190
|
+
lines.push("\n");
|
|
191
|
+
} else if (msg.role === "fileMention") {
|
|
192
|
+
const fileMsg = msg as FileMentionMessage;
|
|
193
|
+
lines.push("## File Mention\n");
|
|
194
|
+
for (const file of fileMsg.files) {
|
|
195
|
+
lines.push(`<file path="${file.path}">`);
|
|
196
|
+
if (file.content) lines.push(file.content);
|
|
197
|
+
if (file.image) lines.push("[Image attached]");
|
|
198
|
+
lines.push("</file>\n");
|
|
199
|
+
}
|
|
200
|
+
lines.push("\n");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
68
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Format messages and session metadata as markdown/plain text (same as
|
|
207
|
+
* AgentSession.formatSessionAsText / /dump).
|
|
208
|
+
*/
|
|
209
|
+
export function formatSessionDumpText(options: FormatSessionDumpTextOptions): string {
|
|
210
|
+
const inventoryTools = toInventoryTools(options.tools ?? []);
|
|
211
|
+
const lines = renderDumpHeader(options, inventoryTools);
|
|
212
|
+
appendMarkdownTranscript(lines, options.messages);
|
|
69
213
|
return lines.join("\n").trim();
|
|
70
214
|
}
|
|
@@ -124,6 +124,10 @@ export interface SessionInitEntry extends SessionEntryBase {
|
|
|
124
124
|
tools: string[];
|
|
125
125
|
/** Output schema if structured output was requested */
|
|
126
126
|
outputSchema?: unknown;
|
|
127
|
+
/** Spawn allowlist the subagent ran with ("" = none, "*" = any, else CSV); absent on pre-spawns files. */
|
|
128
|
+
spawns?: string;
|
|
129
|
+
/** The agent's `readSummarize` setting (`false` = read summarization disabled); absent uses the session default. */
|
|
130
|
+
readSummarize?: boolean;
|
|
127
131
|
}
|
|
128
132
|
|
|
129
133
|
/** Mode change entry - tracks agent mode transitions (e.g. plan mode). */
|
|
@@ -26,6 +26,8 @@ export interface HistoryFormatOptions {
|
|
|
26
26
|
includeThinking?: boolean;
|
|
27
27
|
/** Render tool intent comment before tool call lines. */
|
|
28
28
|
includeToolIntent?: boolean;
|
|
29
|
+
/** Render watched-session roles as inline `**agent**:` / `**user**:` labels (collapsing consecutive same-role messages) instead of `## ` headings, so a primary transcript embedded inside an advisor turn stays visually distinct. */
|
|
30
|
+
watchedRoles?: boolean;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
/** Max length of the primary-arg summary inside `→ tool(...)` lines. */
|
|
@@ -125,7 +127,7 @@ function toolCallLine(
|
|
|
125
127
|
const intent = includeToolIntent ? args?.[INTENT_FIELD] : undefined;
|
|
126
128
|
if (typeof intent === "string" && intent.trim()) {
|
|
127
129
|
const formattedIntent = oneLine(intent, 80);
|
|
128
|
-
return
|
|
130
|
+
return `// ${formattedIntent}\n${base}`;
|
|
129
131
|
}
|
|
130
132
|
return base;
|
|
131
133
|
}
|
|
@@ -191,6 +193,11 @@ export function formatSessionHistoryMarkdown(messages: unknown[], opts?: History
|
|
|
191
193
|
}
|
|
192
194
|
}
|
|
193
195
|
const consumed = new Set<string>();
|
|
196
|
+
// In watched mode, consecutive same-role messages collapse under one label
|
|
197
|
+
// (the watched agent emits one assistant message per tool call, so otherwise
|
|
198
|
+
// every call repeats `**agent**:`). Cleared whenever a
|
|
199
|
+
// non-role-labeled line is emitted so the next turn re-labels.
|
|
200
|
+
let lastWatchedLabel: string | undefined;
|
|
194
201
|
|
|
195
202
|
for (const msg of typed) {
|
|
196
203
|
switch (msg.role) {
|
|
@@ -198,7 +205,17 @@ export function formatSessionHistoryMarkdown(messages: unknown[], opts?: History
|
|
|
198
205
|
case "developer": {
|
|
199
206
|
const text = contentToText(msg.content);
|
|
200
207
|
if (!text.trim()) break;
|
|
201
|
-
|
|
208
|
+
if (opts?.watchedRoles) {
|
|
209
|
+
const label = `**${msg.role}**:`;
|
|
210
|
+
if (lastWatchedLabel === label) {
|
|
211
|
+
lines.push(text, "");
|
|
212
|
+
} else {
|
|
213
|
+
lines.push(label, text, "");
|
|
214
|
+
lastWatchedLabel = label;
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
lines.push(`## ${msg.role}`, "", text, "");
|
|
218
|
+
}
|
|
202
219
|
break;
|
|
203
220
|
}
|
|
204
221
|
case "assistant": {
|
|
@@ -217,45 +234,62 @@ export function formatSessionHistoryMarkdown(messages: unknown[], opts?: History
|
|
|
217
234
|
// redactedThinking elided entirely (no readable text)
|
|
218
235
|
}
|
|
219
236
|
if (body.length === 0) break;
|
|
220
|
-
|
|
237
|
+
if (opts?.watchedRoles) {
|
|
238
|
+
const label = "**agent**:";
|
|
239
|
+
if (lastWatchedLabel === label) {
|
|
240
|
+
lines.push(...body, "");
|
|
241
|
+
} else {
|
|
242
|
+
lines.push(label, ...body, "");
|
|
243
|
+
lastWatchedLabel = label;
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
lines.push("## assistant", "", ...body, "");
|
|
247
|
+
}
|
|
221
248
|
break;
|
|
222
249
|
}
|
|
223
250
|
case "toolResult": {
|
|
224
251
|
// Normally consumed by its toolCall; orphans (e.g. truncated history) get their own line.
|
|
225
252
|
if (consumed.has(msg.toolCallId)) break;
|
|
226
253
|
lines.push(toolCallLine(msg.toolName, undefined, msg, opts?.includeToolIntent), "");
|
|
254
|
+
lastWatchedLabel = undefined;
|
|
227
255
|
break;
|
|
228
256
|
}
|
|
229
257
|
case "bashExecution": {
|
|
230
258
|
const bashMsg = msg as BashExecutionMessage;
|
|
231
259
|
if (bashMsg.excludeFromContext) break;
|
|
232
260
|
lines.push(executionLine("bash", bashMsg.command, bashMsg), "");
|
|
261
|
+
lastWatchedLabel = undefined;
|
|
233
262
|
break;
|
|
234
263
|
}
|
|
235
264
|
case "pythonExecution": {
|
|
236
265
|
const pythonMsg = msg as PythonExecutionMessage;
|
|
237
266
|
if (pythonMsg.excludeFromContext) break;
|
|
238
267
|
lines.push(executionLine("python", pythonMsg.code, pythonMsg), "");
|
|
268
|
+
lastWatchedLabel = undefined;
|
|
239
269
|
break;
|
|
240
270
|
}
|
|
241
271
|
case "custom":
|
|
242
272
|
case "hookMessage": {
|
|
243
273
|
lines.push(customOneLiner(msg as CustomMessage | HookMessage), "");
|
|
274
|
+
lastWatchedLabel = undefined;
|
|
244
275
|
break;
|
|
245
276
|
}
|
|
246
277
|
case "branchSummary": {
|
|
247
278
|
const branchMsg = msg as BranchSummaryMessage;
|
|
248
279
|
lines.push(`[branch] from ${branchMsg.fromId}: ${oneLine(branchMsg.summary)}`, "");
|
|
280
|
+
lastWatchedLabel = undefined;
|
|
249
281
|
break;
|
|
250
282
|
}
|
|
251
283
|
case "compactionSummary": {
|
|
252
284
|
const compactMsg = msg as CompactionSummaryMessage;
|
|
253
285
|
lines.push(`[compaction] ${oneLine(compactMsg.summary)}`, "");
|
|
286
|
+
lastWatchedLabel = undefined;
|
|
254
287
|
break;
|
|
255
288
|
}
|
|
256
289
|
case "fileMention": {
|
|
257
290
|
const fileMsg = msg as FileMentionMessage;
|
|
258
291
|
lines.push(`[file-mention] ${oneLine(fileMsg.files.map(f => f.path).join(", "))}`, "");
|
|
292
|
+
lastWatchedLabel = undefined;
|
|
259
293
|
break;
|
|
260
294
|
}
|
|
261
295
|
}
|
|
@@ -71,6 +71,27 @@ function artifactsDirectoryFor(sessionFile: string | undefined): string | null {
|
|
|
71
71
|
return sessionFile ? sessionFile.slice(0, -JSONL_SUFFIX_LENGTH) : null;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Resolve a breadcrumb's recorded session file to its interactive root. Subagent
|
|
76
|
+
* (and other artifact) sessions live inside a parent session's artifacts dir —
|
|
77
|
+
* `<parent>.jsonl` strips its suffix to `<parent>/`, and a child writes
|
|
78
|
+
* `<parent>/<agentId>.jsonl`. A breadcrumb that points at such a child — a
|
|
79
|
+
* pre-fix poisoned crumb left by a subagent that opened in the parent's TTY, or
|
|
80
|
+
* any nested artifact — must resolve back up to the top-level session so
|
|
81
|
+
* `--continue` resumes the real conversation instead of a subagent transcript.
|
|
82
|
+
*/
|
|
83
|
+
function resolveBreadcrumbToInteractiveRoot(sessionFile: string): string {
|
|
84
|
+
let current = path.resolve(sessionFile);
|
|
85
|
+
// Walk up while the containing dir is itself a session's artifacts dir
|
|
86
|
+
// (`<dir>.jsonl` exists). Capped to defend against pathological layouts.
|
|
87
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
88
|
+
const parentSessionFile = `${path.dirname(current)}.jsonl`;
|
|
89
|
+
if (!fs.existsSync(parentSessionFile)) return current;
|
|
90
|
+
current = parentSessionFile;
|
|
91
|
+
}
|
|
92
|
+
return current;
|
|
93
|
+
}
|
|
94
|
+
|
|
74
95
|
function emptyUsageStatistics(): UsageStatistics {
|
|
75
96
|
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, premiumRequests: 0, cost: 0 };
|
|
76
97
|
}
|
|
@@ -462,7 +483,7 @@ export class SessionManager {
|
|
|
462
483
|
* bytes in the kernel page cache, so the file is software-crash durable.
|
|
463
484
|
*/
|
|
464
485
|
#rewriteSynchronously(): void {
|
|
465
|
-
if (!this.#persist || !this.#sessionFile) return;
|
|
486
|
+
if (!this.#persist || !this.#sessionFile || !this.#shouldHaveSessionFile()) return;
|
|
466
487
|
|
|
467
488
|
try {
|
|
468
489
|
const body = this.#fileBody();
|
|
@@ -931,8 +952,10 @@ export class SessionManager {
|
|
|
931
952
|
async close(): Promise<void> {
|
|
932
953
|
if (!this.#persist) return;
|
|
933
954
|
await this.#scheduleDiskWork(async () => {
|
|
955
|
+
const hadWriter = this.#writer !== undefined;
|
|
934
956
|
await this.#closeWriterHandle();
|
|
935
|
-
this.#
|
|
957
|
+
if (hadWriter || (this.#sessionFile && this.#storage.existsSync(this.#sessionFile)))
|
|
958
|
+
this.#fileIsCurrent = true;
|
|
936
959
|
});
|
|
937
960
|
if (this.#diskFailure) throw this.#diskFailure;
|
|
938
961
|
}
|
|
@@ -1157,7 +1180,14 @@ export class SessionManager {
|
|
|
1157
1180
|
return entry.id;
|
|
1158
1181
|
}
|
|
1159
1182
|
|
|
1160
|
-
appendSessionInit(init: {
|
|
1183
|
+
appendSessionInit(init: {
|
|
1184
|
+
systemPrompt: string;
|
|
1185
|
+
task: string;
|
|
1186
|
+
tools: string[];
|
|
1187
|
+
outputSchema?: unknown;
|
|
1188
|
+
spawns?: string;
|
|
1189
|
+
readSummarize?: boolean;
|
|
1190
|
+
}): string {
|
|
1161
1191
|
const entry: SessionInitEntry = { type: "session_init", ...this.#freshEntryFields(), ...init };
|
|
1162
1192
|
this.#recordEntry(entry);
|
|
1163
1193
|
return entry.id;
|
|
@@ -1528,17 +1558,74 @@ export class SessionManager {
|
|
|
1528
1558
|
filePath: string,
|
|
1529
1559
|
sessionDir?: string,
|
|
1530
1560
|
storage: SessionStorage = new FileSessionStorage(),
|
|
1531
|
-
options?: { initialCwd?: string },
|
|
1561
|
+
options?: { initialCwd?: string; suppressBreadcrumb?: boolean },
|
|
1532
1562
|
): Promise<SessionManager> {
|
|
1533
1563
|
const loaded = await loadEntriesFromFile(filePath, storage);
|
|
1534
1564
|
const header = loaded.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
1535
1565
|
const cwd = header?.cwd ?? options?.initialCwd ?? getProjectDir();
|
|
1536
1566
|
const dir = sessionDir ?? path.dirname(path.resolve(filePath));
|
|
1537
1567
|
const manager = new SessionManager(cwd, dir, true, storage);
|
|
1568
|
+
manager.#suppressBreadcrumb = options?.suppressBreadcrumb === true;
|
|
1538
1569
|
await manager.setSessionFile(filePath);
|
|
1539
1570
|
return manager;
|
|
1540
1571
|
}
|
|
1541
1572
|
|
|
1573
|
+
/**
|
|
1574
|
+
* Lock-free peek for cold subagent revival: returns the recorded working
|
|
1575
|
+
* directory (session header) and the latest `session_init` contract (system
|
|
1576
|
+
* prompt / tools / output schema) WITHOUT taking the single-writer lock that
|
|
1577
|
+
* {@link open} acquires — the caller re-opens for the actual revive. Returns
|
|
1578
|
+
* null when the file can't be read; `init` is null for files written before
|
|
1579
|
+
* `session_init` was recorded (no faithful contract to rebuild from).
|
|
1580
|
+
*/
|
|
1581
|
+
static async peekSessionInit(
|
|
1582
|
+
filePath: string,
|
|
1583
|
+
storage: SessionStorage = new FileSessionStorage(),
|
|
1584
|
+
): Promise<{
|
|
1585
|
+
cwd: string;
|
|
1586
|
+
init: {
|
|
1587
|
+
systemPrompt: string;
|
|
1588
|
+
task: string;
|
|
1589
|
+
tools: string[];
|
|
1590
|
+
outputSchema?: unknown;
|
|
1591
|
+
spawns?: string;
|
|
1592
|
+
readSummarize?: boolean;
|
|
1593
|
+
} | null;
|
|
1594
|
+
} | null> {
|
|
1595
|
+
let loaded: FileEntry[];
|
|
1596
|
+
try {
|
|
1597
|
+
loaded = await loadEntriesFromFile(filePath, storage);
|
|
1598
|
+
} catch {
|
|
1599
|
+
return null;
|
|
1600
|
+
}
|
|
1601
|
+
// A missing/empty file has no usable session — nothing to revive from.
|
|
1602
|
+
if (loaded.length === 0) return null;
|
|
1603
|
+
const header = loaded.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
1604
|
+
let init: {
|
|
1605
|
+
systemPrompt: string;
|
|
1606
|
+
task: string;
|
|
1607
|
+
tools: string[];
|
|
1608
|
+
outputSchema?: unknown;
|
|
1609
|
+
spawns?: string;
|
|
1610
|
+
readSummarize?: boolean;
|
|
1611
|
+
} | null = null;
|
|
1612
|
+
for (let index = loaded.length - 1; index >= 0; index--) {
|
|
1613
|
+
const entry = loaded[index];
|
|
1614
|
+
if (entry.type === "session_init") {
|
|
1615
|
+
init = {
|
|
1616
|
+
systemPrompt: entry.systemPrompt,
|
|
1617
|
+
task: entry.task,
|
|
1618
|
+
tools: entry.tools,
|
|
1619
|
+
outputSchema: entry.outputSchema,
|
|
1620
|
+
readSummarize: entry.readSummarize,
|
|
1621
|
+
spawns: entry.spawns,
|
|
1622
|
+
};
|
|
1623
|
+
break;
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
return { cwd: header?.cwd ?? getProjectDir(), init };
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1542
1629
|
/** Continue the most recent session, or create a new one if none exists. */
|
|
1543
1630
|
static async continueRecent(
|
|
1544
1631
|
cwd: string,
|
|
@@ -1551,6 +1638,9 @@ export class SessionManager {
|
|
|
1551
1638
|
let chosenSession: string | null | undefined;
|
|
1552
1639
|
|
|
1553
1640
|
if (breadcrumb) {
|
|
1641
|
+
// Recover stale crumbs: a subagent open (pre-fix) may have pointed this
|
|
1642
|
+
// terminal's breadcrumb at an artifact child; resume the parent instead.
|
|
1643
|
+
breadcrumb.sessionFile = resolveBreadcrumbToInteractiveRoot(breadcrumb.sessionFile);
|
|
1554
1644
|
const breadcrumbCwd = path.resolve(breadcrumb.cwd);
|
|
1555
1645
|
if (breadcrumbCwd === resolvedCwd) {
|
|
1556
1646
|
chosenSession = breadcrumb.sessionFile;
|
|
@@ -547,17 +547,14 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
547
547
|
name: "dump",
|
|
548
548
|
description: "Copy session transcript to clipboard",
|
|
549
549
|
acpDescription: "Return full transcript as plain text",
|
|
550
|
-
inlineHint: "[raw]",
|
|
551
550
|
allowArgs: true,
|
|
552
|
-
handle: async (
|
|
553
|
-
const
|
|
554
|
-
const text = runtime.session.formatSessionAsText({ compact: !isRaw });
|
|
551
|
+
handle: async (_command, runtime) => {
|
|
552
|
+
const text = runtime.session.formatSessionAsText();
|
|
555
553
|
await runtime.output(text || "No messages to dump yet.");
|
|
556
554
|
return commandConsumed();
|
|
557
555
|
},
|
|
558
|
-
handleTui: (
|
|
559
|
-
|
|
560
|
-
runtime.ctx.handleDumpCommand(isRaw);
|
|
556
|
+
handleTui: (_command, runtime) => {
|
|
557
|
+
runtime.ctx.handleDumpCommand();
|
|
561
558
|
runtime.ctx.editor.setText("");
|
|
562
559
|
},
|
|
563
560
|
},
|
package/src/stt/asr-client.ts
CHANGED
|
@@ -314,6 +314,12 @@ export class SttClient {
|
|
|
314
314
|
const worker = this.#ensureWorker();
|
|
315
315
|
const id = String(++this.#nextRequestId);
|
|
316
316
|
const { promise, resolve, reject } = Promise.withResolvers<string>();
|
|
317
|
+
// `stop()` is normally the only awaiter of `promise`, but with model loading
|
|
318
|
+
// now deferred to the stream, a load failure (or early worker error) can
|
|
319
|
+
// reject it before the caller stops — attach a benign handler so that never
|
|
320
|
+
// surfaces as an unhandled rejection. stop()/await still observes the
|
|
321
|
+
// rejection through the original promise.
|
|
322
|
+
void promise.catch(() => {});
|
|
317
323
|
const signal = options.signal;
|
|
318
324
|
let settled = false;
|
|
319
325
|
const onAbort = (): void => handle.cancel();
|
package/src/stt/downloader.ts
CHANGED
|
@@ -39,12 +39,12 @@ export interface SttDownloadProgress {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* Whether the selected model is
|
|
42
|
+
* Whether the selected model is fully present in the local cache. For
|
|
43
43
|
* transformers.js Whisper tiers a complete download leaves `config.json` plus
|
|
44
|
-
*
|
|
45
|
-
* as not-cached); for
|
|
46
|
-
*
|
|
47
|
-
* ignored).
|
|
44
|
+
* matching `encoder*.onnx` and `decoder*.onnx` shards under `onnx/` (a partial
|
|
45
|
+
* fetch with only one shard, or a bare `config.json`, reads as not-cached); for
|
|
46
|
+
* sherpa-onnx tiers every model file (encoder/decoder/joiner + tokens) must be
|
|
47
|
+
* present (`.part` sidecars from an interrupted fetch are ignored).
|
|
48
48
|
*/
|
|
49
49
|
export async function isSttModelCached(key: string): Promise<boolean> {
|
|
50
50
|
const spec = resolveSttModelSpec(key);
|
|
@@ -63,8 +63,15 @@ export async function isSttModelCached(key: string): Promise<boolean> {
|
|
|
63
63
|
try {
|
|
64
64
|
const root = await fs.readdir(repoDir);
|
|
65
65
|
if (!root.includes("config.json")) return false;
|
|
66
|
+
// Whisper tiers are encoder-decoder: a complete download leaves both an
|
|
67
|
+
// `encoder*.onnx` and a `decoder*.onnx` (the dtype suffix varies). Require
|
|
68
|
+
// both rather than any single `.onnx`, so an interrupted fetch that landed
|
|
69
|
+
// only one shard reads as not-cached and the caller takes the foreground
|
|
70
|
+
// download path with progress instead of silently fetching mid-recording.
|
|
66
71
|
const onnxFiles = await fs.readdir(path.join(repoDir, "onnx")).catch(() => [] as string[]);
|
|
67
|
-
|
|
72
|
+
const hasEncoder = onnxFiles.some(file => file.startsWith("encoder") && file.endsWith(".onnx"));
|
|
73
|
+
const hasDecoder = onnxFiles.some(file => file.startsWith("decoder") && file.endsWith(".onnx"));
|
|
74
|
+
return hasEncoder && hasDecoder;
|
|
68
75
|
} catch {
|
|
69
76
|
return false;
|
|
70
77
|
}
|