@oh-my-pi/pi-coding-agent 12.9.0 → 12.10.1
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 +46 -0
- package/package.json +7 -7
- package/src/capability/rule.ts +173 -5
- package/src/cli/args.ts +3 -0
- package/src/cli/update-cli.ts +1 -66
- package/src/commands/launch.ts +3 -0
- package/src/config/model-registry.ts +311 -21
- package/src/config/model-resolver.ts +5 -4
- package/src/config/settings-schema.ts +12 -0
- package/src/discovery/agents.ts +175 -12
- package/src/discovery/builtin.ts +3 -13
- package/src/discovery/cline.ts +4 -45
- package/src/discovery/cursor.ts +2 -29
- package/src/discovery/helpers.ts +38 -0
- package/src/discovery/windsurf.ts +5 -44
- package/src/export/ttsr.ts +324 -54
- package/src/internal-urls/index.ts +4 -2
- package/src/internal-urls/memory-protocol.ts +133 -0
- package/src/internal-urls/router.ts +4 -2
- package/src/internal-urls/skill-protocol.ts +1 -1
- package/src/internal-urls/types.ts +6 -2
- package/src/main.ts +5 -0
- package/src/memories/index.ts +6 -13
- package/src/modes/components/settings-defs.ts +6 -0
- package/src/modes/rpc/rpc-client.ts +16 -0
- package/src/prompts/memories/consolidation.md +1 -1
- package/src/prompts/memories/read_path.md +4 -4
- package/src/prompts/memories/stage_one_input.md +1 -2
- package/src/prompts/tools/bash.md +10 -23
- package/src/prompts/tools/read.md +2 -0
- package/src/sdk.ts +23 -6
- package/src/session/agent-session.ts +252 -44
- package/src/session/auth-storage.ts +12 -0
- package/src/tools/bash-skill-urls.ts +177 -0
- package/src/tools/bash.ts +7 -1
- package/src/tools/read.ts +2 -2
package/src/main.ts
CHANGED
|
@@ -477,6 +477,11 @@ async function buildSessionOptions(
|
|
|
477
477
|
settings.override("skills.includeSkills", parsed.skills as string[]);
|
|
478
478
|
}
|
|
479
479
|
|
|
480
|
+
// Rules
|
|
481
|
+
if (parsed.noRules) {
|
|
482
|
+
options.rules = [];
|
|
483
|
+
}
|
|
484
|
+
|
|
480
485
|
// Additional extension paths from CLI
|
|
481
486
|
const cliExtensionPaths = parsed.noExtensions ? [] : [...(parsed.extensions ?? []), ...(parsed.hooks ?? [])];
|
|
482
487
|
if (cliExtensionPaths.length > 0) {
|
package/src/memories/index.ts
CHANGED
|
@@ -168,7 +168,6 @@ export async function buildMemoryToolDeveloperInstructions(
|
|
|
168
168
|
if (!truncated.trim()) return undefined;
|
|
169
169
|
|
|
170
170
|
return renderPromptTemplate(readPathTemplate, {
|
|
171
|
-
base_path: memoryRoot,
|
|
172
171
|
memory_summary: truncated,
|
|
173
172
|
});
|
|
174
173
|
}
|
|
@@ -575,8 +574,7 @@ async function runStage1Job(options: {
|
|
|
575
574
|
const budgetTokens = Math.floor(modelMaxTokens * config.rolloutPayloadPercent);
|
|
576
575
|
const truncatedItems = truncateByApproxTokens(serializedItems, budgetTokens);
|
|
577
576
|
const inputPrompt = renderPromptTemplate(stageOneInputTemplate, {
|
|
578
|
-
|
|
579
|
-
cwd: claim.cwd,
|
|
577
|
+
thread_id: claim.threadId,
|
|
580
578
|
response_items_json: truncatedItems,
|
|
581
579
|
});
|
|
582
580
|
|
|
@@ -635,13 +633,9 @@ async function syncPhase2Artifacts(memoryRoot: string, outputs: Stage1OutputRow[
|
|
|
635
633
|
const stem = formatRolloutFilename(row.threadId, row.rolloutSlug);
|
|
636
634
|
const filename = `${stem}.md`;
|
|
637
635
|
keepFiles.add(filename);
|
|
638
|
-
const body = [
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
`cwd: ${row.cwd}`,
|
|
642
|
-
"",
|
|
643
|
-
row.rolloutSummary,
|
|
644
|
-
].join("\n");
|
|
636
|
+
const body = [`thread_id: ${row.threadId}`, `updated_at: ${row.sourceUpdatedAt}`, "", row.rolloutSummary].join(
|
|
637
|
+
"\n",
|
|
638
|
+
);
|
|
645
639
|
await Bun.write(path.join(summariesDir, filename), `${body.trim()}\n`);
|
|
646
640
|
}
|
|
647
641
|
|
|
@@ -668,7 +662,7 @@ function buildRawMemoriesMarkdown(outputs: Stage1OutputRow[]): string {
|
|
|
668
662
|
}
|
|
669
663
|
|
|
670
664
|
const blocks = outputs.map(row => {
|
|
671
|
-
const header = [`## ${row.threadId}`, `updated_at: ${row.sourceUpdatedAt}`,
|
|
665
|
+
const header = [`## ${row.threadId}`, `updated_at: ${row.sourceUpdatedAt}`, ""].join("\n");
|
|
672
666
|
return `${header}${row.rawMemory.trim()}\n`;
|
|
673
667
|
});
|
|
674
668
|
return `# Raw Memories\n\n${blocks.join("\n")}`;
|
|
@@ -707,7 +701,6 @@ async function runConsolidationModel(options: { memoryRoot: string; model: Model
|
|
|
707
701
|
const rawMemories = await Bun.file(path.join(memoryRoot, "raw_memories.md")).text();
|
|
708
702
|
const rolloutSummaries = await readRolloutSummaries(memoryRoot);
|
|
709
703
|
const input = renderPromptTemplate(consolidationTemplate, {
|
|
710
|
-
memory_root: memoryRoot,
|
|
711
704
|
raw_memories: truncateByApproxTokens(rawMemories, 20_000),
|
|
712
705
|
rollout_summaries: truncateByApproxTokens(rolloutSummaries, 12_000),
|
|
713
706
|
});
|
|
@@ -1077,7 +1070,7 @@ function loadMemoryConfig(settings: Settings): MemoryRuntimeConfig {
|
|
|
1077
1070
|
};
|
|
1078
1071
|
}
|
|
1079
1072
|
|
|
1080
|
-
function getMemoryRoot(agentDir: string, cwd: string): string {
|
|
1073
|
+
export function getMemoryRoot(agentDir: string, cwd: string): string {
|
|
1081
1074
|
return path.join(agentDir, "memories", encodeProjectPath(cwd));
|
|
1082
1075
|
}
|
|
1083
1076
|
|
|
@@ -139,6 +139,12 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
|
|
|
139
139
|
{ value: "20", label: "20 messages" },
|
|
140
140
|
{ value: "30", label: "30 messages" },
|
|
141
141
|
],
|
|
142
|
+
"ttsr.interruptMode": [
|
|
143
|
+
{ value: "always", label: "always", description: "Interrupt on prose and tool streams" },
|
|
144
|
+
{ value: "prose-only", label: "prose-only", description: "Interrupt only on reply/thinking matches" },
|
|
145
|
+
{ value: "tool-only", label: "tool-only", description: "Interrupt only on tool-call argument matches" },
|
|
146
|
+
{ value: "never", label: "never", description: "Never interrupt; inject warning after completion" },
|
|
147
|
+
],
|
|
142
148
|
// Virtual terminal
|
|
143
149
|
"bash.virtualTerminal": [
|
|
144
150
|
{ value: "on", label: "On", description: "PTY-backed interactive execution" },
|
|
@@ -28,6 +28,8 @@ export interface RpcClientOptions {
|
|
|
28
28
|
provider?: string;
|
|
29
29
|
/** Model ID to use */
|
|
30
30
|
model?: string;
|
|
31
|
+
/** Session directory for the agent */
|
|
32
|
+
sessionDir?: string;
|
|
31
33
|
/** Additional CLI arguments */
|
|
32
34
|
args?: string[];
|
|
33
35
|
}
|
|
@@ -108,6 +110,9 @@ export class RpcClient {
|
|
|
108
110
|
if (this.options.model) {
|
|
109
111
|
args.push("--model", this.options.model);
|
|
110
112
|
}
|
|
113
|
+
if (this.options.sessionDir) {
|
|
114
|
+
args.push("--session-dir", this.options.sessionDir);
|
|
115
|
+
}
|
|
111
116
|
if (this.options.args) {
|
|
112
117
|
args.push(...this.options.args);
|
|
113
118
|
}
|
|
@@ -153,6 +158,17 @@ export class RpcClient {
|
|
|
153
158
|
this.#pendingRequests.clear();
|
|
154
159
|
}
|
|
155
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Stop the RPC agent process and clean up resources.
|
|
163
|
+
*/
|
|
164
|
+
[Symbol.dispose](): void {
|
|
165
|
+
try {
|
|
166
|
+
this.stop();
|
|
167
|
+
} catch {
|
|
168
|
+
// Ignore cleanup errors
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
156
172
|
/**
|
|
157
173
|
* Subscribe to agent events.
|
|
158
174
|
*/
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Memory Guidance
|
|
2
|
-
Memory root:
|
|
2
|
+
Memory root: memory://root
|
|
3
3
|
Operational rules:
|
|
4
|
-
1) Read `
|
|
5
|
-
2) If needed, inspect `
|
|
4
|
+
1) Read `memory://root/memory_summary.md` first.
|
|
5
|
+
2) If needed, inspect `memory://root/MEMORY.md` and `memory://root/skills/<name>/SKILL.md`.
|
|
6
6
|
3) Decision boundary: trust memory for heuristics/process context; trust current repo files, runtime output, and user instruction for factual state and final decisions.
|
|
7
|
-
4) Citation policy: when memory changes your plan, cite the memory artifact path you used (for example `
|
|
7
|
+
4) Citation policy: when memory changes your plan, cite the memory artifact path you used (for example `memory://root/skills/<name>/SKILL.md`) and pair it with current-repo evidence before acting.
|
|
8
8
|
5) Conflict workflow: if memory disagrees with repo state or user instruction, prefer repo/user, treat memory as stale, proceed with corrected behavior, then update/regenerate memory artifacts through normal execution.
|
|
9
9
|
6) Escalate confidence only after repository verification; memory alone is never sufficient proof.
|
|
10
10
|
Memory summary:
|
|
@@ -4,34 +4,21 @@ Executes bash command in shell session for terminal operations like git, bun, ca
|
|
|
4
4
|
|
|
5
5
|
<instruction>
|
|
6
6
|
- Use `cwd` parameter to set working directory instead of `cd dir && ...`
|
|
7
|
-
- Paths with spaces must use double quotes: `cd "/path/with spaces"`
|
|
8
|
-
- For sequential dependent operations, chain with `&&`: `mkdir foo && cd foo && touch bar`
|
|
9
|
-
- For parallel independent operations, make multiple tool calls in one message
|
|
10
7
|
- Use `;` only when later commands should run regardless of earlier failures
|
|
8
|
+
- `skill://` URIs are auto-resolved to filesystem paths before execution
|
|
9
|
+
- `python skill://my-skill/scripts/init.py` runs the script from the skill directory
|
|
10
|
+
- `skill://<name>/<relative-path>` resolves within the skill's base directory
|
|
11
|
+
- `agent://`, `artifact://`, `plan://`, `memory://`, and `rule://` URIs are also auto-resolved to filesystem paths before execution
|
|
11
12
|
</instruction>
|
|
12
13
|
|
|
13
14
|
<output>
|
|
14
|
-
Returns
|
|
15
|
-
- Output truncated after 50KB or 2000 lines (whichever first); use `head` parameter to limit output
|
|
15
|
+
Returns the output, and an exit code from command execution.
|
|
16
16
|
- If output truncated, full output stored under $ARTIFACTS and referenced as `artifact://<id>` in metadata
|
|
17
|
-
- Exit codes shown on non-zero exit
|
|
17
|
+
- Exit codes shown on non-zero exit
|
|
18
18
|
</output>
|
|
19
19
|
|
|
20
20
|
<critical>
|
|
21
|
-
Do NOT use Bash for these operations
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
- Content-addressed edits → Edit tool
|
|
26
|
-
- Writing new files → Write tool
|
|
27
|
-
</critical>
|
|
28
|
-
|
|
29
|
-
<avoid>
|
|
30
|
-
Do NOT pipe through `head` or `tail`—use `head` and `tail` parameters instead:
|
|
31
|
-
- `command | head -n 50` → use `head: 50` parameter
|
|
32
|
-
- `command | tail -n 100` → use `tail: 100` parameter
|
|
33
|
-
|
|
34
|
-
Pipe pattern breaks streaming output and prevents artifact storage.
|
|
35
|
-
|
|
36
|
-
Do NOT use `2>&1`—stdout and stderr already merged.
|
|
37
|
-
</avoid>
|
|
21
|
+
- Do NOT use Bash for these operations like read, grep, find, edit, write, where specialized tools exist.
|
|
22
|
+
- Do NOT use `2>&1` pattern, stdout and stderr are already merged.
|
|
23
|
+
- Do NOT use `| head -n 50` or `| tail -n 100` pattern, use `head` and `tail` parameters instead.
|
|
24
|
+
</critical>
|
|
@@ -21,6 +21,8 @@ Reads files from local filesystem or internal URLs.
|
|
|
21
21
|
- `skill://<name>` - read SKILL.md for a skill
|
|
22
22
|
- `skill://<name>/<path>` - read relative path within skill directory
|
|
23
23
|
- `rule://<name>` - read rule content
|
|
24
|
+
- `memory://root` - read memory summary (`memory_summary.md`)
|
|
25
|
+
- `memory://root/<path>` - read relative path within project memory root
|
|
24
26
|
- `agent://<id>` - read agent output artifact
|
|
25
27
|
- `agent://<id>/<path>` or `agent://<id>?q=<query>` - extract JSON from agent output
|
|
26
28
|
</instruction>
|
package/src/sdk.ts
CHANGED
|
@@ -40,13 +40,14 @@ import {
|
|
|
40
40
|
AgentProtocolHandler,
|
|
41
41
|
ArtifactProtocolHandler,
|
|
42
42
|
InternalUrlRouter,
|
|
43
|
+
MemoryProtocolHandler,
|
|
43
44
|
PlanProtocolHandler,
|
|
44
45
|
RuleProtocolHandler,
|
|
45
46
|
SkillProtocolHandler,
|
|
46
47
|
} from "./internal-urls";
|
|
47
48
|
import { disposeAllKernelSessions } from "./ipy/executor";
|
|
48
49
|
import { discoverAndLoadMCPTools, type MCPManager, type MCPToolsLoadResult } from "./mcp";
|
|
49
|
-
import { buildMemoryToolDeveloperInstructions, startMemoryStartupTask } from "./memories";
|
|
50
|
+
import { buildMemoryToolDeveloperInstructions, getMemoryRoot, startMemoryStartupTask } from "./memories";
|
|
50
51
|
import { collectEnvSecrets, loadSecrets, obfuscateMessages, SecretObfuscator } from "./secrets";
|
|
51
52
|
import { AgentSession } from "./session/agent-session";
|
|
52
53
|
import { AuthStorage } from "./session/auth-storage";
|
|
@@ -133,6 +134,8 @@ export interface CreateAgentSessionOptions {
|
|
|
133
134
|
skills?: Skill[];
|
|
134
135
|
/** Skills to inline into the system prompt instead of listing available skills. */
|
|
135
136
|
preloadedSkills?: Skill[];
|
|
137
|
+
/** Rules. Default: discovered from multiple locations */
|
|
138
|
+
rules?: Rule[];
|
|
136
139
|
/** Context files (AGENTS.md content). Default: discovered walking up from cwd */
|
|
137
140
|
contextFiles?: Array<{ path: string; content: string }>;
|
|
138
141
|
/** Prompt templates. Default: discovered from cwd/.omp/prompts/ + agentDir/prompts/ */
|
|
@@ -671,17 +674,26 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
671
674
|
// Discover rules
|
|
672
675
|
const ttsrSettings = settings.getGroup("ttsr");
|
|
673
676
|
const ttsrManager = new TtsrManager(ttsrSettings);
|
|
674
|
-
const rulesResult =
|
|
677
|
+
const rulesResult =
|
|
678
|
+
options.rules !== undefined
|
|
679
|
+
? { items: options.rules, warnings: undefined }
|
|
680
|
+
: await loadCapability<Rule>(ruleCapability.id, { cwd });
|
|
681
|
+
const registeredTtsrRuleNames = new Set<string>();
|
|
675
682
|
for (const rule of rulesResult.items) {
|
|
676
|
-
if (rule.
|
|
677
|
-
ttsrManager.addRule(rule)
|
|
683
|
+
if (rule.condition && rule.condition.length > 0) {
|
|
684
|
+
if (ttsrManager.addRule(rule)) {
|
|
685
|
+
registeredTtsrRuleNames.add(rule.name);
|
|
686
|
+
}
|
|
678
687
|
}
|
|
679
688
|
}
|
|
689
|
+
if (existingSession.injectedTtsrRules.length > 0) {
|
|
690
|
+
ttsrManager.restoreInjected(existingSession.injectedTtsrRules);
|
|
691
|
+
}
|
|
680
692
|
time("discoverTtsrRules");
|
|
681
693
|
|
|
682
694
|
// Filter rules for the rulebook (non-TTSR, non-alwaysApply, with descriptions)
|
|
683
695
|
const rulebookRules = rulesResult.items.filter((rule: Rule) => {
|
|
684
|
-
if (rule.
|
|
696
|
+
if (registeredTtsrRuleNames.has(rule.name)) return false;
|
|
685
697
|
if (rule.alwaysApply) return false;
|
|
686
698
|
if (!rule.description) return false;
|
|
687
699
|
return true;
|
|
@@ -729,7 +741,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
729
741
|
modelRegistry,
|
|
730
742
|
};
|
|
731
743
|
|
|
732
|
-
// Initialize internal URL router for agent
|
|
744
|
+
// Initialize internal URL router for internal protocols (agent://, artifact://, plan://, memory://, skill://, rule://)
|
|
733
745
|
const internalRouter = new InternalUrlRouter();
|
|
734
746
|
const getArtifactsDir = () => {
|
|
735
747
|
const sessionFile = sessionManager.getSessionFile();
|
|
@@ -743,6 +755,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
743
755
|
cwd,
|
|
744
756
|
}),
|
|
745
757
|
);
|
|
758
|
+
internalRouter.register(
|
|
759
|
+
new MemoryProtocolHandler({
|
|
760
|
+
getMemoryRoot: () => getMemoryRoot(agentDir, settings.getCwd()),
|
|
761
|
+
}),
|
|
762
|
+
);
|
|
746
763
|
internalRouter.register(
|
|
747
764
|
new SkillProtocolHandler({
|
|
748
765
|
getSkills: () => skills,
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import * as fs from "node:fs";
|
|
17
|
+
import * as path from "node:path";
|
|
17
18
|
|
|
18
19
|
import type { Agent, AgentEvent, AgentMessage, AgentState, AgentTool, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
19
20
|
import type {
|
|
@@ -43,7 +44,7 @@ import {
|
|
|
43
44
|
import type { Settings, SkillsSettings } from "../config/settings";
|
|
44
45
|
import { type BashResult, executeBash as executeBashCommand } from "../exec/bash-executor";
|
|
45
46
|
import { exportSessionToHtml } from "../export/html";
|
|
46
|
-
import type { TtsrManager } from "../export/ttsr";
|
|
47
|
+
import type { TtsrManager, TtsrMatchContext } from "../export/ttsr";
|
|
47
48
|
import type { LoadedCustomCommand } from "../extensibility/custom-commands";
|
|
48
49
|
import type { CustomTool, CustomToolContext } from "../extensibility/custom-tools/types";
|
|
49
50
|
import { CustomToolAdapter } from "../extensibility/custom-tools/wrapper";
|
|
@@ -346,6 +347,7 @@ export class AgentSession {
|
|
|
346
347
|
#ttsrManager: TtsrManager | undefined = undefined;
|
|
347
348
|
#pendingTtsrInjections: Rule[] = [];
|
|
348
349
|
#ttsrAbortPending = false;
|
|
350
|
+
#ttsrRetryToken = 0;
|
|
349
351
|
|
|
350
352
|
#streamingEditAbortTriggered = false;
|
|
351
353
|
#streamingEditCheckedLineCounts = new Map<string, number>();
|
|
@@ -456,46 +458,82 @@ export class AgentSession {
|
|
|
456
458
|
this.#ttsrManager.incrementMessageCount();
|
|
457
459
|
}
|
|
458
460
|
|
|
459
|
-
// TTSR: Check for pattern matches on text
|
|
461
|
+
// TTSR: Check for pattern matches on assistant text/thinking and tool argument deltas
|
|
460
462
|
if (event.type === "message_update" && this.#ttsrManager?.hasRules()) {
|
|
461
463
|
const assistantEvent = event.assistantMessageEvent;
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
464
|
+
let matchContext: TtsrMatchContext | undefined;
|
|
465
|
+
|
|
466
|
+
if (assistantEvent.type === "text_delta") {
|
|
467
|
+
matchContext = { source: "text" };
|
|
468
|
+
} else if (assistantEvent.type === "thinking_delta") {
|
|
469
|
+
matchContext = { source: "thinking" };
|
|
470
|
+
} else if (assistantEvent.type === "toolcall_delta") {
|
|
471
|
+
matchContext = this.#getTtsrToolMatchContext(event.message, assistantEvent.contentIndex);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (matchContext && "delta" in assistantEvent) {
|
|
475
|
+
const matches = this.#ttsrManager.checkDelta(assistantEvent.delta, matchContext);
|
|
466
476
|
if (matches.length > 0) {
|
|
467
|
-
//
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
477
|
+
// Queue rules for injection; mark as injected only after successful enqueue.
|
|
478
|
+
|
|
479
|
+
this.#addPendingTtsrInjections(matches);
|
|
480
|
+
|
|
481
|
+
if (this.#shouldInterruptForTtsrMatch(matchContext)) {
|
|
482
|
+
// Abort the stream immediately — do not gate on extension callbacks
|
|
483
|
+
this.#ttsrAbortPending = true;
|
|
484
|
+
this.agent.abort();
|
|
485
|
+
// Notify extensions (fire-and-forget, does not block abort)
|
|
486
|
+
this.#emitSessionEvent({ type: "ttsr_triggered", rules: matches }).catch(() => {});
|
|
487
|
+
// Schedule retry after a short delay
|
|
488
|
+
const retryToken = ++this.#ttsrRetryToken;
|
|
489
|
+
const generation = this.#promptGeneration;
|
|
490
|
+
const targetMessageTimestamp =
|
|
491
|
+
event.message.role === "assistant" ? event.message.timestamp : undefined;
|
|
492
|
+
setTimeout(async () => {
|
|
493
|
+
if (this.#ttsrRetryToken !== retryToken) {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
486
496
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
497
|
+
const targetAssistantIndex = this.#findTtsrAssistantIndex(targetMessageTimestamp);
|
|
498
|
+
if (
|
|
499
|
+
!this.#ttsrAbortPending ||
|
|
500
|
+
this.#promptGeneration !== generation ||
|
|
501
|
+
targetAssistantIndex === -1
|
|
502
|
+
) {
|
|
503
|
+
this.#ttsrAbortPending = false;
|
|
504
|
+
this.#pendingTtsrInjections = [];
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
this.#ttsrAbortPending = false;
|
|
508
|
+
const ttsrSettings = this.#ttsrManager?.getSettings();
|
|
509
|
+
if (ttsrSettings?.contextMode === "discard") {
|
|
510
|
+
// Remove the partial/aborted assistant turn from agent state
|
|
511
|
+
this.agent.replaceMessages(this.agent.state.messages.slice(0, targetAssistantIndex));
|
|
512
|
+
}
|
|
513
|
+
// Inject TTSR rules as system reminder before retry
|
|
514
|
+
const injection = this.#getTtsrInjectionContent();
|
|
515
|
+
if (injection) {
|
|
516
|
+
const details = { rules: injection.rules.map(rule => rule.name) };
|
|
517
|
+
this.agent.appendMessage({
|
|
518
|
+
role: "custom",
|
|
519
|
+
customType: "ttsr-injection",
|
|
520
|
+
content: injection.content,
|
|
521
|
+
display: false,
|
|
522
|
+
details,
|
|
523
|
+
timestamp: Date.now(),
|
|
524
|
+
});
|
|
525
|
+
this.sessionManager.appendCustomMessageEntry(
|
|
526
|
+
"ttsr-injection",
|
|
527
|
+
injection.content,
|
|
528
|
+
false,
|
|
529
|
+
details,
|
|
530
|
+
);
|
|
531
|
+
this.#markTtsrInjected(details.rules);
|
|
532
|
+
}
|
|
533
|
+
this.agent.continue().catch(() => {});
|
|
534
|
+
}, 50);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
499
537
|
}
|
|
500
538
|
}
|
|
501
539
|
}
|
|
@@ -522,6 +560,9 @@ export class AgentSession {
|
|
|
522
560
|
event.message.display,
|
|
523
561
|
event.message.details,
|
|
524
562
|
);
|
|
563
|
+
if (event.message.role === "custom" && event.message.customType === "ttsr-injection") {
|
|
564
|
+
this.#markTtsrInjected(this.#extractTtsrRuleNames(event.message.details));
|
|
565
|
+
}
|
|
525
566
|
} else if (
|
|
526
567
|
event.message.role === "user" ||
|
|
527
568
|
event.message.role === "assistant" ||
|
|
@@ -536,10 +577,8 @@ export class AgentSession {
|
|
|
536
577
|
// Track assistant message for auto-compaction (checked on agent_end)
|
|
537
578
|
if (event.message.role === "assistant") {
|
|
538
579
|
this.#lastAssistantMessage = event.message;
|
|
539
|
-
|
|
540
|
-
// Reset retry counter immediately on successful assistant response
|
|
541
|
-
// This prevents accumulation across multiple LLM calls within a turn
|
|
542
580
|
const assistantMsg = event.message as AssistantMessage;
|
|
581
|
+
this.#queueDeferredTtsrInjectionIfNeeded(assistantMsg);
|
|
543
582
|
if (
|
|
544
583
|
assistantMsg.stopReason !== "error" &&
|
|
545
584
|
assistantMsg.stopReason !== "aborted" &&
|
|
@@ -622,16 +661,185 @@ export class AgentSession {
|
|
|
622
661
|
}
|
|
623
662
|
}
|
|
624
663
|
|
|
625
|
-
/** Get TTSR injection
|
|
626
|
-
#getTtsrInjectionContent(): string | undefined {
|
|
664
|
+
/** Get TTSR injection payload and clear pending injections. */
|
|
665
|
+
#getTtsrInjectionContent(): { content: string; rules: Rule[] } | undefined {
|
|
627
666
|
if (this.#pendingTtsrInjections.length === 0) return undefined;
|
|
628
|
-
const
|
|
667
|
+
const rules = this.#pendingTtsrInjections;
|
|
668
|
+
const content = rules
|
|
629
669
|
.map(r => renderPromptTemplate(ttsrInterruptTemplate, { name: r.name, path: r.path, content: r.content }))
|
|
630
670
|
.join("\n\n");
|
|
631
671
|
this.#pendingTtsrInjections = [];
|
|
632
|
-
return content;
|
|
672
|
+
return { content, rules };
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
#addPendingTtsrInjections(rules: Rule[]): void {
|
|
676
|
+
const seen = new Set(this.#pendingTtsrInjections.map(rule => rule.name));
|
|
677
|
+
for (const rule of rules) {
|
|
678
|
+
if (seen.has(rule.name)) continue;
|
|
679
|
+
this.#pendingTtsrInjections.push(rule);
|
|
680
|
+
seen.add(rule.name);
|
|
681
|
+
}
|
|
633
682
|
}
|
|
634
683
|
|
|
684
|
+
#extractTtsrRuleNames(details: unknown): string[] {
|
|
685
|
+
if (!details || typeof details !== "object" || Array.isArray(details)) {
|
|
686
|
+
return [];
|
|
687
|
+
}
|
|
688
|
+
const rules = (details as { rules?: unknown }).rules;
|
|
689
|
+
if (!Array.isArray(rules)) {
|
|
690
|
+
return [];
|
|
691
|
+
}
|
|
692
|
+
return rules.filter((ruleName): ruleName is string => typeof ruleName === "string");
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
#markTtsrInjected(ruleNames: string[]): void {
|
|
696
|
+
const uniqueRuleNames = Array.from(
|
|
697
|
+
new Set(ruleNames.map(ruleName => ruleName.trim()).filter(ruleName => ruleName.length > 0)),
|
|
698
|
+
);
|
|
699
|
+
if (uniqueRuleNames.length === 0) {
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
this.#ttsrManager?.markInjectedByNames(uniqueRuleNames);
|
|
703
|
+
this.sessionManager.appendTtsrInjection(uniqueRuleNames);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
#findTtsrAssistantIndex(targetTimestamp: number | undefined): number {
|
|
707
|
+
const messages = this.agent.state.messages;
|
|
708
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
709
|
+
const message = messages[i];
|
|
710
|
+
if (message.role !== "assistant") {
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
if (targetTimestamp === undefined || message.timestamp === targetTimestamp) {
|
|
714
|
+
return i;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return -1;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
#shouldInterruptForTtsrMatch(matchContext: TtsrMatchContext): boolean {
|
|
721
|
+
const mode = this.#ttsrManager?.getSettings().interruptMode ?? "always";
|
|
722
|
+
if (mode === "never") {
|
|
723
|
+
return false;
|
|
724
|
+
}
|
|
725
|
+
if (mode === "prose-only") {
|
|
726
|
+
return matchContext.source === "text" || matchContext.source === "thinking";
|
|
727
|
+
}
|
|
728
|
+
if (mode === "tool-only") {
|
|
729
|
+
return matchContext.source === "tool";
|
|
730
|
+
}
|
|
731
|
+
return true;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
#queueDeferredTtsrInjectionIfNeeded(assistantMsg: AssistantMessage): void {
|
|
735
|
+
if (this.#ttsrAbortPending || this.#pendingTtsrInjections.length === 0) {
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
if (assistantMsg.stopReason === "aborted" || assistantMsg.stopReason === "error") {
|
|
739
|
+
this.#pendingTtsrInjections = [];
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const injection = this.#getTtsrInjectionContent();
|
|
744
|
+
if (!injection) {
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
this.agent.followUp({
|
|
748
|
+
role: "custom",
|
|
749
|
+
customType: "ttsr-injection",
|
|
750
|
+
content: injection.content,
|
|
751
|
+
display: false,
|
|
752
|
+
details: { rules: injection.rules.map(rule => rule.name) },
|
|
753
|
+
timestamp: Date.now(),
|
|
754
|
+
});
|
|
755
|
+
// Mark as injected after this custom message is delivered and persisted (handled in message_end).
|
|
756
|
+
// followUp() only enqueues; resume on the next tick once streaming settles.
|
|
757
|
+
setTimeout(() => {
|
|
758
|
+
if (this.agent.state.isStreaming || !this.agent.hasQueuedMessages()) {
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
this.agent.continue().catch(() => {});
|
|
762
|
+
}, 0);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/** Build TTSR match context for tool call argument deltas. */
|
|
766
|
+
#getTtsrToolMatchContext(message: AgentMessage, contentIndex: number): TtsrMatchContext {
|
|
767
|
+
const context: TtsrMatchContext = { source: "tool" };
|
|
768
|
+
if (message.role !== "assistant") {
|
|
769
|
+
return context;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const content = message.content;
|
|
773
|
+
if (!Array.isArray(content) || contentIndex < 0 || contentIndex >= content.length) {
|
|
774
|
+
return context;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const block = content[contentIndex];
|
|
778
|
+
if (!block || typeof block !== "object" || block.type !== "toolCall") {
|
|
779
|
+
return context;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const toolCall = block as ToolCall;
|
|
783
|
+
context.toolName = toolCall.name;
|
|
784
|
+
context.streamKey = toolCall.id ? `toolcall:${toolCall.id}` : `tool:${toolCall.name}:${contentIndex}`;
|
|
785
|
+
context.filePaths = this.#extractTtsrFilePathsFromArgs(toolCall.arguments);
|
|
786
|
+
return context;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/** Extract path-like arguments from tool call payload for TTSR glob matching. */
|
|
790
|
+
#extractTtsrFilePathsFromArgs(args: unknown): string[] | undefined {
|
|
791
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) {
|
|
792
|
+
return undefined;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const rawPaths: string[] = [];
|
|
796
|
+
for (const [key, value] of Object.entries(args)) {
|
|
797
|
+
const normalizedKey = key.toLowerCase();
|
|
798
|
+
if (typeof value === "string" && (normalizedKey === "path" || normalizedKey.endsWith("path"))) {
|
|
799
|
+
rawPaths.push(value);
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
if (Array.isArray(value) && (normalizedKey === "paths" || normalizedKey.endsWith("paths"))) {
|
|
803
|
+
for (const candidate of value) {
|
|
804
|
+
if (typeof candidate === "string") {
|
|
805
|
+
rawPaths.push(candidate);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const normalizedPaths = rawPaths.flatMap(pathValue => this.#normalizeTtsrPathCandidates(pathValue));
|
|
812
|
+
if (normalizedPaths.length === 0) {
|
|
813
|
+
return undefined;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
return Array.from(new Set(normalizedPaths));
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/** Convert a path argument into stable relative/absolute candidates for glob checks. */
|
|
820
|
+
#normalizeTtsrPathCandidates(rawPath: string): string[] {
|
|
821
|
+
const trimmed = rawPath.trim();
|
|
822
|
+
if (trimmed.length === 0) {
|
|
823
|
+
return [];
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const normalizedInput = trimmed.replaceAll("\\", "/");
|
|
827
|
+
const candidates = new Set<string>([normalizedInput]);
|
|
828
|
+
if (normalizedInput.startsWith("./")) {
|
|
829
|
+
candidates.add(normalizedInput.slice(2));
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const cwd = this.sessionManager.getCwd();
|
|
833
|
+
const absolutePath = path.isAbsolute(trimmed) ? path.normalize(trimmed) : path.resolve(cwd, trimmed);
|
|
834
|
+
candidates.add(absolutePath.replaceAll("\\", "/"));
|
|
835
|
+
|
|
836
|
+
const relativePath = path.relative(cwd, absolutePath).replaceAll("\\", "/");
|
|
837
|
+
if (relativePath && relativePath !== "." && !relativePath.startsWith("../") && relativePath !== "..") {
|
|
838
|
+
candidates.add(relativePath);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
return Array.from(candidates);
|
|
842
|
+
}
|
|
635
843
|
/** Extract text content from a message */
|
|
636
844
|
#getUserMessageText(message: Message): string {
|
|
637
845
|
if (message.role !== "user") return "";
|