@oh-my-pi/pi-coding-agent 16.2.0 → 16.2.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 +12 -0
- package/dist/cli.js +2550 -2542
- package/dist/types/advisor/watchdog.d.ts +11 -0
- package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +6 -0
- package/dist/types/session/blob-store.d.ts +4 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +21 -0
- package/src/advisor/watchdog.ts +15 -0
- package/src/discovery/claude-plugins.ts +3 -2
- package/src/modes/acp/acp-agent.ts +14 -1
- package/src/modes/acp/acp-event-mapper.ts +89 -25
- package/src/prompts/advisor/context-files.md +8 -0
- package/src/sdk.ts +11 -1
- package/src/session/agent-session.ts +11 -0
- package/src/session/blob-store.ts +24 -0
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import type { ActiveRepoContext } from "../utils/active-repo-context";
|
|
2
2
|
export declare function formatActiveRepoWatchdogPrompt(activeRepoContext: ActiveRepoContext): string;
|
|
3
|
+
/**
|
|
4
|
+
* Render the project context files (AGENTS.md and the like) into a block for the
|
|
5
|
+
* advisor's system prompt, mirroring how the primary agent receives them. Gives
|
|
6
|
+
* the read-only reviewer the user's standing project instructions so it can hold
|
|
7
|
+
* the driving agent to them instead of advising against project conventions it
|
|
8
|
+
* cannot otherwise see. Returns undefined when there are no context files.
|
|
9
|
+
*/
|
|
10
|
+
export declare function formatAdvisorContextPrompt(contextFiles: ReadonlyArray<{
|
|
11
|
+
path: string;
|
|
12
|
+
content: string;
|
|
13
|
+
}>): string | undefined;
|
|
3
14
|
/**
|
|
4
15
|
* Discover and load WATCHDOG.md files walking up from cwd, project .omp folder, and user agent dir.
|
|
5
16
|
* Returns formatted watchdog file blocks ready to be appended to the advisor system prompt.
|
|
@@ -8,6 +8,7 @@ interface AcpEventMapperOptions {
|
|
|
8
8
|
getMessageId?: (message: unknown) => string | undefined;
|
|
9
9
|
getMessageProgress?: (message: unknown) => MessageProgress | undefined;
|
|
10
10
|
getToolArgs?: (toolCallId: string) => unknown;
|
|
11
|
+
resolveImageData?: (data: string, mimeType: string | undefined) => string;
|
|
11
12
|
/**
|
|
12
13
|
* Session cwd. Tool call locations sent to ACP clients must be absolute
|
|
13
14
|
* (the editor host needs them to open or focus files). When provided,
|
|
@@ -239,6 +239,12 @@ export interface AgentSessionConfig {
|
|
|
239
239
|
advisorReadOnlyTools?: AgentTool[];
|
|
240
240
|
/** Preloaded watchdog prompt content for the advisor. */
|
|
241
241
|
advisorWatchdogPrompt?: string;
|
|
242
|
+
/**
|
|
243
|
+
* Preloaded project context files (AGENTS.md, etc.) rendered as a system-prompt
|
|
244
|
+
* block for the advisor — the same standing instructions the primary agent
|
|
245
|
+
* receives, so the reviewer holds the agent to them.
|
|
246
|
+
*/
|
|
247
|
+
advisorContextPrompt?: string;
|
|
242
248
|
/**
|
|
243
249
|
* Strip tool descriptions from provider-bound tool specs on side requests
|
|
244
250
|
* (handoff). Must match the session-start value used to build the system
|
|
@@ -27,6 +27,8 @@ export declare class BlobStore {
|
|
|
27
27
|
putSync(data: Buffer, options?: BlobPutOptions): BlobPutResult;
|
|
28
28
|
/** Read blob by hash, returns Buffer or null if not found. */
|
|
29
29
|
get(hash: string): Promise<Buffer | null>;
|
|
30
|
+
/** Synchronous variant of {@link get}. */
|
|
31
|
+
getSync(hash: string): Buffer | null;
|
|
30
32
|
/** Check if a blob exists. */
|
|
31
33
|
has(hash: string): Promise<boolean>;
|
|
32
34
|
}
|
|
@@ -62,3 +64,5 @@ export declare function resolveImageDataUrl(blobStore: BlobStore, data: string):
|
|
|
62
64
|
* If the blob is missing, logs a warning and returns a placeholder.
|
|
63
65
|
*/
|
|
64
66
|
export declare function resolveImageData(blobStore: BlobStore, data: string): Promise<string>;
|
|
67
|
+
/** Synchronous variant of {@link resolveImageData}. */
|
|
68
|
+
export declare function resolveImageDataSync(blobStore: BlobStore, data: string): string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "16.2.
|
|
4
|
+
"version": "16.2.1",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -55,17 +55,17 @@
|
|
|
55
55
|
"@agentclientprotocol/sdk": "0.25.0",
|
|
56
56
|
"@babel/parser": "^7.29.7",
|
|
57
57
|
"@mozilla/readability": "^0.6.0",
|
|
58
|
-
"@oh-my-pi/hashline": "16.2.
|
|
59
|
-
"@oh-my-pi/omp-stats": "16.2.
|
|
60
|
-
"@oh-my-pi/pi-agent-core": "16.2.
|
|
61
|
-
"@oh-my-pi/pi-ai": "16.2.
|
|
62
|
-
"@oh-my-pi/pi-catalog": "16.2.
|
|
63
|
-
"@oh-my-pi/pi-mnemopi": "16.2.
|
|
64
|
-
"@oh-my-pi/pi-natives": "16.2.
|
|
65
|
-
"@oh-my-pi/pi-tui": "16.2.
|
|
66
|
-
"@oh-my-pi/pi-utils": "16.2.
|
|
67
|
-
"@oh-my-pi/pi-wire": "16.2.
|
|
68
|
-
"@oh-my-pi/snapcompact": "16.2.
|
|
58
|
+
"@oh-my-pi/hashline": "16.2.1",
|
|
59
|
+
"@oh-my-pi/omp-stats": "16.2.1",
|
|
60
|
+
"@oh-my-pi/pi-agent-core": "16.2.1",
|
|
61
|
+
"@oh-my-pi/pi-ai": "16.2.1",
|
|
62
|
+
"@oh-my-pi/pi-catalog": "16.2.1",
|
|
63
|
+
"@oh-my-pi/pi-mnemopi": "16.2.1",
|
|
64
|
+
"@oh-my-pi/pi-natives": "16.2.1",
|
|
65
|
+
"@oh-my-pi/pi-tui": "16.2.1",
|
|
66
|
+
"@oh-my-pi/pi-utils": "16.2.1",
|
|
67
|
+
"@oh-my-pi/pi-wire": "16.2.1",
|
|
68
|
+
"@oh-my-pi/snapcompact": "16.2.1",
|
|
69
69
|
"@opentelemetry/api": "^1.9.1",
|
|
70
70
|
"@opentelemetry/context-async-hooks": "^2.7.1",
|
|
71
71
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type AdvisorRuntimeHost,
|
|
17
17
|
deriveAdvisorTelemetry,
|
|
18
18
|
formatAdvisorBatchContent,
|
|
19
|
+
formatAdvisorContextPrompt,
|
|
19
20
|
isAdvisorInterruptImmuneTurnActive,
|
|
20
21
|
isInterruptingSeverity,
|
|
21
22
|
resolveAdvisorDeliveryChannel,
|
|
@@ -57,6 +58,26 @@ describe("advisor", () => {
|
|
|
57
58
|
});
|
|
58
59
|
});
|
|
59
60
|
|
|
61
|
+
describe("formatAdvisorContextPrompt", () => {
|
|
62
|
+
it("renders project context files into a block with path and verbatim content", () => {
|
|
63
|
+
const rendered = formatAdvisorContextPrompt([
|
|
64
|
+
{
|
|
65
|
+
path: "/repo/AGENTS.md",
|
|
66
|
+
content: "Use `bun check`, never `tsc`.\nNo `any` unless absolutely necessary.",
|
|
67
|
+
},
|
|
68
|
+
]);
|
|
69
|
+
expect(rendered).toBeDefined();
|
|
70
|
+
expect(rendered).toContain('<file path="/repo/AGENTS.md">');
|
|
71
|
+
// Content is injected verbatim (noEscape) so backticks/markup survive for the model.
|
|
72
|
+
expect(rendered).toContain("Use `bun check`, never `tsc`.");
|
|
73
|
+
expect(rendered).toContain("No `any` unless absolutely necessary.");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("returns undefined when there are no context files", () => {
|
|
77
|
+
expect(formatAdvisorContextPrompt([])).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
60
81
|
describe("formatSessionHistoryMarkdown includeThinking", () => {
|
|
61
82
|
it("includes thinking text when includeThinking is true", () => {
|
|
62
83
|
const thinking = "I should check the edge case first.";
|
package/src/advisor/watchdog.ts
CHANGED
|
@@ -3,6 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import { getAgentDir, isEnoent, logger, prompt } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { expandAtImports } from "../discovery/at-imports";
|
|
5
5
|
import activeRepoWatchdogTemplate from "../prompts/advisor/active-repo-watchdog.md" with { type: "text" };
|
|
6
|
+
import contextFilesTemplate from "../prompts/advisor/context-files.md" with { type: "text" };
|
|
6
7
|
import type { ActiveRepoContext } from "../utils/active-repo-context";
|
|
7
8
|
import { repo } from "../utils/git";
|
|
8
9
|
import { normalizePromptPath } from "../utils/prompt-path";
|
|
@@ -15,6 +16,20 @@ export function formatActiveRepoWatchdogPrompt(activeRepoContext: ActiveRepoCont
|
|
|
15
16
|
.trim();
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Render the project context files (AGENTS.md and the like) into a block for the
|
|
21
|
+
* advisor's system prompt, mirroring how the primary agent receives them. Gives
|
|
22
|
+
* the read-only reviewer the user's standing project instructions so it can hold
|
|
23
|
+
* the driving agent to them instead of advising against project conventions it
|
|
24
|
+
* cannot otherwise see. Returns undefined when there are no context files.
|
|
25
|
+
*/
|
|
26
|
+
export function formatAdvisorContextPrompt(
|
|
27
|
+
contextFiles: ReadonlyArray<{ path: string; content: string }>,
|
|
28
|
+
): string | undefined {
|
|
29
|
+
if (contextFiles.length === 0) return undefined;
|
|
30
|
+
return prompt.render(contextFilesTemplate, { contextFiles }).trim() || undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
18
33
|
/**
|
|
19
34
|
* Discover and load WATCHDOG.md files walking up from cwd, project .omp folder, and user agent dir.
|
|
20
35
|
* Returns formatted watchdog file blocks ready to be appended to the advisor system prompt.
|
|
@@ -17,6 +17,7 @@ import type { LoadContext, LoadResult } from "../capability/types";
|
|
|
17
17
|
import {
|
|
18
18
|
type ClaudePluginRoot,
|
|
19
19
|
createSourceMeta,
|
|
20
|
+
expandEnvVarsDeep,
|
|
20
21
|
listClaudePluginRoots,
|
|
21
22
|
loadFilesFromDir,
|
|
22
23
|
scanSkillsFromDir,
|
|
@@ -327,8 +328,8 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
|
|
|
327
328
|
...(raw.args !== undefined && { args: substitutePluginRoot(raw.args, root.path) }),
|
|
328
329
|
...(raw.env !== undefined && { env: substitutePluginRoot(raw.env, root.path) }),
|
|
329
330
|
...(raw.cwd !== undefined && { cwd: substitutePluginRoot(raw.cwd, root.path) }),
|
|
330
|
-
...(raw.url !== undefined && { url: raw.url }),
|
|
331
|
-
...(raw.headers !== undefined && { headers: raw.headers }),
|
|
331
|
+
...(raw.url !== undefined && { url: expandEnvVarsDeep(raw.url) }),
|
|
332
|
+
...(raw.headers !== undefined && { headers: expandEnvVarsDeep(raw.headers) }),
|
|
332
333
|
...(raw.auth !== undefined && { auth: raw.auth }),
|
|
333
334
|
...(raw.oauth !== undefined && { oauth: raw.oauth }),
|
|
334
335
|
...(raw.type !== undefined && { transport: raw.type as MCPServer["transport"] }),
|
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
} from "@agentclientprotocol/sdk";
|
|
43
43
|
import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
44
44
|
import type { AssistantMessage, Model } from "@oh-my-pi/pi-ai";
|
|
45
|
-
import { isEnoent, logger, VERSION } from "@oh-my-pi/pi-utils";
|
|
45
|
+
import { getBlobsDir, isEnoent, logger, VERSION } from "@oh-my-pi/pi-utils";
|
|
46
46
|
import { disableProvider, enableProvider, reset as resetCapabilities } from "../../capability";
|
|
47
47
|
import { Settings } from "../../config/settings";
|
|
48
48
|
import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
|
|
@@ -62,6 +62,7 @@ import { loadAllExtensions } from "../../modes/components/extensions/state-manag
|
|
|
62
62
|
import { theme } from "../../modes/theme/theme";
|
|
63
63
|
import { type PlanApprovalDetails, resolveApprovedPlan } from "../../plan-mode/approved-plan";
|
|
64
64
|
import type { AgentSession, AgentSessionEvent } from "../../session/agent-session";
|
|
65
|
+
import { BlobStore, resolveImageDataSync } from "../../session/blob-store";
|
|
65
66
|
import { isSilentAbort, SKILL_PROMPT_MESSAGE_TYPE, USER_INTERRUPT_LABEL } from "../../session/messages";
|
|
66
67
|
import type { UsageStatistics } from "../../session/session-entries";
|
|
67
68
|
import type { SessionInfo as StoredSessionInfo } from "../../session/session-listing";
|
|
@@ -445,6 +446,7 @@ export class AcpAgent implements Agent {
|
|
|
445
446
|
#cleanupRegistered = false;
|
|
446
447
|
#clientCapabilities: ClientCapabilities | undefined;
|
|
447
448
|
#cancelCleanupTimeoutMs = ACP_CANCEL_CLEANUP_TIMEOUT_MS;
|
|
449
|
+
#blobs = new BlobStore(getBlobsDir());
|
|
448
450
|
|
|
449
451
|
constructor(connection: AgentSideConnection, createSession: CreateAcpSession, initialSession?: AgentSession) {
|
|
450
452
|
this.#connection = connection;
|
|
@@ -1187,11 +1189,21 @@ export class AcpAgent implements Agent {
|
|
|
1187
1189
|
}
|
|
1188
1190
|
|
|
1189
1191
|
this.#prepareLiveAssistantMessage(record, event);
|
|
1192
|
+
const imageDataCache = new Map<string, string>();
|
|
1193
|
+
const resolveImageDataForAcp = (data: string, mimeType: string | undefined): string => {
|
|
1194
|
+
const key = `${mimeType ?? ""}\u0000${data}`;
|
|
1195
|
+
const cached = imageDataCache.get(key);
|
|
1196
|
+
if (cached !== undefined) return cached;
|
|
1197
|
+
const resolved = resolveImageDataSync(this.#blobs, data);
|
|
1198
|
+
imageDataCache.set(key, resolved);
|
|
1199
|
+
return resolved;
|
|
1200
|
+
};
|
|
1190
1201
|
for (const notification of mapAgentSessionEventToAcpSessionUpdates(event, record.session.sessionId, {
|
|
1191
1202
|
getMessageId: message => this.#getLiveMessageId(record, message),
|
|
1192
1203
|
getMessageProgress: message => this.#getLiveMessageProgress(record, message),
|
|
1193
1204
|
getToolArgs: toolCallId => record.toolArgsById.get(toolCallId),
|
|
1194
1205
|
cwd: record.session.sessionManager.getCwd(),
|
|
1206
|
+
resolveImageData: resolveImageDataForAcp,
|
|
1195
1207
|
})) {
|
|
1196
1208
|
await this.#connection.sessionUpdate(notification);
|
|
1197
1209
|
}
|
|
@@ -2052,6 +2064,7 @@ export class AcpAgent implements Agent {
|
|
|
2052
2064
|
const notifications = mapAgentSessionEventToAcpSessionUpdates(endEvent, sessionId, {
|
|
2053
2065
|
cwd,
|
|
2054
2066
|
getToolArgs: toolCallId => (toolCallId === message.toolCallId ? options.toolArgs : undefined),
|
|
2067
|
+
resolveImageData: (data, _mimeType) => resolveImageDataSync(this.#blobs, data),
|
|
2055
2068
|
});
|
|
2056
2069
|
if (options.includeStart === false) {
|
|
2057
2070
|
return notifications;
|
|
@@ -20,6 +20,7 @@ interface AcpEventMapperOptions {
|
|
|
20
20
|
getMessageId?: (message: unknown) => string | undefined;
|
|
21
21
|
getMessageProgress?: (message: unknown) => MessageProgress | undefined;
|
|
22
22
|
getToolArgs?: (toolCallId: string) => unknown;
|
|
23
|
+
resolveImageData?: (data: string, mimeType: string | undefined) => string;
|
|
23
24
|
/**
|
|
24
25
|
* Session cwd. Tool call locations sent to ACP clients must be absolute
|
|
25
26
|
* (the editor host needs them to open or focus files). When provided,
|
|
@@ -179,7 +180,7 @@ export function mapAgentSessionEventToAcpSessionUpdates(
|
|
|
179
180
|
case "tool_execution_update": {
|
|
180
181
|
const content = mergeToolUpdateContent(
|
|
181
182
|
buildToolStartContent(event.toolName, event.args),
|
|
182
|
-
extractToolCallContent(event.partialResult),
|
|
183
|
+
extractToolCallContent(event.partialResult, options),
|
|
183
184
|
);
|
|
184
185
|
const update: SessionUpdate = {
|
|
185
186
|
sessionUpdate: "tool_call_update",
|
|
@@ -197,7 +198,10 @@ export function mapAgentSessionEventToAcpSessionUpdates(
|
|
|
197
198
|
return [toSessionNotification(sessionId, update)];
|
|
198
199
|
}
|
|
199
200
|
case "tool_execution_end": {
|
|
200
|
-
const resultContent = [
|
|
201
|
+
const resultContent = [
|
|
202
|
+
...extractDiffToolCallContent(event.result),
|
|
203
|
+
...extractToolCallContent(event.result, options),
|
|
204
|
+
];
|
|
201
205
|
const content = mergeToolUpdateContent(
|
|
202
206
|
buildToolStartContent(event.toolName, getToolExecutionEndArgs(event, options)),
|
|
203
207
|
resultContent,
|
|
@@ -641,13 +645,15 @@ function terminalToolCallContent(terminalId: string): ToolCallContent {
|
|
|
641
645
|
return { type: "terminal", terminalId };
|
|
642
646
|
}
|
|
643
647
|
|
|
644
|
-
function extractToolCallContent(value: unknown): ToolCallContent[] {
|
|
645
|
-
const richContent = extractStructuredToolCallContent(value);
|
|
648
|
+
function extractToolCallContent(value: unknown, options: AcpEventMapperOptions): ToolCallContent[] {
|
|
649
|
+
const richContent = extractStructuredToolCallContent(value, options);
|
|
650
|
+
const detailsImageContent = extractDetailsImageToolCallContent(value, options, richContent);
|
|
651
|
+
const combinedContent = [...richContent, ...detailsImageContent];
|
|
646
652
|
const terminalId = extractTerminalId(value);
|
|
647
653
|
const content =
|
|
648
|
-
terminalId && !hasTerminalContent(
|
|
649
|
-
? [...
|
|
650
|
-
:
|
|
654
|
+
terminalId && !hasTerminalContent(combinedContent, terminalId)
|
|
655
|
+
? [...combinedContent, terminalToolCallContent(terminalId)]
|
|
656
|
+
: combinedContent;
|
|
651
657
|
const fallbackText = extractReadableText(value);
|
|
652
658
|
if (!fallbackText) {
|
|
653
659
|
return content;
|
|
@@ -658,7 +664,7 @@ function extractToolCallContent(value: unknown): ToolCallContent[] {
|
|
|
658
664
|
return [...content, textToolCallContent(fallbackText)];
|
|
659
665
|
}
|
|
660
666
|
|
|
661
|
-
function extractStructuredToolCallContent(value: unknown): ToolCallContent[] {
|
|
667
|
+
function extractStructuredToolCallContent(value: unknown, options: AcpEventMapperOptions): ToolCallContent[] {
|
|
662
668
|
const blocks = getContentBlocks(value);
|
|
663
669
|
if (!blocks) {
|
|
664
670
|
return [];
|
|
@@ -666,7 +672,7 @@ function extractStructuredToolCallContent(value: unknown): ToolCallContent[] {
|
|
|
666
672
|
|
|
667
673
|
const content: ToolCallContent[] = [];
|
|
668
674
|
for (const block of blocks) {
|
|
669
|
-
const toolCallContent = toToolCallContent(block);
|
|
675
|
+
const toolCallContent = toToolCallContent(block, options);
|
|
670
676
|
if (toolCallContent) {
|
|
671
677
|
content.push(toolCallContent);
|
|
672
678
|
}
|
|
@@ -685,7 +691,7 @@ function getContentBlocks(value: unknown): unknown[] | undefined {
|
|
|
685
691
|
return Array.isArray(content) ? content : undefined;
|
|
686
692
|
}
|
|
687
693
|
|
|
688
|
-
function toToolCallContent(value: unknown): ToolCallContent | undefined {
|
|
694
|
+
function toToolCallContent(value: unknown, options: AcpEventMapperOptions): ToolCallContent | undefined {
|
|
689
695
|
const type = getContentType(value);
|
|
690
696
|
if (!type) {
|
|
691
697
|
return undefined;
|
|
@@ -697,21 +703,8 @@ function toToolCallContent(value: unknown): ToolCallContent | undefined {
|
|
|
697
703
|
return text ? textToolCallContent(text) : undefined;
|
|
698
704
|
}
|
|
699
705
|
case "image":
|
|
700
|
-
case "audio":
|
|
701
|
-
|
|
702
|
-
const mimeType = extractStringProperty<BinaryLikeContent>(value, "mimeType");
|
|
703
|
-
if (!data || !mimeType) {
|
|
704
|
-
return undefined;
|
|
705
|
-
}
|
|
706
|
-
return {
|
|
707
|
-
type: "content",
|
|
708
|
-
content: {
|
|
709
|
-
type,
|
|
710
|
-
data,
|
|
711
|
-
mimeType,
|
|
712
|
-
},
|
|
713
|
-
};
|
|
714
|
-
}
|
|
706
|
+
case "audio":
|
|
707
|
+
return binaryToolCallContent(type, value, options);
|
|
715
708
|
case "resource_link": {
|
|
716
709
|
const uri = extractStringProperty<ResourceLinkLikeContent>(value, "uri");
|
|
717
710
|
const name = extractStringProperty<ResourceLinkLikeContent>(value, "name");
|
|
@@ -769,6 +762,64 @@ function toToolCallContent(value: unknown): ToolCallContent | undefined {
|
|
|
769
762
|
}
|
|
770
763
|
}
|
|
771
764
|
|
|
765
|
+
function binaryToolCallContent(
|
|
766
|
+
type: "image" | "audio",
|
|
767
|
+
value: unknown,
|
|
768
|
+
options: AcpEventMapperOptions,
|
|
769
|
+
): ToolCallContent | undefined {
|
|
770
|
+
const data = extractStringProperty<BinaryLikeContent>(value, "data");
|
|
771
|
+
const mimeType = extractStringProperty<BinaryLikeContent>(value, "mimeType");
|
|
772
|
+
if (!data || !mimeType) {
|
|
773
|
+
return undefined;
|
|
774
|
+
}
|
|
775
|
+
return {
|
|
776
|
+
type: "content",
|
|
777
|
+
content: {
|
|
778
|
+
type,
|
|
779
|
+
data: type === "image" ? (options.resolveImageData?.(data, mimeType) ?? data) : data,
|
|
780
|
+
mimeType,
|
|
781
|
+
},
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function extractDetailsImageToolCallContent(
|
|
786
|
+
value: unknown,
|
|
787
|
+
options: AcpEventMapperOptions,
|
|
788
|
+
existing: ToolCallContent[],
|
|
789
|
+
): ToolCallContent[] {
|
|
790
|
+
const images = extractDetailsImages(value);
|
|
791
|
+
if (!images) {
|
|
792
|
+
return [];
|
|
793
|
+
}
|
|
794
|
+
const seen = new Set(existing.map(imageContentKey).filter((key): key is string => key !== undefined));
|
|
795
|
+
const content: ToolCallContent[] = [];
|
|
796
|
+
for (const image of images) {
|
|
797
|
+
const toolCallContent = binaryToolCallContent("image", image, options);
|
|
798
|
+
const key = imageContentKey(toolCallContent);
|
|
799
|
+
if (!toolCallContent || !key || seen.has(key)) {
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
seen.add(key);
|
|
803
|
+
content.push(toolCallContent);
|
|
804
|
+
}
|
|
805
|
+
return content;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function extractDetailsImages(value: unknown): unknown[] | undefined {
|
|
809
|
+
if (typeof value !== "object" || value === null) return undefined;
|
|
810
|
+
const details = (value as DetailsContainer).details;
|
|
811
|
+
if (typeof details !== "object" || details === null) return undefined;
|
|
812
|
+
const images = (details as { images?: unknown }).images;
|
|
813
|
+
return Array.isArray(images) && images.length > 0 ? images : undefined;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function imageContentKey(value: ToolCallContent | undefined): string | undefined {
|
|
817
|
+
if (value?.type !== "content" || value.content.type !== "image") {
|
|
818
|
+
return undefined;
|
|
819
|
+
}
|
|
820
|
+
return `${value.content.mimeType}\u0000${value.content.data}`;
|
|
821
|
+
}
|
|
822
|
+
|
|
772
823
|
function extractEmbeddedResource(
|
|
773
824
|
value: unknown,
|
|
774
825
|
): { uri: string; text: string; mimeType?: string } | { uri: string; blob: string; mimeType?: string } | undefined {
|
|
@@ -846,6 +897,12 @@ function extractReadableText(value: unknown): string | undefined {
|
|
|
846
897
|
if (text.length > 0) {
|
|
847
898
|
return normalizeText(text);
|
|
848
899
|
}
|
|
900
|
+
if (hasBinaryContentBlock(contentBlocks)) {
|
|
901
|
+
return undefined;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
if (extractDetailsImages(value)) {
|
|
905
|
+
return undefined;
|
|
849
906
|
}
|
|
850
907
|
if (isTerminalOnlyDetails(value)) {
|
|
851
908
|
return undefined;
|
|
@@ -895,6 +952,13 @@ function getContentType(value: unknown): string | undefined {
|
|
|
895
952
|
return typeof type === "string" ? type : undefined;
|
|
896
953
|
}
|
|
897
954
|
|
|
955
|
+
function hasBinaryContentBlock(blocks: unknown[]): boolean {
|
|
956
|
+
return blocks.some(block => {
|
|
957
|
+
const type = getContentType(block);
|
|
958
|
+
return type === "image" || type === "audio";
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
|
|
898
962
|
function extractStringProperty<T extends object>(value: unknown, key: keyof T): string | undefined {
|
|
899
963
|
if (typeof value !== "object" || value === null || !(key in value)) {
|
|
900
964
|
return undefined;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<project-context>
|
|
2
|
+
These context files carry the user's standing instructions for this project (AGENTS.md and the like). The driving agent is bound by them. Hold the agent to them and flag drift the moment it starts; never advise against what these files mandate.
|
|
3
|
+
{{#each contextFiles}}
|
|
4
|
+
<file path="{{path}}">
|
|
5
|
+
{{content}}
|
|
6
|
+
</file>
|
|
7
|
+
{{/each}}
|
|
8
|
+
</project-context>
|
package/src/sdk.ts
CHANGED
|
@@ -25,7 +25,12 @@ import { FALLBACK_DIALECT, preferredDialect } from "@oh-my-pi/pi-catalog/identit
|
|
|
25
25
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
26
26
|
import { $env, $flag, getAgentDir, getProjectDir, logger, postmortem, prompt, Snowflake } from "@oh-my-pi/pi-utils";
|
|
27
27
|
import { INTENT_FIELD } from "@oh-my-pi/pi-wire";
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
ADVISOR_READONLY_TOOL_NAMES,
|
|
30
|
+
discoverWatchdogFiles,
|
|
31
|
+
formatActiveRepoWatchdogPrompt,
|
|
32
|
+
formatAdvisorContextPrompt,
|
|
33
|
+
} from "./advisor";
|
|
29
34
|
import { type AsyncJob, AsyncJobManager } from "./async";
|
|
30
35
|
import { AutoLearnController, buildAutoLearnInstructions } from "./autolearn/controller";
|
|
31
36
|
import { loadCapability } from "./capability";
|
|
@@ -2678,11 +2683,16 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2678
2683
|
advisorWatchdogPrompts.push(formatActiveRepoWatchdogPrompt(activeRepoContext));
|
|
2679
2684
|
}
|
|
2680
2685
|
const advisorWatchdogPrompt = advisorWatchdogPrompts.length > 0 ? advisorWatchdogPrompts.join("\n\n") : undefined;
|
|
2686
|
+
// Hand the advisor the same project context files (AGENTS.md, etc.) the
|
|
2687
|
+
// primary agent gets in its system prompt, so the read-only reviewer judges
|
|
2688
|
+
// against the user's standing project rules instead of advising blind.
|
|
2689
|
+
const advisorContextPrompt = formatAdvisorContextPrompt(contextFiles);
|
|
2681
2690
|
// Owned only when this session created the manager; subagents receive a
|
|
2682
2691
|
// parent's manager via `options.mcpManager` and MUST NOT disconnect it.
|
|
2683
2692
|
const ownedMcpManager = options.mcpManager ? undefined : mcpManager;
|
|
2684
2693
|
session = new AgentSession({
|
|
2685
2694
|
advisorWatchdogPrompt,
|
|
2695
|
+
advisorContextPrompt,
|
|
2686
2696
|
agent,
|
|
2687
2697
|
pruneToolDescriptions: inlineToolDescriptors,
|
|
2688
2698
|
thinkingLevel: autoThinking ? AUTO_THINKING : effectiveThinkingLevel,
|
|
@@ -589,6 +589,12 @@ export interface AgentSessionConfig {
|
|
|
589
589
|
advisorReadOnlyTools?: AgentTool[];
|
|
590
590
|
/** Preloaded watchdog prompt content for the advisor. */
|
|
591
591
|
advisorWatchdogPrompt?: string;
|
|
592
|
+
/**
|
|
593
|
+
* Preloaded project context files (AGENTS.md, etc.) rendered as a system-prompt
|
|
594
|
+
* block for the advisor — the same standing instructions the primary agent
|
|
595
|
+
* receives, so the reviewer holds the agent to them.
|
|
596
|
+
*/
|
|
597
|
+
advisorContextPrompt?: string;
|
|
592
598
|
/**
|
|
593
599
|
* Strip tool descriptions from provider-bound tool specs on side requests
|
|
594
600
|
* (handoff). Must match the session-start value used to build the system
|
|
@@ -1220,6 +1226,7 @@ export class AgentSession {
|
|
|
1220
1226
|
#advisorAdviseTool?: AdviseTool;
|
|
1221
1227
|
#advisorReadOnlyTools?: AgentTool[];
|
|
1222
1228
|
#advisorWatchdogPrompt?: string;
|
|
1229
|
+
#advisorContextPrompt?: string;
|
|
1223
1230
|
#advisorYieldQueueUnsubscribe?: () => void;
|
|
1224
1231
|
/** Persists the advisor agent's turns to `<session>/__advisor.jsonl` for stats
|
|
1225
1232
|
* attribution and Agent Hub observability. Undefined when no advisor is active. */
|
|
@@ -1643,6 +1650,7 @@ export class AgentSession {
|
|
|
1643
1650
|
this.agent.serviceTierResolver = model => this.#effectiveServiceTier(model);
|
|
1644
1651
|
this.#advisorReadOnlyTools = config.advisorReadOnlyTools;
|
|
1645
1652
|
this.#advisorWatchdogPrompt = config.advisorWatchdogPrompt;
|
|
1653
|
+
this.#advisorContextPrompt = config.advisorContextPrompt;
|
|
1646
1654
|
this.#pruneToolDescriptions = config.pruneToolDescriptions === true;
|
|
1647
1655
|
this.#validateRetryFallbackChains();
|
|
1648
1656
|
this.#toolRegistry = config.toolRegistry ?? new Map();
|
|
@@ -1960,6 +1968,9 @@ export class AgentSession {
|
|
|
1960
1968
|
const appendOnlyContext = new AppendOnlyContextManager();
|
|
1961
1969
|
const advisorThinkingLevel = advisorSel.thinkingLevel ?? ThinkingLevel.Medium;
|
|
1962
1970
|
const systemPrompt = [advisorSystemPrompt];
|
|
1971
|
+
if (this.#advisorContextPrompt) {
|
|
1972
|
+
systemPrompt.push(this.#advisorContextPrompt);
|
|
1973
|
+
}
|
|
1963
1974
|
if (this.#advisorWatchdogPrompt) {
|
|
1964
1975
|
systemPrompt.push(this.#advisorWatchdogPrompt);
|
|
1965
1976
|
}
|
|
@@ -152,6 +152,17 @@ export class BlobStore {
|
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
/** Synchronous variant of {@link get}. */
|
|
156
|
+
getSync(hash: string): Buffer | null {
|
|
157
|
+
const blobPath = path.join(this.dir, hash);
|
|
158
|
+
try {
|
|
159
|
+
return fs.readFileSync(blobPath);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
if (isEnoent(err)) return null;
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
155
166
|
/** Check if a blob exists. */
|
|
156
167
|
async has(hash: string): Promise<boolean> {
|
|
157
168
|
try {
|
|
@@ -253,3 +264,16 @@ export async function resolveImageData(blobStore: BlobStore, data: string): Prom
|
|
|
253
264
|
}
|
|
254
265
|
return buffer.toString("base64");
|
|
255
266
|
}
|
|
267
|
+
|
|
268
|
+
/** Synchronous variant of {@link resolveImageData}. */
|
|
269
|
+
export function resolveImageDataSync(blobStore: BlobStore, data: string): string {
|
|
270
|
+
const hash = parseBlobRef(data);
|
|
271
|
+
if (!hash) return data;
|
|
272
|
+
|
|
273
|
+
const buffer = blobStore.getSync(hash);
|
|
274
|
+
if (!buffer) {
|
|
275
|
+
logger.warn("Blob not found for image reference", { hash });
|
|
276
|
+
return data;
|
|
277
|
+
}
|
|
278
|
+
return buffer.toString("base64");
|
|
279
|
+
}
|