@oh-my-pi/pi-coding-agent 15.10.3 → 15.10.5
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 +72 -0
- package/dist/types/capability/rule-buckets.d.ts +1 -1
- package/dist/types/capability/rule.d.ts +6 -1
- package/dist/types/cli/update-cli.d.ts +11 -1
- package/dist/types/config/model-registry.d.ts +18 -1
- package/dist/types/discovery/at-imports.d.ts +15 -0
- package/dist/types/edit/diff.d.ts +3 -2
- package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +1 -0
- package/dist/types/eval/__tests__/js-context-manager.test.d.ts +1 -0
- package/dist/types/eval/backend.d.ts +7 -0
- package/dist/types/eval/bridge-timeout.d.ts +1 -1
- package/dist/types/eval/{llm-bridge.d.ts → completion-bridge.d.ts} +8 -8
- package/dist/types/eval/idle-timeout.d.ts +1 -1
- package/dist/types/eval/js/context-manager.d.ts +1 -0
- package/dist/types/eval/js/executor.d.ts +2 -0
- package/dist/types/eval/js/index.d.ts +1 -1
- package/dist/types/eval/js/shared/helpers.d.ts +6 -0
- package/dist/types/eval/js/shared/runtime.d.ts +5 -0
- package/dist/types/eval/js/worker-protocol.d.ts +6 -0
- package/dist/types/eval/py/executor.d.ts +7 -0
- package/dist/types/eval/py/index.d.ts +1 -1
- package/dist/types/export/ttsr.d.ts +14 -0
- package/dist/types/extensibility/extensions/types.d.ts +8 -1
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +1 -1
- package/dist/types/internal-urls/local-protocol.d.ts +10 -0
- package/dist/types/mcp/oauth-flow.d.ts +2 -2
- package/dist/types/modes/components/custom-editor.d.ts +3 -0
- package/dist/types/modes/components/{status-line.d.ts → status-line/component.d.ts} +2 -32
- package/dist/types/modes/components/status-line/index.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +31 -2
- package/dist/types/modes/image-references.d.ts +8 -3
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +2 -1
- package/dist/types/modes/utils/ui-helpers.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +0 -2
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/browser/tab-worker.d.ts +15 -0
- package/dist/types/tools/index.d.ts +17 -0
- package/dist/types/tools/render-utils.d.ts +1 -1
- package/dist/types/tools/tool-timeouts.d.ts +1 -1
- package/dist/types/utils/block-context.d.ts +35 -0
- package/dist/types/utils/image-loading.d.ts +12 -0
- package/package.json +29 -9
- package/src/capability/rule-buckets.ts +4 -2
- package/src/capability/rule.ts +10 -1
- package/src/cli/auth-broker-cli.ts +6 -7
- package/src/cli/auth-gateway-cli.ts +1 -1
- package/src/cli/list-models.ts +5 -0
- package/src/cli/update-cli.ts +138 -16
- package/src/config/model-registry.ts +81 -2
- package/src/debug/index.ts +4 -8
- package/src/discovery/at-imports.ts +273 -0
- package/src/discovery/builtin-rules/index.ts +4 -0
- package/src/discovery/builtin-rules/ts-no-test-timers.md +55 -0
- package/src/discovery/builtin-rules/ts-redundant-clear-guard.md +75 -0
- package/src/discovery/helpers.ts +2 -1
- package/src/edit/diff.ts +114 -4
- package/src/edit/hashline/diff.ts +1 -1
- package/src/edit/hashline/execute.ts +1 -1
- package/src/edit/modes/patch.ts +6 -2
- package/src/edit/modes/replace.ts +1 -1
- package/src/edit/renderer.ts +12 -2
- package/src/eval/__tests__/agent-bridge.test.ts +13 -0
- package/src/eval/__tests__/{llm-bridge.test.ts → completion-bridge.test.ts} +60 -54
- package/src/eval/__tests__/helpers-local-roots.test.ts +58 -0
- package/src/eval/__tests__/js-context-manager.test.ts +241 -0
- package/src/eval/agent-bridge.ts +6 -1
- package/src/eval/backend.ts +15 -0
- package/src/eval/bridge-timeout.ts +1 -1
- package/src/eval/{llm-bridge.ts → completion-bridge.ts} +30 -27
- package/src/eval/idle-timeout.ts +1 -1
- package/src/eval/js/context-manager.ts +70 -8
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/index.ts +7 -1
- package/src/eval/js/shared/helpers.ts +53 -6
- package/src/eval/js/shared/prelude.txt +4 -4
- package/src/eval/js/shared/runtime.ts +8 -0
- package/src/eval/js/tool-bridge.ts +3 -3
- package/src/eval/js/worker-core.ts +1 -0
- package/src/eval/js/worker-entry.ts +6 -0
- package/src/eval/js/worker-protocol.ts +6 -0
- package/src/eval/py/executor.ts +12 -0
- package/src/eval/py/index.ts +7 -1
- package/src/eval/py/prelude.py +46 -7
- package/src/eval/py/runner.py +1 -0
- package/src/exa/render.ts +1 -1
- package/src/export/ttsr.ts +122 -1
- package/src/extensibility/extensions/types.ts +8 -1
- package/src/extensibility/legacy-pi-ai-shim.ts +1 -1
- package/src/extensibility/plugins/doctor.ts +1 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +6 -5
- package/src/goals/tools/goal-tool.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +8 -6
- package/src/internal-urls/local-protocol.ts +13 -0
- package/src/lsp/render.ts +8 -6
- package/src/mcp/oauth-flow.ts +3 -3
- package/src/mcp/render.ts +7 -1
- package/src/modes/components/custom-editor.ts +12 -6
- package/src/modes/components/login-dialog.ts +1 -1
- package/src/modes/components/oauth-selector.ts +4 -4
- package/src/modes/components/read-tool-group.ts +10 -3
- package/src/modes/components/{status-line.ts → status-line/component.ts} +18 -40
- package/src/modes/components/status-line/index.ts +1 -0
- package/src/modes/components/status-line/types.ts +23 -8
- package/src/modes/components/tips.txt +1 -1
- package/src/modes/components/tool-execution.ts +1 -1
- package/src/modes/components/transcript-container.ts +17 -10
- package/src/modes/components/user-message.ts +6 -3
- package/src/modes/components/welcome.ts +1 -1
- package/src/modes/controllers/extension-ui-controller.ts +143 -127
- package/src/modes/controllers/input-controller.ts +36 -10
- package/src/modes/controllers/mcp-command-controller.ts +28 -12
- package/src/modes/controllers/selector-controller.ts +4 -11
- package/src/modes/controllers/ssh-command-controller.ts +2 -2
- package/src/modes/image-references.ts +13 -7
- package/src/modes/interactive-mode.ts +2 -2
- package/src/modes/rpc/rpc-mode.ts +1 -1
- package/src/modes/setup-wizard/scenes/sign-in.ts +3 -11
- package/src/modes/theme/theme.ts +95 -1
- package/src/modes/types.ts +2 -1
- package/src/modes/utils/ui-helpers.ts +14 -5
- package/src/prompts/system/tiny-title-system.md +1 -1
- package/src/prompts/system/title-system.md +16 -3
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/eval.md +6 -6
- package/src/sdk.ts +31 -14
- package/src/session/agent-session.ts +213 -155
- package/src/session/session-manager.ts +1 -1
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/system-prompt.ts +15 -9
- package/src/task/render.ts +20 -8
- package/src/tools/ask.ts +14 -5
- package/src/tools/bash-interactive.ts +1 -1
- package/src/tools/bash.ts +14 -2
- package/src/tools/browser/render.ts +5 -2
- package/src/tools/browser/tab-worker.ts +211 -91
- package/src/tools/debug.ts +5 -2
- package/src/tools/eval-render.ts +8 -5
- package/src/tools/eval.ts +2 -2
- package/src/tools/gh-renderer.ts +29 -15
- package/src/tools/index.ts +32 -0
- package/src/tools/inspect-image-renderer.ts +12 -5
- package/src/tools/job.ts +9 -6
- package/src/tools/memory-render.ts +19 -5
- package/src/tools/read.ts +165 -18
- package/src/tools/render-utils.ts +3 -1
- package/src/tools/resolve.ts +1 -1
- package/src/tools/review.ts +1 -1
- package/src/tools/ssh.ts +4 -1
- package/src/tools/todo.ts +8 -1
- package/src/tools/tool-timeouts.ts +1 -1
- package/src/tools/write.ts +1 -1
- package/src/tui/code-cell.ts +1 -1
- package/src/utils/block-context.ts +312 -0
- package/src/utils/image-loading.ts +31 -1
- package/src/utils/title-generator.ts +2 -2
- package/src/web/search/providers/codex.ts +1 -1
- package/src/web/search/render.ts +14 -6
- /package/dist/types/eval/__tests__/{llm-bridge.test.d.ts → completion-bridge.test.d.ts} +0 -0
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
* Modes use this class and add their own I/O layer on top.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import * as crypto from "node:crypto";
|
|
17
16
|
import * as fs from "node:fs";
|
|
18
17
|
import * as os from "node:os";
|
|
19
18
|
import * as path from "node:path";
|
|
@@ -33,6 +32,7 @@ import {
|
|
|
33
32
|
resolveTelemetry,
|
|
34
33
|
ThinkingLevel,
|
|
35
34
|
} from "@oh-my-pi/pi-agent-core";
|
|
35
|
+
|
|
36
36
|
import {
|
|
37
37
|
AGGRESSIVE_SHAKE_CONFIG,
|
|
38
38
|
AUTO_HANDOFF_THRESHOLD_FOCUS,
|
|
@@ -77,6 +77,7 @@ import type {
|
|
|
77
77
|
import {
|
|
78
78
|
calculateRateLimitBackoffMs,
|
|
79
79
|
clearAnthropicFastModeFallback,
|
|
80
|
+
deriveClaudeDeviceId,
|
|
80
81
|
Effort,
|
|
81
82
|
getSupportedEfforts,
|
|
82
83
|
isContextOverflow,
|
|
@@ -215,6 +216,7 @@ import { parseCommandArgs } from "../utils/command-args";
|
|
|
215
216
|
import { type EditMode, resolveEditMode } from "../utils/edit-mode";
|
|
216
217
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
217
218
|
import { extractFileMentions, generateFileMentionMessages } from "../utils/file-mentions";
|
|
219
|
+
import { normalizeModelContextImages } from "../utils/image-loading";
|
|
218
220
|
import { buildNamedToolChoice } from "../utils/tool-choice";
|
|
219
221
|
import type { AuthStorage } from "./auth-storage";
|
|
220
222
|
import type { ClientBridge, ClientBridgePermissionOption, ClientBridgePermissionOutcome } from "./client-bridge";
|
|
@@ -531,15 +533,6 @@ function formatRetryFallbackBaseSelector(selector: RetryFallbackSelector): strin
|
|
|
531
533
|
}
|
|
532
534
|
|
|
533
535
|
const IRC_REPLY_MAX_BYTES = 4096;
|
|
534
|
-
export const ANTHROPIC_TOOL_CALL_BATCH_CAP = 4;
|
|
535
|
-
const CLAUDE_OPUS_4_8_MODEL_ID = /(?:^|[./_-])claude-opus-4[.-]8\b/i;
|
|
536
|
-
|
|
537
|
-
export function resolveToolCallBatchCapForModel(model: Model | undefined): number | undefined {
|
|
538
|
-
if (!model) return undefined;
|
|
539
|
-
return model.provider === "anthropic" && CLAUDE_OPUS_4_8_MODEL_ID.test(model.id)
|
|
540
|
-
? ANTHROPIC_TOOL_CALL_BATCH_CAP
|
|
541
|
-
: undefined;
|
|
542
|
-
}
|
|
543
536
|
|
|
544
537
|
/**
|
|
545
538
|
* Collapse degenerate IRC ephemeral replies before they hit the relay.
|
|
@@ -613,14 +606,10 @@ function buildSessionMetadata(
|
|
|
613
606
|
const accountUuid = authStorage?.getOAuthAccountId("anthropic", sessionId);
|
|
614
607
|
if (typeof accountUuid === "string" && accountUuid.length > 0) {
|
|
615
608
|
userId.account_uuid = accountUuid;
|
|
616
|
-
// Claude Code's `device_id` is a stable 64-hex install
|
|
617
|
-
// omp's persistent install id
|
|
618
|
-
//
|
|
619
|
-
|
|
620
|
-
userId.device_id = crypto
|
|
621
|
-
.createHash("sha256")
|
|
622
|
-
.update(`omp-claude-device-id-v1:${getInstallId()}`)
|
|
623
|
-
.digest("hex");
|
|
609
|
+
// Claude Code's `device_id` is a stable 64-hex account-scoped install
|
|
610
|
+
// identifier. Include both omp's persistent install id and the Claude
|
|
611
|
+
// account UUID so two accounts on the same install do not share a device.
|
|
612
|
+
userId.device_id = deriveClaudeDeviceId(getInstallId(), accountUuid);
|
|
624
613
|
}
|
|
625
614
|
}
|
|
626
615
|
return { user_id: JSON.stringify(userId) };
|
|
@@ -1102,10 +1091,6 @@ export class AgentSession {
|
|
|
1102
1091
|
this.#flushPendingAgentEnd();
|
|
1103
1092
|
}
|
|
1104
1093
|
|
|
1105
|
-
#syncToolCallBatchCap(model: Model | undefined = this.model): void {
|
|
1106
|
-
this.agent.maxToolCallsPerTurn = resolveToolCallBatchCapForModel(model);
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
1094
|
#flushPendingAgentEnd(): void {
|
|
1110
1095
|
const pending = this.#pendingAgentEndEmit;
|
|
1111
1096
|
if (!pending) return;
|
|
@@ -1224,7 +1209,6 @@ export class AgentSession {
|
|
|
1224
1209
|
this.#agentId = config.agentId;
|
|
1225
1210
|
this.#agentRegistry = config.agentRegistry;
|
|
1226
1211
|
this.#providerSessionId = config.providerSessionId;
|
|
1227
|
-
this.#syncToolCallBatchCap();
|
|
1228
1212
|
this.agent.setAssistantMessageEventInterceptor((message, assistantMessageEvent) => {
|
|
1229
1213
|
const event: AgentEvent = {
|
|
1230
1214
|
type: "message_update",
|
|
@@ -1687,89 +1671,18 @@ export class AgentSession {
|
|
|
1687
1671
|
}
|
|
1688
1672
|
|
|
1689
1673
|
if (matchContext && "delta" in assistantEvent) {
|
|
1674
|
+
const targetMessageTimestamp = event.message.role === "assistant" ? event.message.timestamp : undefined;
|
|
1690
1675
|
const matches = this.#checkTtsrStream(assistantEvent.delta, matchContext, streamingToolCall);
|
|
1691
|
-
if (matches.length > 0) {
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
this.#addPendingTtsrInjections(matches);
|
|
1702
|
-
|
|
1703
|
-
if (shouldInterrupt) {
|
|
1704
|
-
// Abort the stream immediately — do not gate on extension callbacks
|
|
1705
|
-
this.#ttsrAbortPending = true;
|
|
1706
|
-
this.#ensureTtsrResumePromise();
|
|
1707
|
-
this.agent.abort(this.#formatTtsrAbortReason(matches));
|
|
1708
|
-
// Notify extensions (fire-and-forget, does not block abort)
|
|
1709
|
-
this.#emitSessionEvent({ type: "ttsr_triggered", rules: matches }).catch(() => {});
|
|
1710
|
-
// Schedule retry after a short delay
|
|
1711
|
-
const retryToken = ++this.#ttsrRetryToken;
|
|
1712
|
-
const generation = this.#promptGeneration;
|
|
1713
|
-
const targetMessageTimestamp =
|
|
1714
|
-
event.message.role === "assistant" ? event.message.timestamp : undefined;
|
|
1715
|
-
this.#schedulePostPromptTask(
|
|
1716
|
-
async () => {
|
|
1717
|
-
if (this.#ttsrRetryToken !== retryToken) {
|
|
1718
|
-
this.#resolveTtsrResume();
|
|
1719
|
-
return;
|
|
1720
|
-
}
|
|
1721
|
-
|
|
1722
|
-
const targetAssistantIndex = this.#findTtsrAssistantIndex(targetMessageTimestamp);
|
|
1723
|
-
if (
|
|
1724
|
-
!this.#ttsrAbortPending ||
|
|
1725
|
-
this.#promptGeneration !== generation ||
|
|
1726
|
-
targetAssistantIndex === -1
|
|
1727
|
-
) {
|
|
1728
|
-
this.#ttsrAbortPending = false;
|
|
1729
|
-
this.#pendingTtsrInjections = [];
|
|
1730
|
-
this.#perToolTtsrInjections.clear();
|
|
1731
|
-
this.#resolveTtsrResume();
|
|
1732
|
-
return;
|
|
1733
|
-
}
|
|
1734
|
-
this.#ttsrAbortPending = false;
|
|
1735
|
-
this.#perToolTtsrInjections.clear();
|
|
1736
|
-
const ttsrSettings = this.#ttsrManager?.getSettings();
|
|
1737
|
-
if (ttsrSettings?.contextMode === "discard") {
|
|
1738
|
-
// Remove the partial/aborted assistant turn from agent state
|
|
1739
|
-
this.agent.replaceMessages(this.agent.state.messages.slice(0, targetAssistantIndex));
|
|
1740
|
-
}
|
|
1741
|
-
// Inject TTSR rules as system reminder before retry
|
|
1742
|
-
const injection = this.#getTtsrInjectionContent();
|
|
1743
|
-
if (injection) {
|
|
1744
|
-
const details = { rules: injection.rules.map(rule => rule.name) };
|
|
1745
|
-
this.agent.appendMessage({
|
|
1746
|
-
role: "custom",
|
|
1747
|
-
customType: "ttsr-injection",
|
|
1748
|
-
content: injection.content,
|
|
1749
|
-
display: false,
|
|
1750
|
-
details,
|
|
1751
|
-
attribution: "agent",
|
|
1752
|
-
timestamp: Date.now(),
|
|
1753
|
-
});
|
|
1754
|
-
this.sessionManager.appendCustomMessageEntry(
|
|
1755
|
-
"ttsr-injection",
|
|
1756
|
-
injection.content,
|
|
1757
|
-
false,
|
|
1758
|
-
details,
|
|
1759
|
-
"agent",
|
|
1760
|
-
);
|
|
1761
|
-
this.#markTtsrInjected(details.rules);
|
|
1762
|
-
}
|
|
1763
|
-
try {
|
|
1764
|
-
await this.agent.continue();
|
|
1765
|
-
} catch {
|
|
1766
|
-
this.#resolveTtsrResume();
|
|
1767
|
-
}
|
|
1768
|
-
},
|
|
1769
|
-
{ delayMs: 50 },
|
|
1770
|
-
);
|
|
1771
|
-
return;
|
|
1772
|
-
}
|
|
1676
|
+
if (matches.length > 0 && this.#handleTtsrMatches(matches, matchContext, targetMessageTimestamp)) {
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
// ast-grep `astCondition` rules match against the reconstructed edit/write
|
|
1680
|
+
// snapshot, which only exists for tool argument streams. The native worker
|
|
1681
|
+
// call is async, so this path is awaited and self-throttled by the manager.
|
|
1682
|
+
if (matchContext.source === "tool" && this.#ttsrManager?.hasAstRules()) {
|
|
1683
|
+
const astMatches = await this.#checkTtsrAstStream(matchContext, streamingToolCall);
|
|
1684
|
+
if (astMatches.length > 0 && this.#handleTtsrMatches(astMatches, matchContext, targetMessageTimestamp)) {
|
|
1685
|
+
return;
|
|
1773
1686
|
}
|
|
1774
1687
|
}
|
|
1775
1688
|
}
|
|
@@ -2441,19 +2354,134 @@ export class AgentSession {
|
|
|
2441
2354
|
if (!manager) {
|
|
2442
2355
|
return [];
|
|
2443
2356
|
}
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
tools.find(t => t.name === toolCall.name) ??
|
|
2448
|
-
tools.find(t => t.customWireName !== undefined && t.customWireName === toolCall.name);
|
|
2449
|
-
const digest = tool?.matcherDigest?.(toolCall.arguments ?? {});
|
|
2450
|
-
if (digest !== undefined) {
|
|
2451
|
-
return manager.checkSnapshot(digest, matchContext);
|
|
2452
|
-
}
|
|
2357
|
+
const digest = this.#resolveTtsrMatcherDigest(toolCall);
|
|
2358
|
+
if (digest !== undefined) {
|
|
2359
|
+
return manager.checkSnapshot(digest, matchContext);
|
|
2453
2360
|
}
|
|
2454
2361
|
return manager.checkDelta(delta, matchContext);
|
|
2455
2362
|
}
|
|
2456
2363
|
|
|
2364
|
+
/** Reconstruct the tool's normalized source snapshot via its `matcherDigest`, if any. */
|
|
2365
|
+
#resolveTtsrMatcherDigest(toolCall: ToolCall | undefined): string | undefined {
|
|
2366
|
+
if (!toolCall) {
|
|
2367
|
+
return undefined;
|
|
2368
|
+
}
|
|
2369
|
+
const tools = this.agent.state.tools;
|
|
2370
|
+
const tool =
|
|
2371
|
+
tools.find(t => t.name === toolCall.name) ??
|
|
2372
|
+
tools.find(t => t.customWireName !== undefined && t.customWireName === toolCall.name);
|
|
2373
|
+
return tool?.matcherDigest?.(toolCall.arguments ?? {});
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
/**
|
|
2377
|
+
* Match ast-grep `astCondition` rules against the reconstructed tool snapshot.
|
|
2378
|
+
*
|
|
2379
|
+
* Only edit/write tool streams expose a `matcherDigest`, which is the real source
|
|
2380
|
+
* the call introduces; AST matching needs that (and a language inferred from the
|
|
2381
|
+
* path argument), so non-digest streams never produce AST matches.
|
|
2382
|
+
*/
|
|
2383
|
+
async #checkTtsrAstStream(matchContext: TtsrMatchContext, toolCall: ToolCall | undefined): Promise<Rule[]> {
|
|
2384
|
+
const manager = this.#ttsrManager;
|
|
2385
|
+
if (!manager) {
|
|
2386
|
+
return [];
|
|
2387
|
+
}
|
|
2388
|
+
const digest = this.#resolveTtsrMatcherDigest(toolCall);
|
|
2389
|
+
if (digest === undefined) {
|
|
2390
|
+
return [];
|
|
2391
|
+
}
|
|
2392
|
+
return manager.checkAstSnapshot(digest, matchContext);
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
/**
|
|
2396
|
+
* Route TTSR matches to either a per-tool injection or a stream-interrupting
|
|
2397
|
+
* retry. Returns true when the stream was aborted and the caller should stop
|
|
2398
|
+
* processing this event.
|
|
2399
|
+
*/
|
|
2400
|
+
#handleTtsrMatches(
|
|
2401
|
+
matches: Rule[],
|
|
2402
|
+
matchContext: TtsrMatchContext,
|
|
2403
|
+
targetMessageTimestamp: number | undefined,
|
|
2404
|
+
): boolean {
|
|
2405
|
+
// Decide first: a non-interrupting tool-source match attaches to the
|
|
2406
|
+
// specific tool call's result instead of driving a loop-wide follow-up.
|
|
2407
|
+
const shouldInterrupt = this.#shouldInterruptForTtsrMatch(matches, matchContext);
|
|
2408
|
+
const perToolId = shouldInterrupt ? undefined : this.#extractTtsrToolCallId(matchContext);
|
|
2409
|
+
if (perToolId) {
|
|
2410
|
+
this.#addPerToolTtsrInjections(perToolId, matches);
|
|
2411
|
+
this.#emitSessionEvent({ type: "ttsr_triggered", rules: matches }).catch(() => {});
|
|
2412
|
+
return false;
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
// Queue rules for injection; mark as injected only after successful enqueue.
|
|
2416
|
+
this.#addPendingTtsrInjections(matches);
|
|
2417
|
+
if (!shouldInterrupt) {
|
|
2418
|
+
return false;
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
// Abort the stream immediately — do not gate on extension callbacks
|
|
2422
|
+
this.#ttsrAbortPending = true;
|
|
2423
|
+
this.#ensureTtsrResumePromise();
|
|
2424
|
+
this.agent.abort(this.#formatTtsrAbortReason(matches));
|
|
2425
|
+
// Notify extensions (fire-and-forget, does not block abort)
|
|
2426
|
+
this.#emitSessionEvent({ type: "ttsr_triggered", rules: matches }).catch(() => {});
|
|
2427
|
+
// Schedule retry after a short delay
|
|
2428
|
+
const retryToken = ++this.#ttsrRetryToken;
|
|
2429
|
+
const generation = this.#promptGeneration;
|
|
2430
|
+
this.#schedulePostPromptTask(
|
|
2431
|
+
async () => {
|
|
2432
|
+
if (this.#ttsrRetryToken !== retryToken) {
|
|
2433
|
+
this.#resolveTtsrResume();
|
|
2434
|
+
return;
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
const targetAssistantIndex = this.#findTtsrAssistantIndex(targetMessageTimestamp);
|
|
2438
|
+
if (!this.#ttsrAbortPending || this.#promptGeneration !== generation || targetAssistantIndex === -1) {
|
|
2439
|
+
this.#ttsrAbortPending = false;
|
|
2440
|
+
this.#pendingTtsrInjections = [];
|
|
2441
|
+
this.#perToolTtsrInjections.clear();
|
|
2442
|
+
this.#resolveTtsrResume();
|
|
2443
|
+
return;
|
|
2444
|
+
}
|
|
2445
|
+
this.#ttsrAbortPending = false;
|
|
2446
|
+
this.#perToolTtsrInjections.clear();
|
|
2447
|
+
const ttsrSettings = this.#ttsrManager?.getSettings();
|
|
2448
|
+
if (ttsrSettings?.contextMode === "discard") {
|
|
2449
|
+
// Remove the partial/aborted assistant turn from agent state
|
|
2450
|
+
this.agent.replaceMessages(this.agent.state.messages.slice(0, targetAssistantIndex));
|
|
2451
|
+
}
|
|
2452
|
+
// Inject TTSR rules as system reminder before retry
|
|
2453
|
+
const injection = this.#getTtsrInjectionContent();
|
|
2454
|
+
if (injection) {
|
|
2455
|
+
const details = { rules: injection.rules.map(rule => rule.name) };
|
|
2456
|
+
this.agent.appendMessage({
|
|
2457
|
+
role: "custom",
|
|
2458
|
+
customType: "ttsr-injection",
|
|
2459
|
+
content: injection.content,
|
|
2460
|
+
display: false,
|
|
2461
|
+
details,
|
|
2462
|
+
attribution: "agent",
|
|
2463
|
+
timestamp: Date.now(),
|
|
2464
|
+
});
|
|
2465
|
+
this.sessionManager.appendCustomMessageEntry(
|
|
2466
|
+
"ttsr-injection",
|
|
2467
|
+
injection.content,
|
|
2468
|
+
false,
|
|
2469
|
+
details,
|
|
2470
|
+
"agent",
|
|
2471
|
+
);
|
|
2472
|
+
this.#markTtsrInjected(details.rules);
|
|
2473
|
+
}
|
|
2474
|
+
try {
|
|
2475
|
+
await this.agent.continue();
|
|
2476
|
+
} catch {
|
|
2477
|
+
this.#resolveTtsrResume();
|
|
2478
|
+
}
|
|
2479
|
+
},
|
|
2480
|
+
{ delayMs: 50 },
|
|
2481
|
+
);
|
|
2482
|
+
return true;
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2457
2485
|
/** Extract path-like arguments from tool call payload for TTSR glob matching. */
|
|
2458
2486
|
#extractTtsrFilePathsFromArgs(args: unknown): string[] | undefined {
|
|
2459
2487
|
if (!args || typeof args !== "object" || Array.isArray(args)) {
|
|
@@ -2992,10 +3020,10 @@ export class AgentSession {
|
|
|
2992
3020
|
* `metadata.user_id` shaped like real Claude Code's `getAPIMetadata` output:
|
|
2993
3021
|
* `{ session_id, account_uuid, device_id }`. `account_uuid` is included only
|
|
2994
3022
|
* when an Anthropic OAuth credential with a known account UUID is loaded;
|
|
2995
|
-
* `device_id` is derived from the persistent omp install id
|
|
2996
|
-
* keeps the value in sync with auth-state changes
|
|
2997
|
-
* refresh that surfaces a new account
|
|
2998
|
-
* `#syncAgentSessionId()` on every such event.
|
|
3023
|
+
* `device_id` is derived from both the persistent omp install id and that
|
|
3024
|
+
* account UUID. Resolving live keeps the value in sync with auth-state changes
|
|
3025
|
+
* (login/logout, token refresh that surfaces a new account UUID) without
|
|
3026
|
+
* needing to re-call `#syncAgentSessionId()` on every such event.
|
|
2999
3027
|
*/
|
|
3000
3028
|
#syncAgentSessionId(sessionId?: string): void {
|
|
3001
3029
|
const sid = this.#activeProviderSessionId(sessionId);
|
|
@@ -4286,6 +4314,27 @@ export class AgentSession {
|
|
|
4286
4314
|
};
|
|
4287
4315
|
}
|
|
4288
4316
|
|
|
4317
|
+
async #normalizeMessageContentImages(
|
|
4318
|
+
content: string | (TextContent | ImageContent)[],
|
|
4319
|
+
): Promise<string | (TextContent | ImageContent)[]> {
|
|
4320
|
+
if (typeof content === "string") return content;
|
|
4321
|
+
const images = content.filter((part): part is ImageContent => part.type === "image");
|
|
4322
|
+
if (images.length === 0) return content;
|
|
4323
|
+
const normalizedImages = await normalizeModelContextImages(images);
|
|
4324
|
+
if (!normalizedImages) return content;
|
|
4325
|
+
let imageIndex = 0;
|
|
4326
|
+
return content.map(part => (part.type === "image" ? normalizedImages[imageIndex++]! : part));
|
|
4327
|
+
}
|
|
4328
|
+
|
|
4329
|
+
async #normalizeAgentMessageImages<T extends AgentMessage>(message: T): Promise<T> {
|
|
4330
|
+
if (!("content" in message)) return message;
|
|
4331
|
+
const content = message.content;
|
|
4332
|
+
if (typeof content !== "string" && !Array.isArray(content)) return message;
|
|
4333
|
+
const normalized = await this.#normalizeMessageContentImages(content as string | (TextContent | ImageContent)[]);
|
|
4334
|
+
if (normalized === content) return message;
|
|
4335
|
+
return { ...message, content: normalized } as T;
|
|
4336
|
+
}
|
|
4337
|
+
|
|
4289
4338
|
/**
|
|
4290
4339
|
* Send a prompt to the agent.
|
|
4291
4340
|
* - Handles extension commands (registered via pi.registerCommand) immediately, even during streaming
|
|
@@ -4385,10 +4434,11 @@ export class AgentSession {
|
|
|
4385
4434
|
const hasPendingUserDirective = this.#toolChoiceQueue.inspect().includes("user-force");
|
|
4386
4435
|
const eagerTodoPrelude =
|
|
4387
4436
|
!options?.synthetic && !hasPendingUserDirective ? this.#createEagerTodoPrelude(expandedText) : undefined;
|
|
4437
|
+
const normalizedImages = await normalizeModelContextImages(options?.images);
|
|
4388
4438
|
|
|
4389
4439
|
const userContent: (TextContent | ImageContent)[] = [{ type: "text", text: expandedText }];
|
|
4390
|
-
if (
|
|
4391
|
-
userContent.push(...
|
|
4440
|
+
if (normalizedImages) {
|
|
4441
|
+
userContent.push(...normalizedImages);
|
|
4392
4442
|
}
|
|
4393
4443
|
|
|
4394
4444
|
const promptAttribution = options?.attribution ?? (options?.synthetic ? "agent" : "user");
|
|
@@ -4405,6 +4455,7 @@ export class AgentSession {
|
|
|
4405
4455
|
try {
|
|
4406
4456
|
await this.#promptWithMessage(message, expandedText, {
|
|
4407
4457
|
...options,
|
|
4458
|
+
images: normalizedImages,
|
|
4408
4459
|
prependMessages: eagerTodoPrelude ? [eagerTodoPrelude.message] : undefined,
|
|
4409
4460
|
appendMessages: keywordNotices.length > 0 ? keywordNotices : undefined,
|
|
4410
4461
|
});
|
|
@@ -4547,7 +4598,9 @@ export class AgentSession {
|
|
|
4547
4598
|
useHashLines: resolveFileDisplayMode(this).hashLines,
|
|
4548
4599
|
snapshotStore: getFileSnapshotStore(this),
|
|
4549
4600
|
});
|
|
4550
|
-
|
|
4601
|
+
for (const fileMentionMessage of fileMentionMessages) {
|
|
4602
|
+
messages.push(await this.#normalizeAgentMessageImages(fileMentionMessage));
|
|
4603
|
+
}
|
|
4551
4604
|
}
|
|
4552
4605
|
|
|
4553
4606
|
const beforeAgentStartSystemPrompt = await this.#buildSystemPromptForAgentStart(expandedText);
|
|
@@ -4563,15 +4616,18 @@ export class AgentSession {
|
|
|
4563
4616
|
const promptAttribution: "user" | "agent" | undefined =
|
|
4564
4617
|
"attribution" in message ? message.attribution : undefined;
|
|
4565
4618
|
for (const msg of result.messages) {
|
|
4566
|
-
messages.push(
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4619
|
+
messages.push(
|
|
4620
|
+
await this.#normalizeAgentMessageImages({
|
|
4621
|
+
role: "custom",
|
|
4622
|
+
customType: msg.customType,
|
|
4623
|
+
content: msg.content,
|
|
4624
|
+
display: msg.display,
|
|
4625
|
+
details: msg.details,
|
|
4626
|
+
attribution:
|
|
4627
|
+
msg.attribution ?? promptAttribution ?? (message.role === "user" ? "user" : "agent"),
|
|
4628
|
+
timestamp: Date.now(),
|
|
4629
|
+
}),
|
|
4630
|
+
);
|
|
4575
4631
|
}
|
|
4576
4632
|
}
|
|
4577
4633
|
|
|
@@ -4779,11 +4835,12 @@ export class AgentSession {
|
|
|
4779
4835
|
* Internal: Queue a steering message (already expanded, no extension command check).
|
|
4780
4836
|
*/
|
|
4781
4837
|
async #queueSteer(text: string, images?: ImageContent[]): Promise<void> {
|
|
4838
|
+
const normalizedImages = await normalizeModelContextImages(images);
|
|
4782
4839
|
const displayText = text || (images && images.length > 0 ? "[Image]" : "");
|
|
4783
4840
|
this.#steeringMessages.push({ text: displayText });
|
|
4784
4841
|
const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
|
|
4785
|
-
if (
|
|
4786
|
-
content.push(...
|
|
4842
|
+
if (normalizedImages && normalizedImages.length > 0) {
|
|
4843
|
+
content.push(...normalizedImages);
|
|
4787
4844
|
}
|
|
4788
4845
|
this.agent.steer({
|
|
4789
4846
|
role: "user",
|
|
@@ -4798,11 +4855,12 @@ export class AgentSession {
|
|
|
4798
4855
|
* Internal: Queue a follow-up message (already expanded, no extension command check).
|
|
4799
4856
|
*/
|
|
4800
4857
|
async #queueFollowUp(text: string, images?: ImageContent[]): Promise<void> {
|
|
4858
|
+
const normalizedImages = await normalizeModelContextImages(images);
|
|
4801
4859
|
const displayText = text || (images && images.length > 0 ? "[Image]" : "");
|
|
4802
4860
|
this.#followUpMessages.push({ text: displayText });
|
|
4803
4861
|
const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
|
|
4804
|
-
if (
|
|
4805
|
-
content.push(...
|
|
4862
|
+
if (normalizedImages && normalizedImages.length > 0) {
|
|
4863
|
+
content.push(...normalizedImages);
|
|
4806
4864
|
}
|
|
4807
4865
|
this.agent.followUp({
|
|
4808
4866
|
role: "user",
|
|
@@ -4946,16 +5004,17 @@ export class AgentSession {
|
|
|
4946
5004
|
attribution: message.attribution ?? "agent",
|
|
4947
5005
|
timestamp: Date.now(),
|
|
4948
5006
|
};
|
|
5007
|
+
const normalizedAppMessage = await this.#normalizeAgentMessageImages(appMessage);
|
|
4949
5008
|
if (this.isStreaming) {
|
|
4950
5009
|
if (options?.deliverAs === "nextTurn") {
|
|
4951
|
-
this.#queueHiddenNextTurnMessage(
|
|
5010
|
+
this.#queueHiddenNextTurnMessage(normalizedAppMessage, options?.triggerTurn ?? false);
|
|
4952
5011
|
return;
|
|
4953
5012
|
}
|
|
4954
5013
|
|
|
4955
5014
|
if (options?.deliverAs === "followUp") {
|
|
4956
|
-
this.agent.followUp(
|
|
5015
|
+
this.agent.followUp(normalizedAppMessage);
|
|
4957
5016
|
} else {
|
|
4958
|
-
this.agent.steer(
|
|
5017
|
+
this.agent.steer(normalizedAppMessage);
|
|
4959
5018
|
}
|
|
4960
5019
|
return;
|
|
4961
5020
|
}
|
|
@@ -4963,16 +5022,16 @@ export class AgentSession {
|
|
|
4963
5022
|
if (options?.deliverAs === "nextTurn") {
|
|
4964
5023
|
if (options?.triggerTurn) {
|
|
4965
5024
|
if (this.#clientBridge?.deferAgentInitiatedTurns && !this.#allowAcpAgentInitiatedTurns) {
|
|
4966
|
-
this.#queueHiddenNextTurnMessage(
|
|
5025
|
+
this.#queueHiddenNextTurnMessage(normalizedAppMessage, false);
|
|
4967
5026
|
return;
|
|
4968
5027
|
}
|
|
4969
|
-
await this.agent.prompt(
|
|
5028
|
+
await this.agent.prompt(normalizedAppMessage);
|
|
4970
5029
|
return;
|
|
4971
5030
|
}
|
|
4972
|
-
this.agent.appendMessage(
|
|
5031
|
+
this.agent.appendMessage(normalizedAppMessage);
|
|
4973
5032
|
this.sessionManager.appendCustomMessageEntry(
|
|
4974
|
-
|
|
4975
|
-
|
|
5033
|
+
normalizedAppMessage.customType,
|
|
5034
|
+
normalizedAppMessage.content,
|
|
4976
5035
|
message.display,
|
|
4977
5036
|
message.details,
|
|
4978
5037
|
message.attribution ?? "agent",
|
|
@@ -4982,17 +5041,17 @@ export class AgentSession {
|
|
|
4982
5041
|
|
|
4983
5042
|
if (options?.triggerTurn) {
|
|
4984
5043
|
if (this.#clientBridge?.deferAgentInitiatedTurns && !this.#allowAcpAgentInitiatedTurns) {
|
|
4985
|
-
this.#queueHiddenNextTurnMessage(
|
|
5044
|
+
this.#queueHiddenNextTurnMessage(normalizedAppMessage, false);
|
|
4986
5045
|
return;
|
|
4987
5046
|
}
|
|
4988
|
-
await this.agent.prompt(
|
|
5047
|
+
await this.agent.prompt(normalizedAppMessage);
|
|
4989
5048
|
return;
|
|
4990
5049
|
}
|
|
4991
5050
|
|
|
4992
|
-
this.agent.appendMessage(
|
|
5051
|
+
this.agent.appendMessage(normalizedAppMessage);
|
|
4993
5052
|
this.sessionManager.appendCustomMessageEntry(
|
|
4994
|
-
|
|
4995
|
-
|
|
5053
|
+
normalizedAppMessage.customType,
|
|
5054
|
+
normalizedAppMessage.content,
|
|
4996
5055
|
message.display,
|
|
4997
5056
|
message.details,
|
|
4998
5057
|
message.attribution ?? "agent",
|
|
@@ -6749,9 +6808,13 @@ export class AgentSession {
|
|
|
6749
6808
|
return undefined;
|
|
6750
6809
|
}
|
|
6751
6810
|
|
|
6752
|
-
|
|
6753
|
-
|
|
6754
|
-
|
|
6811
|
+
// Must check the active tool set, not just the registry: tool discovery
|
|
6812
|
+
// (tools.discoveryMode === "all") can register `todo` while hiding it from
|
|
6813
|
+
// the exposed tools. Forcing a named tool_choice for an inactive tool makes
|
|
6814
|
+
// the provider reject the request (HTTP 400).
|
|
6815
|
+
if (!this.getActiveToolNames().includes("todo")) {
|
|
6816
|
+
logger.warn("Eager todo enforcement skipped because todo is not active", {
|
|
6817
|
+
activeToolNames: this.getActiveToolNames(),
|
|
6755
6818
|
});
|
|
6756
6819
|
return undefined;
|
|
6757
6820
|
}
|
|
@@ -6913,7 +6976,6 @@ export class AgentSession {
|
|
|
6913
6976
|
this.#closeProviderSessionsForModelSwitch(currentModel, model);
|
|
6914
6977
|
}
|
|
6915
6978
|
this.agent.setModel(model);
|
|
6916
|
-
this.#syncToolCallBatchCap(model);
|
|
6917
6979
|
|
|
6918
6980
|
// Re-evaluate append-only context mode — provider or setting may have changed
|
|
6919
6981
|
this.#syncAppendOnlyContext(model);
|
|
@@ -9109,7 +9171,6 @@ export class AgentSession {
|
|
|
9109
9171
|
this.#setModelWithProviderSessionReset(match);
|
|
9110
9172
|
} else {
|
|
9111
9173
|
this.agent.setModel(match);
|
|
9112
|
-
this.#syncToolCallBatchCap(match);
|
|
9113
9174
|
}
|
|
9114
9175
|
}
|
|
9115
9176
|
}
|
|
@@ -9192,9 +9253,6 @@ export class AgentSession {
|
|
|
9192
9253
|
this.#scheduledHiddenNextTurnGeneration = previousScheduledHiddenNextTurnGeneration;
|
|
9193
9254
|
if (previousModel) {
|
|
9194
9255
|
this.agent.setModel(previousModel);
|
|
9195
|
-
this.#syncToolCallBatchCap(previousModel);
|
|
9196
|
-
} else {
|
|
9197
|
-
this.#syncToolCallBatchCap(undefined);
|
|
9198
9256
|
}
|
|
9199
9257
|
this.#thinkingLevel = previousThinkingLevel;
|
|
9200
9258
|
this.#autoThinking = previousAutoThinking;
|
|
@@ -3558,7 +3558,7 @@ export class SessionManager {
|
|
|
3558
3558
|
}
|
|
3559
3559
|
const relocated = sourceCwdGone && (mostRecent === null || (mostRecentIsBreadcrumb && !hasCurrentCwdSession));
|
|
3560
3560
|
if (relocated) {
|
|
3561
|
-
|
|
3561
|
+
logger.info("Re-rooting moved session", { from: resolvedBreadcrumbCwd, to: resolvedCwd });
|
|
3562
3562
|
const manager = await SessionManager.open(breadcrumb.sessionFile, undefined, storage);
|
|
3563
3563
|
await manager.moveTo(cwd, sessionDir);
|
|
3564
3564
|
return manager;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import { getOAuthProviders } from "@oh-my-pi/pi-ai/
|
|
4
|
+
import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
|
|
5
5
|
import { Snowflake, setProjectDir } from "@oh-my-pi/pi-utils";
|
|
6
6
|
import { $ } from "bun";
|
|
7
7
|
import type { SettingPath, SettingValue } from "../config/settings";
|
package/src/system-prompt.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { contextFileCapability } from "./capability/context-file";
|
|
|
10
10
|
import { systemPromptCapability } from "./capability/system-prompt";
|
|
11
11
|
import type { SkillsSettings } from "./config/settings";
|
|
12
12
|
import { type ContextFile, loadCapability, type SystemPrompt as SystemPromptFile } from "./discovery";
|
|
13
|
+
import { expandAtImports } from "./discovery/at-imports";
|
|
13
14
|
import { loadSkills, type Skill } from "./extensibility/skills";
|
|
14
15
|
import { hasObsidian } from "./internal-urls/vault-protocol";
|
|
15
16
|
import customSystemPromptTemplate from "./prompts/system/custom-system-prompt.md" with { type: "text" };
|
|
@@ -254,15 +255,20 @@ export async function loadProjectContextFiles(
|
|
|
254
255
|
|
|
255
256
|
const result = await loadCapability(contextFileCapability.id, { cwd: resolvedCwd });
|
|
256
257
|
|
|
257
|
-
//
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
258
|
+
// Materialize ContextFile items, expanding any `@path/to/file` includes
|
|
259
|
+
// in their content. The expansion uses the file's own directory as the
|
|
260
|
+
// resolution base so relative imports work the same way Claude Code,
|
|
261
|
+
// Goose, and other tools document.
|
|
262
|
+
const files = await Promise.all(
|
|
263
|
+
result.items.map(async item => {
|
|
264
|
+
const contextFile = item as ContextFile;
|
|
265
|
+
return {
|
|
266
|
+
path: contextFile.path,
|
|
267
|
+
content: await expandAtImports(contextFile.content, contextFile.path),
|
|
268
|
+
depth: contextFile.depth,
|
|
269
|
+
};
|
|
270
|
+
}),
|
|
271
|
+
);
|
|
266
272
|
|
|
267
273
|
// Sort by depth (descending): higher depth (farther from cwd) comes first,
|
|
268
274
|
// so files closer to cwd appear later and are more prominent
|