@oh-my-pi/pi-coding-agent 17.1.5 → 17.1.6
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 +31 -0
- package/dist/{CHANGELOG-qs3vd6xf.md → CHANGELOG-x9zt79k8.md} +31 -0
- package/dist/cli.js +2989 -2978
- package/dist/types/capability/index.d.ts +1 -1
- package/dist/types/capability/types.d.ts +23 -1
- package/dist/types/config/settings-schema.d.ts +23 -1
- package/dist/types/internal-urls/mcp-protocol.d.ts +3 -2
- package/dist/types/internal-urls/parse.d.ts +12 -0
- package/dist/types/internal-urls/router.d.ts +6 -0
- package/dist/types/internal-urls/types.d.ts +6 -0
- package/dist/types/lsp/config.d.ts +1 -0
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/mcp/manager.d.ts +5 -0
- package/dist/types/mcp/tool-bridge.d.ts +13 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +2 -0
- package/dist/types/sdk.d.ts +3 -1
- package/dist/types/session/agent-session-types.d.ts +3 -1
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/model-controls.d.ts +3 -0
- package/dist/types/session/session-advisors.d.ts +7 -1
- package/dist/types/session/tool-choice-queue.d.ts +6 -4
- package/dist/types/session/turn-recovery.d.ts +4 -2
- package/dist/types/task/index.d.ts +1 -1
- package/dist/types/task/types.d.ts +3 -11
- package/dist/types/thinking.d.ts +21 -2
- package/dist/types/utils/title-generator.d.ts +17 -16
- package/package.json +12 -12
- package/src/capability/index.ts +43 -12
- package/src/capability/mcp.ts +21 -0
- package/src/capability/types.ts +20 -1
- package/src/cli/read-cli.ts +44 -2
- package/src/config/settings-schema.ts +27 -1
- package/src/eval/py/runner.py +16 -2
- package/src/extensibility/extensions/runner.ts +91 -5
- package/src/extensibility/extensions/types.ts +0 -1
- package/src/extensibility/hooks/types.ts +0 -1
- package/src/internal-urls/mcp-protocol.ts +17 -3
- package/src/internal-urls/parse.ts +31 -0
- package/src/internal-urls/router.ts +24 -4
- package/src/internal-urls/types.ts +6 -0
- package/src/live/transport.ts +2 -2
- package/src/lsp/client.ts +2 -2
- package/src/lsp/config.ts +4 -0
- package/src/lsp/types.ts +2 -0
- package/src/mcp/config.ts +26 -14
- package/src/mcp/manager.ts +26 -9
- package/src/mcp/tool-bridge.ts +52 -1
- package/src/memories/index.ts +25 -6
- package/src/modes/components/status-line/segments.ts +3 -1
- package/src/modes/controllers/command-controller.ts +10 -10
- package/src/modes/controllers/extension-ui-controller.ts +3 -3
- package/src/modes/rpc/rpc-mode.ts +60 -47
- package/src/prompts/steering/user-interjection.md +2 -5
- package/src/prompts/tools/task.md +4 -2
- package/src/sdk.ts +17 -8
- package/src/session/agent-session-types.ts +3 -0
- package/src/session/agent-session.ts +21 -4
- package/src/session/model-controls.ts +43 -7
- package/src/session/session-advisors.ts +30 -14
- package/src/session/session-tools.ts +4 -2
- package/src/session/tool-choice-queue.ts +19 -4
- package/src/session/turn-recovery.ts +21 -2
- package/src/task/executor.ts +11 -3
- package/src/task/index.ts +43 -32
- package/src/task/types.ts +12 -17
- package/src/thinking.ts +68 -5
- package/src/tools/read.ts +2 -2
- package/src/utils/title-generator.ts +88 -34
- package/dist/types/advisor/__tests__/advisor.test.d.ts +0 -1
- package/dist/types/advisor/__tests__/config.test.d.ts +0 -1
- package/dist/types/advisor/__tests__/emission-guard.test.d.ts +0 -1
- package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +0 -1
- package/dist/types/cli/update-cli.test.d.ts +0 -1
- package/dist/types/config/__tests__/model-registry.test.d.ts +0 -1
- package/dist/types/eval/__tests__/agent-bridge.test.d.ts +0 -1
- package/dist/types/eval/__tests__/bridge-timeout.test.d.ts +0 -1
- package/dist/types/eval/__tests__/budget-bridge.test.d.ts +0 -1
- package/dist/types/eval/__tests__/completion-bridge.test.d.ts +0 -1
- package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +0 -1
- package/dist/types/eval/__tests__/idle-timeout.test.d.ts +0 -1
- package/dist/types/eval/__tests__/js-context-manager.test.d.ts +0 -1
- package/dist/types/eval/__tests__/julia-prelude.test.d.ts +0 -1
- package/dist/types/eval/__tests__/kernel-spawn.test.d.ts +0 -1
- package/dist/types/eval/__tests__/prelude-agent.test.d.ts +0 -1
- package/dist/types/eval/__tests__/process-entry-import.test.d.ts +0 -1
- package/dist/types/eval/py/__tests__/prelude.test.d.ts +0 -1
- package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +0 -1
- package/dist/types/hindsight/client.test.d.ts +0 -1
- package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +0 -1
- package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +0 -1
- package/dist/types/launch/broker-list-order.test.d.ts +0 -1
- package/dist/types/launch/broker-output-snapshot.test.d.ts +0 -1
- package/dist/types/launch/protocol.test.d.ts +0 -1
- package/dist/types/launch/spawn-options.test.d.ts +0 -1
- package/dist/types/launch/terminal-output.test.d.ts +0 -1
- package/dist/types/live/protocol.test.d.ts +0 -1
- package/dist/types/mcp/config-writer.test.d.ts +0 -1
- package/dist/types/mcp/smithery-auth.test.d.ts +0 -1
- package/dist/types/mcp/smithery-registry.test.d.ts +0 -1
- package/dist/types/mcp/transports/stdio.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/skill-message.test.d.ts +0 -1
- package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +0 -1
- package/dist/types/modes/components/custom-editor.test.d.ts +0 -1
- package/dist/types/modes/components/login-dialog.test.d.ts +0 -1
- package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +0 -1
- package/dist/types/modes/components/status-line/component.test.d.ts +0 -1
- package/dist/types/modes/components/tool-execution.test.d.ts +0 -1
- package/dist/types/modes/controllers/extension-ui-controller.test.d.ts +0 -1
- package/dist/types/modes/noninteractive-dispose.test.d.ts +0 -1
- package/dist/types/modes/print-mode.test.d.ts +0 -1
- package/dist/types/modes/session-teardown.test.d.ts +0 -1
- package/dist/types/modes/theme/mermaid-rendering.test.d.ts +0 -1
- package/dist/types/modes/utils/transcript-render-helpers.test.d.ts +0 -1
- package/dist/types/modes/warp-events.test.d.ts +0 -1
- package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +0 -1
- package/dist/types/plan-mode/model-transition.test.d.ts +0 -1
- package/dist/types/plan-mode/reentry-prompt.test.d.ts +0 -1
- package/dist/types/session/agent-session-error-log.test.d.ts +0 -1
- package/dist/types/session/blob-store.test.d.ts +0 -1
- package/dist/types/session/messages.test.d.ts +0 -1
- package/dist/types/session/session-context.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +0 -1
- package/dist/types/system-prompt.test.d.ts +0 -1
- package/dist/types/task/render.test.d.ts +0 -1
- package/dist/types/task/spawn-policy.test.d.ts +0 -1
- package/dist/types/tools/__tests__/eval-description.test.d.ts +0 -1
- package/dist/types/tools/__tests__/glob.test.d.ts +0 -1
- package/dist/types/tools/__tests__/json-tree.test.d.ts +0 -1
- package/dist/types/tools/__tests__/vibe-render.test.d.ts +0 -1
- package/dist/types/tools/hub/launch-compat.test.d.ts +0 -1
- package/dist/types/vibe/__tests__/token-rate.test.d.ts +0 -1
- package/src/advisor/__tests__/advisor.test.ts +0 -4889
- package/src/advisor/__tests__/config.test.ts +0 -349
- package/src/advisor/__tests__/emission-guard.test.ts +0 -147
- package/src/cli/__tests__/auth-gateway-catalog.test.ts +0 -111
- package/src/cli/update-cli.test.ts +0 -28
- package/src/config/__tests__/model-registry.test.ts +0 -182
- package/src/eval/__tests__/agent-bridge.test.ts +0 -1509
- package/src/eval/__tests__/bridge-timeout.test.ts +0 -170
- package/src/eval/__tests__/budget-bridge.test.ts +0 -80
- package/src/eval/__tests__/completion-bridge.test.ts +0 -412
- package/src/eval/__tests__/helpers-local-roots.test.ts +0 -55
- package/src/eval/__tests__/idle-timeout.test.ts +0 -80
- package/src/eval/__tests__/js-context-manager.test.ts +0 -456
- package/src/eval/__tests__/julia-prelude.test.ts +0 -66
- package/src/eval/__tests__/kernel-spawn.test.ts +0 -115
- package/src/eval/__tests__/prelude-agent.test.ts +0 -156
- package/src/eval/__tests__/process-entry-import.test.ts +0 -137
- package/src/eval/py/__tests__/prelude.test.ts +0 -104
- package/src/eval/py/__tests__/runner-shell-output.test.ts +0 -157
- package/src/hindsight/client.test.ts +0 -75
- package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +0 -141
- package/src/internal-urls/__tests__/ssh-protocol.test.ts +0 -331
- package/src/launch/broker-list-order.test.ts +0 -89
- package/src/launch/broker-output-snapshot.test.ts +0 -126
- package/src/launch/protocol.test.ts +0 -59
- package/src/launch/spawn-options.test.ts +0 -31
- package/src/launch/terminal-output.test.ts +0 -107
- package/src/live/protocol.test.ts +0 -140
- package/src/mcp/config-writer.test.ts +0 -43
- package/src/mcp/smithery-auth.test.ts +0 -29
- package/src/mcp/smithery-registry.test.ts +0 -51
- package/src/mcp/transports/stdio.test.ts +0 -427
- package/src/modes/components/__tests__/dynamic-border.test.ts +0 -55
- package/src/modes/components/__tests__/move-overlay.test.ts +0 -252
- package/src/modes/components/__tests__/pause-screen.test.ts +0 -143
- package/src/modes/components/__tests__/skill-message.test.ts +0 -94
- package/src/modes/components/custom-editor-plugin-ctor.test.ts +0 -36
- package/src/modes/components/custom-editor.test.ts +0 -510
- package/src/modes/components/login-dialog.test.ts +0 -56
- package/src/modes/components/status-line/component.jj-cache.test.ts +0 -229
- package/src/modes/components/status-line/component.test.ts +0 -84
- package/src/modes/components/tool-execution.test.ts +0 -162
- package/src/modes/controllers/extension-ui-controller.test.ts +0 -250
- package/src/modes/noninteractive-dispose.test.ts +0 -73
- package/src/modes/print-mode.test.ts +0 -71
- package/src/modes/session-teardown.test.ts +0 -219
- package/src/modes/theme/mermaid-rendering.test.ts +0 -53
- package/src/modes/utils/transcript-render-helpers.test.ts +0 -38
- package/src/modes/warp-events.test.ts +0 -794
- package/src/plan-mode/approved-plan-prompt.test.ts +0 -36
- package/src/plan-mode/model-transition.test.ts +0 -60
- package/src/plan-mode/reentry-prompt.test.ts +0 -41
- package/src/session/agent-session-error-log.test.ts +0 -59
- package/src/session/blob-store.test.ts +0 -56
- package/src/session/messages.test.ts +0 -282
- package/src/session/session-context.test.ts +0 -384
- package/src/ssh/__tests__/connection-manager-args.test.ts +0 -191
- package/src/ssh/__tests__/connection-manager-timeout.test.ts +0 -61
- package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +0 -105
- package/src/ssh/__tests__/sshfs-mount.test.ts +0 -13
- package/src/system-prompt.test.ts +0 -236
- package/src/task/render.test.ts +0 -290
- package/src/task/spawn-policy.test.ts +0 -62
- package/src/tools/__tests__/eval-description.test.ts +0 -18
- package/src/tools/__tests__/glob.test.ts +0 -37
- package/src/tools/__tests__/json-tree.test.ts +0 -35
- package/src/tools/__tests__/vibe-render.test.ts +0 -210
- package/src/tools/hub/launch-compat.test.ts +0 -40
- package/src/vibe/__tests__/token-rate.test.ts +0 -96
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { prompt } from "@oh-my-pi/pi-utils";
|
|
3
|
-
import planModeApprovedPrompt from "../prompts/system/plan-mode-approved.md" with { type: "text" };
|
|
4
|
-
import planModeCompactInstructionsPrompt from "../prompts/system/plan-mode-compact-instructions.md" with {
|
|
5
|
-
type: "text",
|
|
6
|
-
};
|
|
7
|
-
import planModeReferencePrompt from "../prompts/system/plan-mode-reference.md" with { type: "text" };
|
|
8
|
-
|
|
9
|
-
const PLAN_FILE_PATH = "local://durable-plan.md";
|
|
10
|
-
const PLAN_SENTINEL = "SENTINEL_HEADROOM_COMPRESSED_PLAN_CONTENT";
|
|
11
|
-
|
|
12
|
-
describe("approved plan execution prompts", () => {
|
|
13
|
-
it("requires reading the durable plan file without inlining plan content", () => {
|
|
14
|
-
const approved = prompt.render(planModeApprovedPrompt, {
|
|
15
|
-
planContent: PLAN_SENTINEL,
|
|
16
|
-
planFilePath: PLAN_FILE_PATH,
|
|
17
|
-
contextPreserved: false,
|
|
18
|
-
});
|
|
19
|
-
const reference = prompt.render(planModeReferencePrompt, {
|
|
20
|
-
planContent: PLAN_SENTINEL,
|
|
21
|
-
planFilePath: PLAN_FILE_PATH,
|
|
22
|
-
});
|
|
23
|
-
const compact = prompt.render(planModeCompactInstructionsPrompt, {
|
|
24
|
-
planFilePath: PLAN_FILE_PATH,
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
for (const rendered of [approved, reference, compact]) {
|
|
28
|
-
expect(rendered).toContain(PLAN_FILE_PATH);
|
|
29
|
-
}
|
|
30
|
-
for (const rendered of [approved, reference, compact]) {
|
|
31
|
-
expect(rendered).not.toContain(PLAN_SENTINEL);
|
|
32
|
-
}
|
|
33
|
-
expect(approved).toContain("MUST read `local://durable-plan.md`");
|
|
34
|
-
expect(reference).toContain("MUST read `local://durable-plan.md`");
|
|
35
|
-
});
|
|
36
|
-
});
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
3
|
-
import type { Model } from "@oh-my-pi/pi-ai";
|
|
4
|
-
import type { ResolvedModelRoleValue } from "../config/model-resolver";
|
|
5
|
-
import { AUTO_THINKING } from "../thinking";
|
|
6
|
-
import { resolvePlanModelTransition } from "./model-transition";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Plan-mode model transition policy (issue #5657). The active model in plan
|
|
10
|
-
* mode IS the plan-role model, so reassigning that role mid-planning must move
|
|
11
|
-
* the session onto the new model — or defer the switch when a turn is streaming.
|
|
12
|
-
*/
|
|
13
|
-
const model = (provider: string, id: string): Model => ({ provider, id }) as unknown as Model;
|
|
14
|
-
|
|
15
|
-
const resolved = (
|
|
16
|
-
m: Model | undefined,
|
|
17
|
-
thinking?: ResolvedModelRoleValue["thinkingLevel"],
|
|
18
|
-
): ResolvedModelRoleValue => ({
|
|
19
|
-
model: m,
|
|
20
|
-
thinkingLevel: thinking,
|
|
21
|
-
explicitThinkingLevel: thinking !== undefined,
|
|
22
|
-
warning: undefined,
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
describe("resolvePlanModelTransition", () => {
|
|
26
|
-
it("switches to the resolved plan model when it differs from the active one", () => {
|
|
27
|
-
const plan = model("anthropic", "opus");
|
|
28
|
-
const transition = resolvePlanModelTransition(model("openai", "gpt"), resolved(plan), false);
|
|
29
|
-
expect(transition).toEqual({ kind: "apply", model: plan, thinkingLevel: undefined, deferred: false });
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("defers the switch while the session is streaming", () => {
|
|
33
|
-
const plan = model("anthropic", "opus");
|
|
34
|
-
const transition = resolvePlanModelTransition(model("openai", "gpt"), resolved(plan), true);
|
|
35
|
-
expect(transition).toEqual({ kind: "apply", model: plan, thinkingLevel: undefined, deferred: true });
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("carries the plan role's explicit thinking level into the switch", () => {
|
|
39
|
-
const plan = model("anthropic", "opus");
|
|
40
|
-
const transition = resolvePlanModelTransition(model("openai", "gpt"), resolved(plan, ThinkingLevel.High), false);
|
|
41
|
-
expect(transition).toEqual({ kind: "apply", model: plan, thinkingLevel: ThinkingLevel.High, deferred: false });
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("only adjusts thinking when the model is unchanged but the level is explicit", () => {
|
|
45
|
-
const plan = model("anthropic", "opus");
|
|
46
|
-
const transition = resolvePlanModelTransition(plan, resolved(model("anthropic", "opus"), AUTO_THINKING), false);
|
|
47
|
-
expect(transition).toEqual({ kind: "thinking", thinkingLevel: AUTO_THINKING });
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("is a no-op when the model matches and no explicit thinking level is set", () => {
|
|
51
|
-
const plan = model("anthropic", "opus");
|
|
52
|
-
const transition = resolvePlanModelTransition(plan, resolved(model("anthropic", "opus")), false);
|
|
53
|
-
expect(transition).toEqual({ kind: "none" });
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("is a no-op when the plan role resolves to no model", () => {
|
|
57
|
-
const transition = resolvePlanModelTransition(model("openai", "gpt"), resolved(undefined), false);
|
|
58
|
-
expect(transition).toEqual({ kind: "none" });
|
|
59
|
-
});
|
|
60
|
-
});
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { prompt } from "@oh-my-pi/pi-utils";
|
|
3
|
-
import planModeActivePrompt from "../prompts/system/plan-mode-active.md" with { type: "text" };
|
|
4
|
-
|
|
5
|
-
const BASE = {
|
|
6
|
-
planFilePath: "local://old-feature-plan.md",
|
|
7
|
-
askToolName: "ask",
|
|
8
|
-
writeToolName: "write",
|
|
9
|
-
editToolName: "edit",
|
|
10
|
-
isHashlineEditMode: false,
|
|
11
|
-
iterative: false,
|
|
12
|
-
} as const;
|
|
13
|
-
|
|
14
|
-
function render(overrides: { reentry: boolean; planExists: boolean }): string {
|
|
15
|
-
return prompt.render(planModeActivePrompt, { ...BASE, ...overrides });
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe("plan-mode re-entry prompt", () => {
|
|
19
|
-
it("only emits the Re-entry section when re-entering", () => {
|
|
20
|
-
expect(render({ reentry: false, planExists: true })).not.toContain("## Re-entry");
|
|
21
|
-
expect(render({ reentry: true, planExists: true })).toContain("## Re-entry");
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("anchors the turn on the new request, not the old plan", () => {
|
|
25
|
-
const rendered = render({ reentry: true, planExists: true });
|
|
26
|
-
const reentry = rendered.slice(rendered.indexOf("## Re-entry"));
|
|
27
|
-
// The new request is the primary input; the old plan is reference only.
|
|
28
|
-
expect(reentry).toMatch(/NEW request[\s\S]*primary input/);
|
|
29
|
-
// Corrections to an incomplete old plan must be folded into the new plan,
|
|
30
|
-
// never substituted for it (the reported failure: dropping the new request).
|
|
31
|
-
expect(reentry).toMatch(/combine, never substitute/);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("does not contradict the planExists guidance on a different task", () => {
|
|
35
|
-
const rendered = render({ reentry: true, planExists: true });
|
|
36
|
-
// planExists branch: different task -> leave old plan, write a fresh file.
|
|
37
|
-
expect(rendered).toContain("leave that plan in place and start a fresh");
|
|
38
|
-
// Re-entry must agree; the old "Different task -> overwrite it" directive is gone.
|
|
39
|
-
expect(rendered).not.toMatch(/[Dd]ifferent task → overwrite/);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Contract: a turn ending in a provider error surfaces one warn-level log
|
|
3
|
-
* carrying `provider`/`model`/`errorMessage`/`errorStatus`/`errorId`, so
|
|
4
|
-
* recurring provider stream failures are diagnosable from the main log alone
|
|
5
|
-
* (issue #6177). Non-error stops must not emit it.
|
|
6
|
-
*/
|
|
7
|
-
import { afterAll, afterEach, describe, expect, it } from "bun:test";
|
|
8
|
-
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
|
9
|
-
import { logger } from "@oh-my-pi/pi-utils";
|
|
10
|
-
import { logProviderTurnError } from "./messages";
|
|
11
|
-
|
|
12
|
-
function makeMessage(overrides: Partial<AssistantMessage>): AssistantMessage {
|
|
13
|
-
return {
|
|
14
|
-
role: "assistant",
|
|
15
|
-
content: [],
|
|
16
|
-
api: "cursor",
|
|
17
|
-
provider: "cursor",
|
|
18
|
-
model: "composer-2.5",
|
|
19
|
-
usage: {} as AssistantMessage["usage"],
|
|
20
|
-
stopReason: "error",
|
|
21
|
-
timestamp: 1,
|
|
22
|
-
...overrides,
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
describe("logProviderTurnError", () => {
|
|
27
|
-
const events: logger.LogEvent[] = [];
|
|
28
|
-
const dispose = logger.registerLogSink(event => events.push(event));
|
|
29
|
-
|
|
30
|
-
afterEach(() => {
|
|
31
|
-
events.length = 0;
|
|
32
|
-
});
|
|
33
|
-
afterAll(() => dispose());
|
|
34
|
-
|
|
35
|
-
it("emits a warn log with the provider error fields for stopReason:error", () => {
|
|
36
|
-
logProviderTurnError(
|
|
37
|
-
makeMessage({
|
|
38
|
-
errorMessage: "stream stall: idle watchdog fired",
|
|
39
|
-
errorStatus: 500,
|
|
40
|
-
errorId: 42,
|
|
41
|
-
}),
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
const warn = events.find(e => e.level === "warn" && e.message === "agent turn ended with provider error");
|
|
45
|
-
expect(warn).toBeDefined();
|
|
46
|
-
expect(warn?.context).toMatchObject({
|
|
47
|
-
provider: "cursor",
|
|
48
|
-
model: "composer-2.5",
|
|
49
|
-
errorMessage: "stream stall: idle watchdog fired",
|
|
50
|
-
errorStatus: 500,
|
|
51
|
-
errorId: 42,
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("does not emit for a successful stop", () => {
|
|
56
|
-
logProviderTurnError(makeMessage({ stopReason: "stop", errorMessage: undefined }));
|
|
57
|
-
expect(events.some(e => e.message === "agent turn ended with provider error")).toBe(false);
|
|
58
|
-
});
|
|
59
|
-
});
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { afterAll, describe, expect, it } from "bun:test";
|
|
2
|
-
import * as fs from "node:fs";
|
|
3
|
-
import * as os from "node:os";
|
|
4
|
-
import * as path from "node:path";
|
|
5
|
-
import { BlobStore, parseBlobRef, resolveImageData, resolveImageDataSync, resolveImageDataUrl } from "./blob-store";
|
|
6
|
-
|
|
7
|
-
const base = fs.mkdtempSync(path.join(os.tmpdir(), "blob-store-test-"));
|
|
8
|
-
const blobDir = path.join(base, "agent", "blobs", "data");
|
|
9
|
-
fs.mkdirSync(blobDir, { recursive: true });
|
|
10
|
-
fs.writeFileSync(path.join(base, "secret.txt"), "TOP-SECRET-CONTENTS");
|
|
11
|
-
const store = new BlobStore(blobDir);
|
|
12
|
-
|
|
13
|
-
afterAll(() => {
|
|
14
|
-
fs.rmSync(base, { recursive: true, force: true });
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
describe("parseBlobRef validation", () => {
|
|
18
|
-
it("accepts a canonical 64-char lowercase hex suffix", () => {
|
|
19
|
-
const hash = "a".repeat(64);
|
|
20
|
-
expect(parseBlobRef(`blob:sha256:${hash}`)).toBe(hash);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("returns null for non-blob strings", () => {
|
|
24
|
-
expect(parseBlobRef("data:image/png;base64,AAAA")).toBeNull();
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it.each([
|
|
28
|
-
"../../../secret.txt",
|
|
29
|
-
`${"../".repeat(6)}etc/passwd`,
|
|
30
|
-
"A".repeat(64), // uppercase hex is not the canonical shape
|
|
31
|
-
"a".repeat(63), // too short
|
|
32
|
-
"a".repeat(65), // too long
|
|
33
|
-
"", // empty
|
|
34
|
-
])("rejects malformed suffix %p", suffix => {
|
|
35
|
-
expect(parseBlobRef(`blob:sha256:${suffix}`)).toBeNull();
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
describe("blob resolution path confinement", () => {
|
|
40
|
-
const traversalRef = "blob:sha256:../../../secret.txt";
|
|
41
|
-
|
|
42
|
-
it("leaves a traversal ref unresolved instead of reading outside the blob dir (base64 path)", async () => {
|
|
43
|
-
expect(await resolveImageData(store, traversalRef)).toBe(traversalRef);
|
|
44
|
-
expect(resolveImageDataSync(store, traversalRef)).toBe(traversalRef);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("leaves a traversal ref unresolved instead of reading outside the blob dir (data-url path)", async () => {
|
|
48
|
-
expect(await resolveImageDataUrl(store, traversalRef)).toBe(traversalRef);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("still resolves a valid stored blob", async () => {
|
|
52
|
-
const put = store.putSync(Buffer.from("hello"));
|
|
53
|
-
expect(Buffer.from(resolveImageDataSync(store, put.ref), "base64").toString("utf8")).toBe("hello");
|
|
54
|
-
expect(Buffer.from(await resolveImageData(store, put.ref), "base64").toString("utf8")).toBe("hello");
|
|
55
|
-
});
|
|
56
|
-
});
|
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
3
|
-
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
|
4
|
-
import {
|
|
5
|
-
type CustomMessage,
|
|
6
|
-
convertToLlm,
|
|
7
|
-
INTERRUPTED_THINKING_MESSAGE_TYPE,
|
|
8
|
-
replaceLlmImagesWithText,
|
|
9
|
-
SKILL_PROMPT_MESSAGE_TYPE,
|
|
10
|
-
type SkillPromptDetails,
|
|
11
|
-
stripImagesFromMessage,
|
|
12
|
-
} from "./messages";
|
|
13
|
-
|
|
14
|
-
function customMessage(customType: string, attribution: "agent" | "user"): CustomMessage<SkillPromptDetails> {
|
|
15
|
-
return {
|
|
16
|
-
role: "custom",
|
|
17
|
-
customType,
|
|
18
|
-
content: "Use this skill.",
|
|
19
|
-
display: true,
|
|
20
|
-
details: { name: "atomic-commit", path: "/tmp/SKILL.md", lineCount: 1 },
|
|
21
|
-
attribution,
|
|
22
|
-
timestamp: 1,
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const interruptedUsage: AssistantMessage["usage"] = {
|
|
27
|
-
input: 0,
|
|
28
|
-
output: 0,
|
|
29
|
-
cacheRead: 0,
|
|
30
|
-
cacheWrite: 0,
|
|
31
|
-
totalTokens: 0,
|
|
32
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
function abortedAssistant(content: AssistantMessage["content"]): AssistantMessage {
|
|
36
|
-
return {
|
|
37
|
-
role: "assistant",
|
|
38
|
-
content,
|
|
39
|
-
api: "anthropic-messages",
|
|
40
|
-
provider: "anthropic",
|
|
41
|
-
model: "claude-sonnet-4-5",
|
|
42
|
-
usage: interruptedUsage,
|
|
43
|
-
stopReason: "aborted",
|
|
44
|
-
timestamp: 1,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function interruptedThinkingContinuity(): CustomMessage {
|
|
49
|
-
return {
|
|
50
|
-
role: "custom",
|
|
51
|
-
customType: INTERRUPTED_THINKING_MESSAGE_TYPE,
|
|
52
|
-
content: "preserved reasoning",
|
|
53
|
-
display: false,
|
|
54
|
-
attribution: "agent",
|
|
55
|
-
timestamp: 2,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
describe("convertToLlm", () => {
|
|
60
|
-
it("presents user-invoked skill prompts as user turns", () => {
|
|
61
|
-
const [message] = convertToLlm([customMessage(SKILL_PROMPT_MESSAGE_TYPE, "user")]);
|
|
62
|
-
|
|
63
|
-
expect(message?.role).toBe("user");
|
|
64
|
-
if (message?.role !== "user") {
|
|
65
|
-
throw new Error(`Expected user role, received ${message?.role ?? "none"}`);
|
|
66
|
-
}
|
|
67
|
-
expect(message.attribution).toBe("user");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("keeps auto-applied skill prompts and other custom messages as developer turns", () => {
|
|
71
|
-
const [autoSkill, otherCustom] = convertToLlm([
|
|
72
|
-
customMessage(SKILL_PROMPT_MESSAGE_TYPE, "agent"),
|
|
73
|
-
customMessage("extension-note", "user"),
|
|
74
|
-
]);
|
|
75
|
-
|
|
76
|
-
expect(autoSkill?.role).toBe("developer");
|
|
77
|
-
expect(otherCustom?.role).toBe("developer");
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("strips the demoted trailing thinking run from the assistant LLM view when its continuity message follows", () => {
|
|
81
|
-
const messages: AgentMessage[] = [
|
|
82
|
-
abortedAssistant([
|
|
83
|
-
{ type: "text", text: "partial answer" },
|
|
84
|
-
{ type: "thinking", thinking: "interrupted reasoning" },
|
|
85
|
-
]),
|
|
86
|
-
interruptedThinkingContinuity(),
|
|
87
|
-
];
|
|
88
|
-
|
|
89
|
-
const llm = convertToLlm(messages);
|
|
90
|
-
const assistant = llm.find(entry => entry.role === "assistant");
|
|
91
|
-
expect(Array.isArray(assistant?.content) && assistant.content.map(block => block.type)).toEqual(["text"]);
|
|
92
|
-
expect(llm.some(entry => entry.role === "developer")).toBe(true);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("keeps trailing thinking on the assistant LLM view when no continuity message follows", () => {
|
|
96
|
-
const messages: AgentMessage[] = [
|
|
97
|
-
abortedAssistant([
|
|
98
|
-
{ type: "text", text: "partial answer" },
|
|
99
|
-
{ type: "thinking", thinking: "interrupted reasoning" },
|
|
100
|
-
]),
|
|
101
|
-
];
|
|
102
|
-
|
|
103
|
-
const llm = convertToLlm(messages);
|
|
104
|
-
const assistant = llm.find(entry => entry.role === "assistant");
|
|
105
|
-
expect(Array.isArray(assistant?.content) && assistant.content.map(block => block.type)).toEqual([
|
|
106
|
-
"text",
|
|
107
|
-
"thinking",
|
|
108
|
-
]);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("keeps a signed (complete) trailing thinking block in the assistant LLM view even with a continuity message", () => {
|
|
112
|
-
const messages: AgentMessage[] = [
|
|
113
|
-
abortedAssistant([
|
|
114
|
-
{ type: "text", text: "partial answer" },
|
|
115
|
-
{ type: "thinking", thinking: "complete reasoning", thinkingSignature: "sig" },
|
|
116
|
-
]),
|
|
117
|
-
interruptedThinkingContinuity(),
|
|
118
|
-
];
|
|
119
|
-
|
|
120
|
-
const llm = convertToLlm(messages);
|
|
121
|
-
const assistant = llm.find(entry => entry.role === "assistant");
|
|
122
|
-
expect(Array.isArray(assistant?.content) && assistant.content.map(block => block.type)).toEqual([
|
|
123
|
-
"text",
|
|
124
|
-
"thinking",
|
|
125
|
-
]);
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
function settledAssistant(text: string): AssistantMessage {
|
|
130
|
-
return {
|
|
131
|
-
role: "assistant",
|
|
132
|
-
content: [{ type: "text", text }],
|
|
133
|
-
api: "anthropic-messages",
|
|
134
|
-
provider: "anthropic",
|
|
135
|
-
model: "claude-sonnet-4-5",
|
|
136
|
-
usage: {
|
|
137
|
-
input: 100,
|
|
138
|
-
output: 20,
|
|
139
|
-
cacheRead: 0,
|
|
140
|
-
cacheWrite: 0,
|
|
141
|
-
totalTokens: 120,
|
|
142
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
143
|
-
},
|
|
144
|
-
stopReason: "stop",
|
|
145
|
-
timestamp: 1,
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function userMessage(text: string, timestamp: number): AgentMessage {
|
|
150
|
-
return { role: "user", content: text, attribution: "user", timestamp } as AgentMessage;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
describe("convertToLlm caching", () => {
|
|
154
|
-
it("reuses the outer array on an exact repeat of the same history", () => {
|
|
155
|
-
const messages: AgentMessage[] = [userMessage("hello", 1), settledAssistant("hi")];
|
|
156
|
-
const first = convertToLlm(messages);
|
|
157
|
-
const second = convertToLlm(messages);
|
|
158
|
-
expect(second).toBe(first);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it("reuses the unchanged prefix output on append-only growth", () => {
|
|
162
|
-
const messages: AgentMessage[] = [userMessage("one", 1), settledAssistant("reply one")];
|
|
163
|
-
const first = convertToLlm(messages);
|
|
164
|
-
messages.push(userMessage("two", 2));
|
|
165
|
-
const grown = convertToLlm(messages);
|
|
166
|
-
// New outer array (no held-result aliasing), but the converted prefix is
|
|
167
|
-
// byte-identical and the appended turn is present.
|
|
168
|
-
expect(grown).not.toBe(first);
|
|
169
|
-
expect(grown.length).toBe(first.length + 1);
|
|
170
|
-
expect(grown.slice(0, first.length)).toEqual(first);
|
|
171
|
-
expect(grown[grown.length - 1]?.role).toBe("user");
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it("recomputes the boundary assistant when a following interrupted-thinking marker appears on growth", () => {
|
|
175
|
-
const messages: AgentMessage[] = [
|
|
176
|
-
abortedAssistant([
|
|
177
|
-
{ type: "text", text: "partial answer" },
|
|
178
|
-
{ type: "thinking", thinking: "interrupted reasoning" },
|
|
179
|
-
]),
|
|
180
|
-
];
|
|
181
|
-
const before = convertToLlm(messages);
|
|
182
|
-
const beforeAssistant = before.find(entry => entry.role === "assistant");
|
|
183
|
-
expect(Array.isArray(beforeAssistant?.content) && beforeAssistant.content.map(b => b.type)).toEqual([
|
|
184
|
-
"text",
|
|
185
|
-
"thinking",
|
|
186
|
-
]);
|
|
187
|
-
|
|
188
|
-
// Append the continuity marker on the same array: the assistant is now the
|
|
189
|
-
// boundary message and its LLM view must drop the trailing thinking run.
|
|
190
|
-
messages.push(interruptedThinkingContinuity());
|
|
191
|
-
const after = convertToLlm(messages);
|
|
192
|
-
const afterAssistant = after.find(entry => entry.role === "assistant");
|
|
193
|
-
expect(Array.isArray(afterAssistant?.content) && afterAssistant.content.map(b => b.type)).toEqual(["text"]);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it("recomputes a message after strip-images invalidates its cache", () => {
|
|
197
|
-
const withImage: AgentMessage = {
|
|
198
|
-
role: "user",
|
|
199
|
-
content: [
|
|
200
|
-
{ type: "text", text: "look" },
|
|
201
|
-
{ type: "image", data: "aaaa", mimeType: "image/png" },
|
|
202
|
-
],
|
|
203
|
-
attribution: "user",
|
|
204
|
-
timestamp: 1,
|
|
205
|
-
};
|
|
206
|
-
const messages: AgentMessage[] = [withImage];
|
|
207
|
-
const before = convertToLlm(messages);
|
|
208
|
-
const beforeUser = before.find(entry => entry.role === "user");
|
|
209
|
-
expect(Array.isArray(beforeUser?.content) && beforeUser.content.some(b => b.type === "image")).toBe(true);
|
|
210
|
-
|
|
211
|
-
// Mutate in place through the owner seam, which must invalidate the cache.
|
|
212
|
-
stripImagesFromMessage(withImage);
|
|
213
|
-
const after = convertToLlm(messages);
|
|
214
|
-
const afterUser = after.find(entry => entry.role === "user");
|
|
215
|
-
expect(Array.isArray(afterUser?.content) && afterUser.content.some(b => b.type === "image")).toBe(false);
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
describe("replaceLlmImagesWithText", () => {
|
|
220
|
-
it("replaces image blocks in user, developer, and tool-result messages with the placeholder", () => {
|
|
221
|
-
const converted = convertToLlm([
|
|
222
|
-
{
|
|
223
|
-
role: "user",
|
|
224
|
-
content: [
|
|
225
|
-
{ type: "text", text: "look" },
|
|
226
|
-
{ type: "image", data: "aaaa", mimeType: "image/png" },
|
|
227
|
-
],
|
|
228
|
-
attribution: "user",
|
|
229
|
-
timestamp: 1,
|
|
230
|
-
},
|
|
231
|
-
{
|
|
232
|
-
role: "toolResult",
|
|
233
|
-
toolCallId: "c1",
|
|
234
|
-
toolName: "inspect_image",
|
|
235
|
-
content: [{ type: "image", data: "bbbb", mimeType: "image/png" }],
|
|
236
|
-
isError: false,
|
|
237
|
-
timestamp: 2,
|
|
238
|
-
},
|
|
239
|
-
]);
|
|
240
|
-
|
|
241
|
-
const scrubbed = replaceLlmImagesWithText(converted, "[image omitted]");
|
|
242
|
-
|
|
243
|
-
expect(scrubbed).not.toBe(converted);
|
|
244
|
-
const types = scrubbed.flatMap(m => (Array.isArray(m.content) ? m.content.map(b => b.type) : []));
|
|
245
|
-
expect(types).not.toContain("image");
|
|
246
|
-
const user = scrubbed.find(m => m.role === "user");
|
|
247
|
-
expect(Array.isArray(user?.content) && user.content.map(b => (b.type === "text" ? b.text : b.type))).toEqual([
|
|
248
|
-
"look",
|
|
249
|
-
"[image omitted]",
|
|
250
|
-
]);
|
|
251
|
-
const toolResult = scrubbed.find(m => m.role === "toolResult");
|
|
252
|
-
expect(Array.isArray(toolResult?.content) && toolResult.content).toEqual([
|
|
253
|
-
{ type: "text", text: "[image omitted]" },
|
|
254
|
-
]);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it("collapses consecutive image blocks into a single placeholder", () => {
|
|
258
|
-
const converted = convertToLlm([
|
|
259
|
-
{
|
|
260
|
-
role: "user",
|
|
261
|
-
content: [
|
|
262
|
-
{ type: "image", data: "aaaa", mimeType: "image/png" },
|
|
263
|
-
{ type: "image", data: "bbbb", mimeType: "image/png" },
|
|
264
|
-
],
|
|
265
|
-
attribution: "user",
|
|
266
|
-
timestamp: 1,
|
|
267
|
-
},
|
|
268
|
-
]);
|
|
269
|
-
|
|
270
|
-
const scrubbed = replaceLlmImagesWithText(converted, "[image omitted]");
|
|
271
|
-
const user = scrubbed.find(m => m.role === "user");
|
|
272
|
-
expect(Array.isArray(user?.content) && user.content).toEqual([{ type: "text", text: "[image omitted]" }]);
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
it("returns the same array reference when there are no image blocks", () => {
|
|
276
|
-
const converted = convertToLlm([
|
|
277
|
-
{ role: "user", content: [{ type: "text", text: "hi" }], attribution: "user", timestamp: 1 },
|
|
278
|
-
]);
|
|
279
|
-
|
|
280
|
-
expect(replaceLlmImagesWithText(converted, "[image omitted]")).toBe(converted);
|
|
281
|
-
});
|
|
282
|
-
});
|