@oh-my-pi/pi-coding-agent 11.2.3 → 11.4.0
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 +119 -4
- package/examples/extensions/plan-mode.ts +1 -1
- package/examples/hooks/qna.ts +1 -1
- package/examples/hooks/status-line.ts +1 -1
- package/examples/sdk/11-sessions.ts +1 -1
- package/package.json +8 -8
- package/src/cli/args.ts +9 -6
- package/src/cli/update-cli.ts +2 -2
- package/src/commands/index/index.ts +2 -5
- package/src/commit/agentic/agent.ts +1 -1
- package/src/commit/changelog/index.ts +2 -2
- package/src/config/keybindings.ts +16 -1
- package/src/config/model-registry.ts +25 -20
- package/src/config/model-resolver.ts +8 -8
- package/src/config/resolve-config-value.ts +92 -0
- package/src/config/settings-schema.ts +9 -0
- package/src/config.ts +14 -1
- package/src/export/html/template.css +7 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +33 -16
- package/src/extensibility/custom-commands/bundled/review/index.ts +1 -1
- package/src/extensibility/extensions/index.ts +18 -0
- package/src/extensibility/extensions/loader.ts +15 -0
- package/src/extensibility/extensions/runner.ts +78 -1
- package/src/extensibility/extensions/types.ts +131 -5
- package/src/extensibility/extensions/wrapper.ts +1 -1
- package/src/extensibility/plugins/git-url.ts +270 -0
- package/src/extensibility/plugins/index.ts +2 -0
- package/src/extensibility/slash-commands.ts +45 -0
- package/src/index.ts +7 -0
- package/src/lsp/render.ts +50 -43
- package/src/lsp/utils.ts +2 -2
- package/src/main.ts +11 -10
- package/src/mcp/transports/stdio.ts +3 -5
- package/src/modes/components/custom-message.ts +0 -8
- package/src/modes/components/diff.ts +41 -13
- package/src/modes/components/footer.ts +4 -4
- package/src/modes/components/model-selector.ts +4 -0
- package/src/modes/components/todo-display.ts +13 -3
- package/src/modes/components/tool-execution.ts +30 -16
- package/src/modes/components/tree-selector.ts +50 -19
- package/src/modes/controllers/event-controller.ts +1 -0
- package/src/modes/controllers/extension-ui-controller.ts +34 -2
- package/src/modes/controllers/input-controller.ts +47 -33
- package/src/modes/controllers/selector-controller.ts +10 -15
- package/src/modes/interactive-mode.ts +50 -38
- package/src/modes/print-mode.ts +6 -0
- package/src/modes/rpc/rpc-client.ts +4 -4
- package/src/modes/rpc/rpc-mode.ts +17 -2
- package/src/modes/rpc/rpc-types.ts +2 -2
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +3 -1
- package/src/patch/applicator.ts +106 -4
- package/src/patch/fuzzy.ts +1 -1
- package/src/patch/shared.ts +77 -63
- package/src/prompts/system/plan-mode-active.md +6 -6
- package/src/prompts/system/system-prompt.md +2 -1
- package/src/prompts/tools/ask.md +2 -2
- package/src/prompts/tools/gemini-image.md +2 -2
- package/src/prompts/tools/lsp.md +2 -2
- package/src/prompts/tools/patch.md +1 -1
- package/src/prompts/tools/python.md +3 -3
- package/src/prompts/tools/task.md +7 -1
- package/src/prompts/tools/todo-write.md +2 -2
- package/src/prompts/tools/web-search.md +2 -2
- package/src/prompts/tools/write.md +2 -5
- package/src/sdk.ts +15 -11
- package/src/session/agent-session.ts +92 -34
- package/src/session/auth-storage.ts +2 -1
- package/src/session/blob-store.ts +105 -0
- package/src/session/session-manager.ts +107 -44
- package/src/task/executor.ts +19 -9
- package/src/task/render.ts +80 -58
- package/src/tools/ask.ts +28 -5
- package/src/tools/bash.ts +47 -39
- package/src/tools/browser.ts +248 -26
- package/src/tools/calculator.ts +42 -23
- package/src/tools/fetch.ts +33 -16
- package/src/tools/find.ts +57 -22
- package/src/tools/grep.ts +54 -25
- package/src/tools/index.ts +5 -5
- package/src/tools/notebook.ts +19 -6
- package/src/tools/path-utils.ts +26 -1
- package/src/tools/python.ts +20 -14
- package/src/tools/read.ts +21 -8
- package/src/tools/render-utils.ts +5 -45
- package/src/tools/ssh.ts +59 -53
- package/src/tools/submit-result.ts +2 -2
- package/src/tools/todo-write.ts +32 -14
- package/src/tools/truncate.ts +1 -1
- package/src/tools/write.ts +42 -26
- package/src/tui/code-cell.ts +1 -1
- package/src/tui/output-block.ts +61 -3
- package/src/tui/tree-list.ts +4 -4
- package/src/tui/utils.ts +71 -1
- package/src/utils/frontmatter.ts +1 -1
- package/src/utils/title-generator.ts +1 -1
- package/src/utils/tools-manager.ts +18 -2
- package/src/web/scrapers/osv.ts +4 -1
- package/src/web/scrapers/youtube.ts +1 -1
- package/src/web/search/index.ts +1 -1
- package/src/web/search/render.ts +96 -90
package/src/sdk.ts
CHANGED
|
@@ -387,7 +387,7 @@ function customToolToDefinition(tool: CustomTool): ToolDefinition {
|
|
|
387
387
|
label: tool.label,
|
|
388
388
|
description: tool.description,
|
|
389
389
|
parameters: tool.parameters,
|
|
390
|
-
execute: (toolCallId, params, onUpdate, ctx
|
|
390
|
+
execute: (toolCallId, params, signal, onUpdate, ctx) =>
|
|
391
391
|
tool.execute(toolCallId, params, onUpdate, createCustomToolContext(ctx), signal),
|
|
392
392
|
onSession: tool.onSession ? (event, ctx) => tool.onSession?.(event, createCustomToolContext(ctx)) : undefined,
|
|
393
393
|
renderCall: tool.renderCall,
|
|
@@ -506,6 +506,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
506
506
|
const existingSession = sessionManager.buildSessionContext();
|
|
507
507
|
time("loadSession");
|
|
508
508
|
const hasExistingSession = existingSession.messages.length > 0;
|
|
509
|
+
const hasThinkingEntry = sessionManager.getBranch().some(entry => entry.type === "thinking_level_change");
|
|
509
510
|
|
|
510
511
|
const hasExplicitModel = options.model !== undefined;
|
|
511
512
|
let model = options.model;
|
|
@@ -563,7 +564,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
563
564
|
|
|
564
565
|
// If session has data, restore thinking level from it
|
|
565
566
|
if (thinkingLevel === undefined && hasExistingSession) {
|
|
566
|
-
thinkingLevel =
|
|
567
|
+
thinkingLevel = hasThinkingEntry
|
|
568
|
+
? (existingSession.thinkingLevel as ThinkingLevel)
|
|
569
|
+
: ((settingsInstance.get("defaultThinkingLevel") ?? "off") as ThinkingLevel);
|
|
567
570
|
}
|
|
568
571
|
|
|
569
572
|
// Fall back to settings default
|
|
@@ -698,7 +701,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
698
701
|
onConnecting: serverNames => {
|
|
699
702
|
if (options.hasUI && serverNames.length > 0) {
|
|
700
703
|
process.stderr.write(
|
|
701
|
-
chalk.gray(`Connecting to MCP servers: ${serverNames.join(", ")}
|
|
704
|
+
chalk.gray(`Connecting to MCP servers: ${serverNames.join(", ")}…
|
|
702
705
|
`),
|
|
703
706
|
);
|
|
704
707
|
}
|
|
@@ -1016,14 +1019,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1016
1019
|
thinkingBudgets: settingsInstance.getGroup("thinkingBudgets"),
|
|
1017
1020
|
kimiApiFormat: settingsInstance.get("providers.kimiApiFormat") ?? "anthropic",
|
|
1018
1021
|
getToolContext: tc => toolContextStore.getContext(tc),
|
|
1019
|
-
getApiKey: async
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
}
|
|
1024
|
-
const key = await modelRegistry.getApiKey(currentModel, sessionId);
|
|
1022
|
+
getApiKey: async provider => {
|
|
1023
|
+
// Use the provider argument from the in-flight request;
|
|
1024
|
+
// agent.state.model may already be switched mid-turn.
|
|
1025
|
+
const key = await modelRegistry.getApiKeyForProvider(provider, sessionId);
|
|
1025
1026
|
if (!key) {
|
|
1026
|
-
throw new Error(`No API key found for provider "${
|
|
1027
|
+
throw new Error(`No API key found for provider "${provider}"`);
|
|
1027
1028
|
}
|
|
1028
1029
|
return key;
|
|
1029
1030
|
},
|
|
@@ -1036,6 +1037,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1036
1037
|
// Restore messages if session has existing data
|
|
1037
1038
|
if (hasExistingSession) {
|
|
1038
1039
|
agent.replaceMessages(existingSession.messages);
|
|
1040
|
+
if (!hasThinkingEntry) {
|
|
1041
|
+
sessionManager.appendThinkingLevelChange(thinkingLevel);
|
|
1042
|
+
}
|
|
1039
1043
|
} else {
|
|
1040
1044
|
// Save initial model and thinking level for new sessions so they can be restored on resume
|
|
1041
1045
|
if (model) {
|
|
@@ -1072,7 +1076,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1072
1076
|
const result = await warmupLspServers(cwd, {
|
|
1073
1077
|
onConnecting: serverNames => {
|
|
1074
1078
|
if (options.hasUI && serverNames.length > 0) {
|
|
1075
|
-
process.stderr.write(chalk.gray(`Starting LSP servers: ${serverNames.join(", ")}
|
|
1079
|
+
process.stderr.write(chalk.gray(`Starting LSP servers: ${serverNames.join(", ")}…\n`));
|
|
1076
1080
|
}
|
|
1077
1081
|
},
|
|
1078
1082
|
});
|
|
@@ -29,7 +29,6 @@ import type {
|
|
|
29
29
|
} from "@oh-my-pi/pi-ai";
|
|
30
30
|
import { isContextOverflow, modelsAreEqual, supportsXhigh } from "@oh-my-pi/pi-ai";
|
|
31
31
|
import { abortableSleep, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
32
|
-
import { YAML } from "bun";
|
|
33
32
|
import type { Rule } from "../capability/rule";
|
|
34
33
|
import { getAgentDbPath } from "../config";
|
|
35
34
|
import { MODEL_ROLE_IDS, type ModelRegistry, type ModelRole } from "../config/model-registry";
|
|
@@ -237,6 +236,8 @@ const noOpUIContext: ExtensionUIContext = {
|
|
|
237
236
|
setFooter: () => {},
|
|
238
237
|
setHeader: () => {},
|
|
239
238
|
setEditorComponent: () => {},
|
|
239
|
+
getToolsExpanded: () => false,
|
|
240
|
+
setToolsExpanded: () => {},
|
|
240
241
|
};
|
|
241
242
|
|
|
242
243
|
async function cleanupSshResources(): Promise<void> {
|
|
@@ -906,6 +907,11 @@ export class AgentSession {
|
|
|
906
907
|
return this.agent.state.isStreaming || this._promptInFlight;
|
|
907
908
|
}
|
|
908
909
|
|
|
910
|
+
/** Current effective system prompt (includes any per-turn extension modifications) */
|
|
911
|
+
get systemPrompt(): string {
|
|
912
|
+
return this.agent.state.systemPrompt;
|
|
913
|
+
}
|
|
914
|
+
|
|
909
915
|
/** Current retry attempt (0 if not retrying) */
|
|
910
916
|
get retryAttempt(): number {
|
|
911
917
|
return this._retryAttempt;
|
|
@@ -1426,6 +1432,11 @@ export class AgentSession {
|
|
|
1426
1432
|
instructionsOrOptions && typeof instructionsOrOptions === "object" ? instructionsOrOptions : undefined;
|
|
1427
1433
|
await this.compact(instructions, options);
|
|
1428
1434
|
},
|
|
1435
|
+
switchSession: async sessionPath => {
|
|
1436
|
+
const success = await this.switchSession(sessionPath);
|
|
1437
|
+
return { cancelled: !success };
|
|
1438
|
+
},
|
|
1439
|
+
getSystemPrompt: () => this.systemPrompt,
|
|
1429
1440
|
};
|
|
1430
1441
|
}
|
|
1431
1442
|
|
|
@@ -1477,25 +1488,25 @@ export class AgentSession {
|
|
|
1477
1488
|
/**
|
|
1478
1489
|
* Queue a steering message to interrupt the agent mid-run.
|
|
1479
1490
|
*/
|
|
1480
|
-
async steer(text: string): Promise<void> {
|
|
1491
|
+
async steer(text: string, images?: ImageContent[]): Promise<void> {
|
|
1481
1492
|
if (text.startsWith("/")) {
|
|
1482
1493
|
this._throwIfExtensionCommand(text);
|
|
1483
1494
|
}
|
|
1484
1495
|
|
|
1485
1496
|
const expandedText = expandPromptTemplate(text, [...this._promptTemplates]);
|
|
1486
|
-
await this._queueSteer(expandedText);
|
|
1497
|
+
await this._queueSteer(expandedText, images);
|
|
1487
1498
|
}
|
|
1488
1499
|
|
|
1489
1500
|
/**
|
|
1490
1501
|
* Queue a follow-up message to process after the agent would otherwise stop.
|
|
1491
1502
|
*/
|
|
1492
|
-
async followUp(text: string): Promise<void> {
|
|
1503
|
+
async followUp(text: string, images?: ImageContent[]): Promise<void> {
|
|
1493
1504
|
if (text.startsWith("/")) {
|
|
1494
1505
|
this._throwIfExtensionCommand(text);
|
|
1495
1506
|
}
|
|
1496
1507
|
|
|
1497
1508
|
const expandedText = expandPromptTemplate(text, [...this._promptTemplates]);
|
|
1498
|
-
await this._queueFollowUp(expandedText);
|
|
1509
|
+
await this._queueFollowUp(expandedText, images);
|
|
1499
1510
|
}
|
|
1500
1511
|
|
|
1501
1512
|
/**
|
|
@@ -1728,11 +1739,14 @@ export class AgentSession {
|
|
|
1728
1739
|
await this.abort();
|
|
1729
1740
|
this.agent.reset();
|
|
1730
1741
|
await this.sessionManager.flush();
|
|
1731
|
-
this.sessionManager.newSession(options);
|
|
1742
|
+
await this.sessionManager.newSession(options);
|
|
1732
1743
|
this.agent.sessionId = this.sessionManager.getSessionId();
|
|
1733
1744
|
this._steeringMessages = [];
|
|
1734
1745
|
this._followUpMessages = [];
|
|
1735
1746
|
this._pendingNextTurnMessages = [];
|
|
1747
|
+
|
|
1748
|
+
this.sessionManager.appendThinkingLevelChange(this.thinkingLevel);
|
|
1749
|
+
|
|
1736
1750
|
this._todoReminderCount = 0;
|
|
1737
1751
|
this._planReferenceSent = false;
|
|
1738
1752
|
this._reconnectToAgent();
|
|
@@ -1934,22 +1948,39 @@ export class AgentSession {
|
|
|
1934
1948
|
return { model: next.model, thinkingLevel: this.thinkingLevel, role: next.role };
|
|
1935
1949
|
}
|
|
1936
1950
|
|
|
1951
|
+
private async _getScopedModelsWithApiKey(): Promise<Array<{ model: Model; thinkingLevel: ThinkingLevel }>> {
|
|
1952
|
+
const apiKeysByProvider = new Map<string, string | undefined>();
|
|
1953
|
+
const result: Array<{ model: Model; thinkingLevel: ThinkingLevel }> = [];
|
|
1954
|
+
|
|
1955
|
+
for (const scoped of this._scopedModels) {
|
|
1956
|
+
const provider = scoped.model.provider;
|
|
1957
|
+
let apiKey: string | undefined;
|
|
1958
|
+
if (apiKeysByProvider.has(provider)) {
|
|
1959
|
+
apiKey = apiKeysByProvider.get(provider);
|
|
1960
|
+
} else {
|
|
1961
|
+
apiKey = await this._modelRegistry.getApiKeyForProvider(provider, this.sessionId);
|
|
1962
|
+
apiKeysByProvider.set(provider, apiKey);
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
if (apiKey) {
|
|
1966
|
+
result.push(scoped);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
return result;
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1937
1973
|
private async _cycleScopedModel(direction: "forward" | "backward"): Promise<ModelCycleResult | undefined> {
|
|
1938
|
-
|
|
1974
|
+
const scopedModels = await this._getScopedModelsWithApiKey();
|
|
1975
|
+
if (scopedModels.length <= 1) return undefined;
|
|
1939
1976
|
|
|
1940
1977
|
const currentModel = this.model;
|
|
1941
|
-
let currentIndex =
|
|
1978
|
+
let currentIndex = scopedModels.findIndex(sm => modelsAreEqual(sm.model, currentModel));
|
|
1942
1979
|
|
|
1943
1980
|
if (currentIndex === -1) currentIndex = 0;
|
|
1944
|
-
const len =
|
|
1981
|
+
const len = scopedModels.length;
|
|
1945
1982
|
const nextIndex = direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len;
|
|
1946
|
-
const next =
|
|
1947
|
-
|
|
1948
|
-
// Validate API key
|
|
1949
|
-
const apiKey = await this._modelRegistry.getApiKey(next.model, this.sessionId);
|
|
1950
|
-
if (!apiKey) {
|
|
1951
|
-
throw new Error(`No API key for ${next.model.provider}/${next.model.id}`);
|
|
1952
|
-
}
|
|
1983
|
+
const next = scopedModels[nextIndex];
|
|
1953
1984
|
|
|
1954
1985
|
// Apply model
|
|
1955
1986
|
this.agent.setModel(next.model);
|
|
@@ -2005,15 +2036,22 @@ export class AgentSession {
|
|
|
2005
2036
|
/**
|
|
2006
2037
|
* Set thinking level.
|
|
2007
2038
|
* Clamps to model capabilities based on available thinking levels.
|
|
2008
|
-
* Saves to session
|
|
2039
|
+
* Saves to session and settings only if the level actually changes.
|
|
2009
2040
|
*/
|
|
2010
2041
|
setThinkingLevel(level: ThinkingLevel, persist: boolean = false): void {
|
|
2011
2042
|
const availableLevels = this.getAvailableThinkingLevels();
|
|
2012
2043
|
const effectiveLevel = availableLevels.includes(level) ? level : this._clampThinkingLevel(level, availableLevels);
|
|
2044
|
+
|
|
2045
|
+
// Only persist if actually changing
|
|
2046
|
+
const isChanging = effectiveLevel !== this.agent.state.thinkingLevel;
|
|
2047
|
+
|
|
2013
2048
|
this.agent.setThinkingLevel(effectiveLevel);
|
|
2014
|
-
|
|
2015
|
-
if (
|
|
2016
|
-
this.
|
|
2049
|
+
|
|
2050
|
+
if (isChanging) {
|
|
2051
|
+
this.sessionManager.appendThinkingLevelChange(effectiveLevel);
|
|
2052
|
+
if (persist) {
|
|
2053
|
+
this.settings.set("defaultThinkingLevel", effectiveLevel);
|
|
2054
|
+
}
|
|
2017
2055
|
}
|
|
2018
2056
|
}
|
|
2019
2057
|
|
|
@@ -2404,7 +2442,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
2404
2442
|
|
|
2405
2443
|
// Start a new session
|
|
2406
2444
|
await this.sessionManager.flush();
|
|
2407
|
-
this.sessionManager.newSession();
|
|
2445
|
+
await this.sessionManager.newSession();
|
|
2408
2446
|
this.agent.reset();
|
|
2409
2447
|
this.agent.sessionId = this.sessionManager.getSessionId();
|
|
2410
2448
|
this._steeringMessages = [];
|
|
@@ -2903,8 +2941,8 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
2903
2941
|
}
|
|
2904
2942
|
|
|
2905
2943
|
private _isRetryableErrorMessage(errorMessage: string): boolean {
|
|
2906
|
-
// Match: overloaded_error, rate limit, usage limit, 429, 500, 502, 503, 504, service unavailable, connection error, fetch failed
|
|
2907
|
-
return /overloaded|rate.?limit|usage.?limit|too many requests|429|500|502|503|504|service.?unavailable|server error|internal error|connection.?error|fetch failed/i.test(
|
|
2944
|
+
// Match: overloaded_error, rate limit, usage limit, 429, 500, 502, 503, 504, service unavailable, connection error, fetch failed, retry delay exceeded
|
|
2945
|
+
return /overloaded|rate.?limit|usage.?limit|too many requests|429|500|502|503|504|service.?unavailable|server error|internal error|connection.?error|fetch failed|retry delay/i.test(
|
|
2908
2946
|
errorMessage,
|
|
2909
2947
|
);
|
|
2910
2948
|
}
|
|
@@ -3354,9 +3392,19 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3354
3392
|
}
|
|
3355
3393
|
}
|
|
3356
3394
|
|
|
3357
|
-
|
|
3358
|
-
|
|
3395
|
+
const hasThinkingEntry = this.sessionManager.getBranch().some(entry => entry.type === "thinking_level_change");
|
|
3396
|
+
const defaultThinkingLevel = (this.settings.get("defaultThinkingLevel") ?? "off") as ThinkingLevel;
|
|
3397
|
+
|
|
3398
|
+
if (hasThinkingEntry) {
|
|
3399
|
+
// Restore thinking level if saved (setThinkingLevel clamps to model capabilities)
|
|
3359
3400
|
this.setThinkingLevel(sessionContext.thinkingLevel as ThinkingLevel);
|
|
3401
|
+
} else {
|
|
3402
|
+
const availableLevels = this.getAvailableThinkingLevels();
|
|
3403
|
+
const effectiveLevel = availableLevels.includes(defaultThinkingLevel)
|
|
3404
|
+
? defaultThinkingLevel
|
|
3405
|
+
: this._clampThinkingLevel(defaultThinkingLevel, availableLevels);
|
|
3406
|
+
this.agent.setThinkingLevel(effectiveLevel);
|
|
3407
|
+
this.sessionManager.appendThinkingLevelChange(effectiveLevel);
|
|
3360
3408
|
}
|
|
3361
3409
|
|
|
3362
3410
|
this._reconnectToAgent();
|
|
@@ -3404,7 +3452,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3404
3452
|
await this.sessionManager.flush();
|
|
3405
3453
|
|
|
3406
3454
|
if (!selectedEntry.parentId) {
|
|
3407
|
-
this.sessionManager.newSession();
|
|
3455
|
+
await this.sessionManager.newSession({ parentSession: previousSessionFile });
|
|
3408
3456
|
} else {
|
|
3409
3457
|
this.sessionManager.createBranchedSession(selectedEntry.parentId);
|
|
3410
3458
|
}
|
|
@@ -3826,6 +3874,16 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3826
3874
|
formatSessionAsText(): string {
|
|
3827
3875
|
const lines: string[] = [];
|
|
3828
3876
|
|
|
3877
|
+
/** Serialize an object as XML parameter elements, one per key. */
|
|
3878
|
+
function formatArgsAsXml(args: Record<string, unknown>, indent = "\t"): string {
|
|
3879
|
+
const parts: string[] = [];
|
|
3880
|
+
for (const [key, value] of Object.entries(args)) {
|
|
3881
|
+
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
3882
|
+
parts.push(`${indent}<parameter name="${key}">${text}</parameter>`);
|
|
3883
|
+
}
|
|
3884
|
+
return parts.join("\n");
|
|
3885
|
+
}
|
|
3886
|
+
|
|
3829
3887
|
// Include system prompt at the beginning
|
|
3830
3888
|
const systemPrompt = this.agent.state.systemPrompt;
|
|
3831
3889
|
if (systemPrompt) {
|
|
@@ -3865,12 +3923,11 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3865
3923
|
if (tools.length > 0) {
|
|
3866
3924
|
lines.push("## Available Tools\n");
|
|
3867
3925
|
for (const tool of tools) {
|
|
3868
|
-
lines.push(
|
|
3926
|
+
lines.push(`<tool name="${tool.name}">`);
|
|
3869
3927
|
lines.push(tool.description);
|
|
3870
|
-
lines.push("\n```yaml");
|
|
3871
3928
|
const parametersClean = stripTypeBoxFields(tool.parameters);
|
|
3872
|
-
lines.push(
|
|
3873
|
-
lines.push("
|
|
3929
|
+
lines.push(`\nParameters:\n${formatArgsAsXml(parametersClean as Record<string, unknown>)}`);
|
|
3930
|
+
lines.push("<" + "/tool>\n");
|
|
3874
3931
|
}
|
|
3875
3932
|
lines.push("\n");
|
|
3876
3933
|
}
|
|
@@ -3902,10 +3959,11 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3902
3959
|
lines.push(c.thinking);
|
|
3903
3960
|
lines.push("</thinking>\n");
|
|
3904
3961
|
} else if (c.type === "toolCall") {
|
|
3905
|
-
lines.push(
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3962
|
+
lines.push(`<invoke name="${c.name}">`);
|
|
3963
|
+
if (c.arguments && typeof c.arguments === "object") {
|
|
3964
|
+
lines.push(formatArgsAsXml(c.arguments as Record<string, unknown>));
|
|
3965
|
+
}
|
|
3966
|
+
lines.push("<" + "/invoke>\n");
|
|
3909
3967
|
}
|
|
3910
3968
|
}
|
|
3911
3969
|
lines.push("");
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
} from "@oh-my-pi/pi-ai";
|
|
35
35
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
36
36
|
import { getAgentDbPath } from "../config";
|
|
37
|
+
import { resolveConfigValue } from "../config/resolve-config-value";
|
|
37
38
|
import { AgentStorage } from "./agent-storage";
|
|
38
39
|
|
|
39
40
|
export type ApiKeyCredential = {
|
|
@@ -1294,7 +1295,7 @@ export class AuthStorage {
|
|
|
1294
1295
|
const apiKeySelection = this.selectCredentialByType(provider, "api_key", sessionId);
|
|
1295
1296
|
if (apiKeySelection) {
|
|
1296
1297
|
this.recordSessionCredential(provider, sessionId, "api_key", apiKeySelection.index);
|
|
1297
|
-
return apiKeySelection.credential.key;
|
|
1298
|
+
return resolveConfigValue(apiKeySelection.credential.key);
|
|
1298
1299
|
}
|
|
1299
1300
|
|
|
1300
1301
|
const oauthKey = await this.resolveOAuthApiKey(provider, sessionId, options);
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
4
|
+
|
|
5
|
+
const BLOB_PREFIX = "blob:sha256:";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Content-addressed blob store for externalizing large binary data (images) from session JSONL files.
|
|
9
|
+
*
|
|
10
|
+
* Files are stored at `<dir>/<sha256-hex>` with no extension. The SHA-256 hash is computed
|
|
11
|
+
* over the raw binary data (not base64). Content-addressing makes writes idempotent and
|
|
12
|
+
* provides automatic deduplication across sessions.
|
|
13
|
+
*/
|
|
14
|
+
export class BlobStore {
|
|
15
|
+
constructor(readonly dir: string) {}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Write binary data to the blob store.
|
|
19
|
+
* @returns SHA-256 hex hash of the data
|
|
20
|
+
*/
|
|
21
|
+
async put(data: Buffer): Promise<string> {
|
|
22
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
23
|
+
hasher.update(data);
|
|
24
|
+
const hash = hasher.digest("hex");
|
|
25
|
+
const blobPath = path.join(this.dir, hash);
|
|
26
|
+
|
|
27
|
+
// Content-addressed: skip write if blob already exists
|
|
28
|
+
try {
|
|
29
|
+
await fs.access(blobPath);
|
|
30
|
+
return hash;
|
|
31
|
+
} catch {
|
|
32
|
+
// Does not exist, write it
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
await Bun.write(blobPath, data);
|
|
36
|
+
return hash;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Read blob by hash, returns Buffer or null if not found. */
|
|
40
|
+
async get(hash: string): Promise<Buffer | null> {
|
|
41
|
+
const blobPath = path.join(this.dir, hash);
|
|
42
|
+
try {
|
|
43
|
+
const file = Bun.file(blobPath);
|
|
44
|
+
const ab = await file.arrayBuffer();
|
|
45
|
+
return Buffer.from(ab);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
if (isEnoent(err)) return null;
|
|
48
|
+
throw err;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Check if a blob exists. */
|
|
53
|
+
async has(hash: string): Promise<boolean> {
|
|
54
|
+
try {
|
|
55
|
+
await fs.access(path.join(this.dir, hash));
|
|
56
|
+
return true;
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Check if a data string is a blob reference. */
|
|
64
|
+
export function isBlobRef(data: string): boolean {
|
|
65
|
+
return data.startsWith(BLOB_PREFIX);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Extract the SHA-256 hash from a blob reference string. */
|
|
69
|
+
export function parseBlobRef(data: string): string | null {
|
|
70
|
+
if (!data.startsWith(BLOB_PREFIX)) return null;
|
|
71
|
+
return data.slice(BLOB_PREFIX.length);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Create a blob reference string from a SHA-256 hash. */
|
|
75
|
+
export function makeBlobRef(hash: string): string {
|
|
76
|
+
return `${BLOB_PREFIX}${hash}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Externalize an image's base64 data to the blob store, returning a blob reference.
|
|
81
|
+
* If the data is already a blob reference, returns it unchanged.
|
|
82
|
+
*/
|
|
83
|
+
export async function externalizeImageData(blobStore: BlobStore, base64Data: string): Promise<string> {
|
|
84
|
+
if (isBlobRef(base64Data)) return base64Data;
|
|
85
|
+
const buffer = Buffer.from(base64Data, "base64");
|
|
86
|
+
const hash = await blobStore.put(buffer);
|
|
87
|
+
return makeBlobRef(hash);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Resolve a blob reference back to base64 data.
|
|
92
|
+
* If the data is not a blob reference, returns it unchanged.
|
|
93
|
+
* If the blob is missing, logs a warning and returns a placeholder.
|
|
94
|
+
*/
|
|
95
|
+
export async function resolveImageData(blobStore: BlobStore, data: string): Promise<string> {
|
|
96
|
+
const hash = parseBlobRef(data);
|
|
97
|
+
if (!hash) return data;
|
|
98
|
+
|
|
99
|
+
const buffer = await blobStore.get(hash);
|
|
100
|
+
if (!buffer) {
|
|
101
|
+
logger.warn("Blob not found for image reference", { hash });
|
|
102
|
+
return data; // Return the ref as-is; downstream will see invalid base64 but won't crash
|
|
103
|
+
}
|
|
104
|
+
return buffer.toString("base64");
|
|
105
|
+
}
|