@oh-my-pi/pi-coding-agent 16.4.4 → 16.4.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 +62 -0
- package/dist/cli.js +3799 -3729
- package/dist/types/async/job-manager.d.ts +8 -0
- package/dist/types/cli/bench-cli.d.ts +1 -7
- package/dist/types/cli/usage-cli.d.ts +1 -0
- package/dist/types/commands/usage.d.ts +7 -0
- package/dist/types/config/settings-schema.d.ts +19 -9
- package/dist/types/config/settings.d.ts +3 -2
- package/dist/types/discovery/helpers.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +36 -0
- package/dist/types/irc/bus.d.ts +4 -0
- package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +1 -0
- package/dist/types/modes/components/ask-dialog.d.ts +27 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -8
- package/dist/types/modes/components/index.d.ts +2 -1
- package/dist/types/modes/components/model-browser.d.ts +100 -0
- package/dist/types/modes/components/model-hub.d.ts +52 -0
- package/dist/types/modes/components/pause-screen.d.ts +43 -0
- package/dist/types/modes/components/session-selector.d.ts +13 -0
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
- package/dist/types/modes/controllers/input-controller.d.ts +2 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -0
- package/dist/types/modes/queue-input.d.ts +8 -0
- package/dist/types/modes/shared.d.ts +1 -1
- package/dist/types/modes/types.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/agent-storage.d.ts +57 -0
- package/dist/types/session/session-context.d.ts +9 -0
- package/dist/types/task/executor.d.ts +26 -13
- package/dist/types/task/index.d.ts +12 -11
- package/dist/types/task/label.d.ts +4 -0
- package/dist/types/task/repair-args.d.ts +8 -8
- package/dist/types/task/types.d.ts +31 -56
- package/dist/types/tools/ask.d.ts +12 -0
- package/dist/types/tools/conflict-detect.d.ts +17 -1
- package/dist/types/tools/job.d.ts +16 -0
- package/package.json +12 -12
- package/scripts/build-binary.ts +0 -1
- package/scripts/compile-binary.ts +4 -3
- package/src/async/job-manager.ts +9 -0
- package/src/cli/bench-cli.ts +7 -26
- package/src/cli/usage-cli.ts +11 -0
- package/src/commands/usage.ts +13 -2
- package/src/commit/agentic/tools/analyze-file.ts +2 -3
- package/src/config/settings-schema.ts +18 -7
- package/src/config/settings.ts +13 -4
- package/src/discovery/helpers.ts +3 -4
- package/src/extensibility/custom-tools/loader.ts +70 -37
- package/src/extensibility/extensions/types.ts +46 -0
- package/src/irc/bus.ts +61 -20
- package/src/modes/components/__tests__/pause-screen.test.ts +143 -0
- package/src/modes/components/advisor-config.ts +32 -22
- package/src/modes/components/ask-dialog.ts +888 -0
- package/src/modes/components/custom-editor.test.ts +58 -1
- package/src/modes/components/custom-editor.ts +42 -11
- package/src/modes/components/index.ts +2 -1
- package/src/modes/components/model-browser.ts +769 -0
- package/src/modes/components/model-hub.ts +2002 -0
- package/src/modes/components/pause-screen.ts +208 -0
- package/src/modes/components/session-selector.ts +299 -42
- package/src/modes/components/tool-execution.ts +2 -0
- package/src/modes/components/usage-row.ts +5 -6
- package/src/modes/controllers/event-controller.ts +8 -2
- package/src/modes/controllers/extension-ui-controller.ts +252 -5
- package/src/modes/controllers/input-controller.ts +140 -6
- package/src/modes/controllers/selector-controller.ts +160 -97
- package/src/modes/controllers/tan-command-controller.ts +1 -1
- package/src/modes/controllers/todo-command-controller.ts +1 -2
- package/src/modes/interactive-mode.ts +8 -0
- package/src/modes/queue-input.ts +132 -0
- package/src/modes/shared.ts +1 -1
- package/src/modes/theme/theme.ts +3 -3
- package/src/modes/types.ts +4 -0
- package/src/modes/utils/ui-helpers.ts +50 -24
- package/src/prompts/agents/scout.md +0 -1
- package/src/prompts/agents/task.md +1 -1
- package/src/prompts/system/subagent-system-prompt.md +1 -5
- package/src/prompts/system/subagent-yield-reminder.md +10 -0
- package/src/prompts/system/task-label.md +23 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/prompts/tools/task-summary.md +3 -0
- package/src/prompts/tools/task.md +17 -18
- package/src/session/agent-session.ts +186 -49
- package/src/session/agent-storage.ts +330 -3
- package/src/session/history-storage.ts +1 -34
- package/src/session/session-context.test.ts +73 -0
- package/src/session/session-context.ts +43 -26
- package/src/slash-commands/builtin-registry.ts +18 -0
- package/src/task/agents.ts +2 -0
- package/src/task/executor.ts +159 -46
- package/src/task/index.ts +377 -239
- package/src/task/label.ts +38 -0
- package/src/task/render.ts +74 -22
- package/src/task/repair-args.ts +20 -31
- package/src/task/spawn-policy.test.ts +4 -4
- package/src/task/types.ts +46 -66
- package/src/tools/ask.ts +233 -40
- package/src/tools/conflict-detect.ts +102 -5
- package/src/tools/index.ts +1 -0
- package/src/tools/irc.ts +20 -11
- package/src/tools/job.ts +158 -18
- package/src/tools/write.ts +70 -6
- package/src/vibe/runtime.ts +1 -1
- package/src/web/search/providers/browser-headers.ts +30 -13
- package/dist/types/modes/components/model-selector.d.ts +0 -37
- package/dist/types/tools/bash-command-fixup.d.ts +0 -3
- package/src/modes/components/model-selector.ts +0 -1291
- package/src/tools/bash-command-fixup.ts +0 -4
package/src/cli/bench-cli.ts
CHANGED
|
@@ -74,7 +74,7 @@ export interface BenchRunSuccess {
|
|
|
74
74
|
ttftMs: number;
|
|
75
75
|
durationMs: number;
|
|
76
76
|
outputTokens: number;
|
|
77
|
-
/**
|
|
77
|
+
/** Output tokens/sec over the total request duration. */
|
|
78
78
|
tokensPerSecond: number;
|
|
79
79
|
}
|
|
80
80
|
|
|
@@ -181,23 +181,6 @@ function hasVisibleFinalContent(message: AssistantMessage): boolean {
|
|
|
181
181
|
});
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
/**
|
|
185
|
-
* Tokens/s over the generation window (duration minus TTFT) so queue/prefill
|
|
186
|
-
* latency does not dilute throughput. Falls back to total duration when the
|
|
187
|
-
* response arrived as a single chunk (TTFT ~ duration).
|
|
188
|
-
*/
|
|
189
|
-
export function computeTokensPerSecond(
|
|
190
|
-
outputTokens: number,
|
|
191
|
-
durationMs: number,
|
|
192
|
-
ttftMs: number,
|
|
193
|
-
deltaChunkCount: number,
|
|
194
|
-
): number {
|
|
195
|
-
const decodeMs = durationMs - ttftMs;
|
|
196
|
-
// Fall back to total duration when the response arrived as a single chunk/non-streaming.
|
|
197
|
-
const windowMs = decodeMs > 0 && deltaChunkCount >= 2 ? decodeMs : durationMs;
|
|
198
|
-
return windowMs > 0 ? (outputTokens * 1000) / windowMs : 0;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
184
|
interface BenchRequestOptions {
|
|
202
185
|
apiKey: ApiKeyResolver;
|
|
203
186
|
sessionId: string;
|
|
@@ -247,17 +230,10 @@ async function runBenchRequest(
|
|
|
247
230
|
headers: model.provider === "openrouter" ? { "X-OpenRouter-Cache": "false" } : undefined,
|
|
248
231
|
});
|
|
249
232
|
let message: AssistantMessage | undefined;
|
|
250
|
-
let deltaChunkCount = 0;
|
|
251
233
|
for await (const event of stream) {
|
|
252
234
|
if (firstTokenAt === undefined && isFirstTokenEvent(event)) {
|
|
253
235
|
firstTokenAt = now();
|
|
254
236
|
}
|
|
255
|
-
if (
|
|
256
|
-
(event.type === "text_delta" || event.type === "thinking_delta" || event.type === "toolcall_delta") &&
|
|
257
|
-
event.delta.length > 0
|
|
258
|
-
) {
|
|
259
|
-
deltaChunkCount++;
|
|
260
|
-
}
|
|
261
237
|
if (event.type === "error") {
|
|
262
238
|
return { ok: false, error: event.error.errorMessage ?? "request failed" };
|
|
263
239
|
}
|
|
@@ -291,7 +267,12 @@ async function runBenchRequest(
|
|
|
291
267
|
ttftMs,
|
|
292
268
|
durationMs,
|
|
293
269
|
outputTokens,
|
|
294
|
-
|
|
270
|
+
// TPS over the TOTAL request duration, deliberately not the post-TTFT
|
|
271
|
+
// decode window: reasoning models can spend seconds generating hidden
|
|
272
|
+
// thinking tokens (counted in usage.output) before the first visible
|
|
273
|
+
// byte, so "duration - TTFT" inflates TPS several-fold on providers
|
|
274
|
+
// that buffer or hide reasoning (e.g. google vs google-vertex).
|
|
275
|
+
tokensPerSecond: durationMs > 0 ? (outputTokens * 1000) / durationMs : 0,
|
|
295
276
|
};
|
|
296
277
|
} catch (error) {
|
|
297
278
|
return { ok: false, error: getErrorMessage(error) };
|
package/src/cli/usage-cli.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { discoverAuthStorage } from "../sdk";
|
|
|
23
23
|
const BAR_WIDTH = 28;
|
|
24
24
|
|
|
25
25
|
export interface UsageCommandArgs {
|
|
26
|
+
action?: string;
|
|
26
27
|
json?: boolean;
|
|
27
28
|
provider?: string;
|
|
28
29
|
redact?: boolean;
|
|
@@ -755,6 +756,16 @@ function redactReportForJson(
|
|
|
755
756
|
export async function runUsageCommand(cmd: UsageCommandArgs): Promise<void> {
|
|
756
757
|
const authStorage = await discoverAuthStorage();
|
|
757
758
|
try {
|
|
759
|
+
if (cmd.action === "invalidate") {
|
|
760
|
+
const provider = cmd.provider?.toLowerCase();
|
|
761
|
+
await authStorage.invalidateUsageCache(provider);
|
|
762
|
+
if (provider) {
|
|
763
|
+
process.stdout.write(`Invalidated cached usage reports for provider "${provider}".\n`);
|
|
764
|
+
} else {
|
|
765
|
+
process.stdout.write("Invalidated cached usage reports for all providers.\n");
|
|
766
|
+
}
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
758
769
|
if (cmd.history) {
|
|
759
770
|
const days = cmd.days !== undefined && Number.isFinite(cmd.days) && cmd.days > 0 ? cmd.days : 7;
|
|
760
771
|
const nowMs = Date.now();
|
package/src/commands/usage.ts
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Show provider usage limits for every authenticated account.
|
|
3
3
|
*/
|
|
4
|
-
import { Command, Flags } from "@oh-my-pi/pi-utils/cli";
|
|
4
|
+
import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
|
|
5
5
|
import { runUsageCommand } from "../cli/usage-cli";
|
|
6
6
|
|
|
7
7
|
export default class Usage extends Command {
|
|
8
8
|
static description = "Show provider usage limits for every authenticated account";
|
|
9
9
|
|
|
10
|
+
static args = {
|
|
11
|
+
action: Args.string({
|
|
12
|
+
description: "Optional subcommand to execute",
|
|
13
|
+
required: false,
|
|
14
|
+
options: ["invalidate"],
|
|
15
|
+
}),
|
|
16
|
+
};
|
|
17
|
+
|
|
10
18
|
static flags = {
|
|
11
19
|
json: Flags.boolean({ char: "j", description: "Output usage reports as JSON", default: false }),
|
|
12
20
|
provider: Flags.string({ char: "p", description: "Only show usage for this provider id (e.g. anthropic)" }),
|
|
@@ -28,11 +36,14 @@ export default class Usage extends Command {
|
|
|
28
36
|
"# Redact account identifiers for screenshots\n omp usage --redact",
|
|
29
37
|
"# Machine-readable output\n omp usage --json",
|
|
30
38
|
"# Usage-limit trend over the last 30 days\n omp usage --history --days 30",
|
|
39
|
+
"# Invalidate cached usage reports for all providers\n omp usage invalidate",
|
|
40
|
+
"# Invalidate cached usage reports for a specific provider\n omp usage invalidate --provider anthropic",
|
|
31
41
|
];
|
|
32
42
|
|
|
33
43
|
async run(): Promise<void> {
|
|
34
|
-
const { flags } = await this.parse(Usage);
|
|
44
|
+
const { args, flags } = await this.parse(Usage);
|
|
35
45
|
await runUsageCommand({
|
|
46
|
+
action: args.action,
|
|
36
47
|
json: flags.json,
|
|
37
48
|
provider: flags.provider,
|
|
38
49
|
redact: flags.redact,
|
|
@@ -83,10 +83,9 @@ export function createAnalyzeFileTool(options: {
|
|
|
83
83
|
related_files: relatedFiles,
|
|
84
84
|
});
|
|
85
85
|
const taskParams: TaskParams = {
|
|
86
|
+
name: `AnalyzeFile${index + 1}`,
|
|
86
87
|
agent: "sonic",
|
|
87
|
-
|
|
88
|
-
description: `Analyze ${file}`,
|
|
89
|
-
assignment,
|
|
88
|
+
task: assignment,
|
|
90
89
|
};
|
|
91
90
|
return taskTool.execute(`${toolCallId}-${index + 1}`, taskParams, signal);
|
|
92
91
|
}),
|
|
@@ -1375,7 +1375,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
1375
1375
|
group: "Retry & Fallback",
|
|
1376
1376
|
label: "Retry Fallback Chains",
|
|
1377
1377
|
description:
|
|
1378
|
-
'JSON object mapping model roles to ordered fallback
|
|
1378
|
+
'JSON object mapping model roles, model selectors ("provider/model-id"), or provider wildcards ("provider/*") to ordered fallback selectors, e.g. {"default":["openai/gpt-4o-mini"],"google-antigravity/*":["google/*","google-vertex/*"]}. Model-oriented keys apply whenever that model/provider is active, regardless of role; a "provider/*" entry keeps the failing model\'s id and swaps the provider.',
|
|
1379
1379
|
},
|
|
1380
1380
|
},
|
|
1381
1381
|
"retry.fallbackRevertPolicy": {
|
|
@@ -3633,6 +3633,17 @@ export const SETTINGS_SCHEMA = {
|
|
|
3633
3633
|
},
|
|
3634
3634
|
},
|
|
3635
3635
|
|
|
3636
|
+
"ask.enabled": {
|
|
3637
|
+
type: "boolean",
|
|
3638
|
+
default: true,
|
|
3639
|
+
ui: {
|
|
3640
|
+
tab: "tools",
|
|
3641
|
+
group: "Available Tools",
|
|
3642
|
+
label: "Ask",
|
|
3643
|
+
description: "Enable the ask tool for interactive user questions",
|
|
3644
|
+
},
|
|
3645
|
+
},
|
|
3646
|
+
|
|
3636
3647
|
"browser.enabled": {
|
|
3637
3648
|
type: "boolean",
|
|
3638
3649
|
default: true,
|
|
@@ -4142,31 +4153,31 @@ export const SETTINGS_SCHEMA = {
|
|
|
4142
4153
|
|
|
4143
4154
|
"task.softRequestBudget": {
|
|
4144
4155
|
type: "number",
|
|
4145
|
-
default:
|
|
4156
|
+
default: 200,
|
|
4146
4157
|
ui: {
|
|
4147
4158
|
tab: "tasks",
|
|
4148
4159
|
group: "Subagents",
|
|
4149
4160
|
label: "Soft Subagent Request Budget",
|
|
4150
4161
|
description:
|
|
4151
|
-
"Soft per-subagent request budget (assistant requests per run). Crossing it
|
|
4162
|
+
"Soft per-subagent request budget (assistant requests per run). Crossing it injects a wrap-up steering notice (see task.softRequestBudgetNotice); at 1.5x the budget the run is force-stopped and the agent must yield its partial findings. 0 disables the guard. Bundled scout/sonic agents use a lower built-in budget.",
|
|
4152
4163
|
options: [
|
|
4153
4164
|
{ value: "0", label: "Disabled" },
|
|
4154
|
-
{ value: "
|
|
4155
|
-
{ value: "90", label: "90 requests", description: "Default" },
|
|
4165
|
+
{ value: "90", label: "90 requests" },
|
|
4156
4166
|
{ value: "150", label: "150 requests" },
|
|
4167
|
+
{ value: "200", label: "200 requests", description: "Default" },
|
|
4157
4168
|
],
|
|
4158
4169
|
},
|
|
4159
4170
|
},
|
|
4160
4171
|
|
|
4161
4172
|
"task.softRequestBudgetNotice": {
|
|
4162
4173
|
type: "boolean",
|
|
4163
|
-
default:
|
|
4174
|
+
default: true,
|
|
4164
4175
|
ui: {
|
|
4165
4176
|
tab: "tasks",
|
|
4166
4177
|
group: "Subagents",
|
|
4167
4178
|
label: "Soft Request Budget Notice",
|
|
4168
4179
|
description:
|
|
4169
|
-
"Inject one steering notice when a subagent crosses its soft request budget
|
|
4180
|
+
"Inject one steering notice when a subagent crosses its soft request budget, asking it to wrap up before the 1.5x forced-yield stop.",
|
|
4170
4181
|
},
|
|
4171
4182
|
},
|
|
4172
4183
|
|
package/src/config/settings.ts
CHANGED
|
@@ -604,9 +604,10 @@ export class Settings {
|
|
|
604
604
|
}
|
|
605
605
|
|
|
606
606
|
/**
|
|
607
|
-
* Set a model role (helper for modelRoles record).
|
|
607
|
+
* Set a model role (helper for modelRoles record). Passing `undefined`
|
|
608
|
+
* clears the role from the persisted record and any runtime override.
|
|
608
609
|
*/
|
|
609
|
-
setModelRole(role: ModelRole | string, modelId: string): void {
|
|
610
|
+
setModelRole(role: ModelRole | string, modelId: string | undefined): void {
|
|
610
611
|
const current = this.#modelRolesFromLayer(this.#global);
|
|
611
612
|
const runtimeOverrides = getByPath(this.#overrides, ["modelRoles"]);
|
|
612
613
|
const updateRuntimeOverride =
|
|
@@ -615,12 +616,20 @@ export class Settings {
|
|
|
615
616
|
!Array.isArray(runtimeOverrides) &&
|
|
616
617
|
Object.hasOwn(runtimeOverrides, role);
|
|
617
618
|
|
|
618
|
-
|
|
619
|
+
if (modelId === undefined) {
|
|
620
|
+
delete current[role];
|
|
621
|
+
} else {
|
|
622
|
+
current[role] = modelId;
|
|
623
|
+
}
|
|
619
624
|
this.set("modelRoles", current);
|
|
620
625
|
|
|
621
626
|
if (updateRuntimeOverride) {
|
|
622
627
|
const nextRuntimeOverride = this.#modelRolesFromLayer(this.#overrides);
|
|
623
|
-
|
|
628
|
+
if (modelId === undefined) {
|
|
629
|
+
delete nextRuntimeOverride[role];
|
|
630
|
+
} else {
|
|
631
|
+
nextRuntimeOverride[role] = modelId;
|
|
632
|
+
}
|
|
624
633
|
this.override("modelRoles", nextRuntimeOverride);
|
|
625
634
|
}
|
|
626
635
|
}
|
package/src/discovery/helpers.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
5
4
|
import { FileType, glob } from "@oh-my-pi/pi-natives";
|
|
6
5
|
import {
|
|
7
6
|
CONFIG_DIR_NAME,
|
|
@@ -17,7 +16,7 @@ import { invalidate as invalidateFsCache, readDirEntries, readFile } from "../ca
|
|
|
17
16
|
import { parseRuleConditionAndScope, type Rule, type RuleFrontmatter } from "../capability/rule";
|
|
18
17
|
import type { Skill, SkillFrontmatter } from "../capability/skill";
|
|
19
18
|
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
|
|
20
|
-
import {
|
|
19
|
+
import { type ConfiguredThinkingLevel, parseConfiguredThinkingLevel } from "../thinking";
|
|
21
20
|
import { normalizeToolNames } from "../tools/builtin-names";
|
|
22
21
|
|
|
23
22
|
import { buildPluginDirRoot } from "./plugin-dir-roots";
|
|
@@ -229,7 +228,7 @@ export interface ParsedAgentFields {
|
|
|
229
228
|
spawns?: string[] | "*";
|
|
230
229
|
model?: string[];
|
|
231
230
|
output?: unknown;
|
|
232
|
-
thinkingLevel?:
|
|
231
|
+
thinkingLevel?: ConfiguredThinkingLevel;
|
|
233
232
|
autoloadSkills?: string[];
|
|
234
233
|
readSummarize?: boolean;
|
|
235
234
|
blocking?: boolean;
|
|
@@ -283,7 +282,7 @@ export function parseAgentFields(frontmatter: Record<string, unknown>): ParsedAg
|
|
|
283
282
|
? frontmatter.thinking
|
|
284
283
|
: undefined;
|
|
285
284
|
|
|
286
|
-
const thinkingLevel =
|
|
285
|
+
const thinkingLevel = parseConfiguredThinkingLevel(rawThinkingLevel);
|
|
287
286
|
const model = parseModelList(frontmatter.model);
|
|
288
287
|
const blocking = parseBoolean(frontmatter.blocking);
|
|
289
288
|
const readSummarize = parseBoolean(frontmatter.readSummarize);
|
|
@@ -21,6 +21,34 @@ import * as typebox from "../typebox";
|
|
|
21
21
|
import { createNoOpUIContext, resolvePath, withExitGuard } from "../utils";
|
|
22
22
|
import type { CustomToolAPI, CustomToolFactory, LoadedCustomTool, ToolLoadError } from "./types";
|
|
23
23
|
|
|
24
|
+
interface LoadToolResult {
|
|
25
|
+
tools: LoadedCustomTool[];
|
|
26
|
+
errors: ToolLoadError[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isLoadableCustomTool(value: unknown): value is LoadedCustomTool["tool"] {
|
|
30
|
+
return (
|
|
31
|
+
typeof value === "object" &&
|
|
32
|
+
value !== null &&
|
|
33
|
+
"name" in value &&
|
|
34
|
+
typeof value.name === "string" &&
|
|
35
|
+
value.name.length > 0 &&
|
|
36
|
+
"description" in value &&
|
|
37
|
+
typeof value.description === "string" &&
|
|
38
|
+
"parameters" in value &&
|
|
39
|
+
"execute" in value &&
|
|
40
|
+
typeof value.execute === "function"
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function invalidToolError(path: string, index: number, source: ToolLoadError["source"]): ToolLoadError {
|
|
45
|
+
return {
|
|
46
|
+
path,
|
|
47
|
+
error: `Tool factory returned invalid tool at index ${index}: expected object with string name, string description, parameters, and execute function`,
|
|
48
|
+
source,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
24
52
|
/**
|
|
25
53
|
* Load a single tool module using native Bun import.
|
|
26
54
|
*/
|
|
@@ -29,18 +57,20 @@ async function loadTool(
|
|
|
29
57
|
cwd: string,
|
|
30
58
|
sharedApi: CustomToolAPI,
|
|
31
59
|
source?: { provider: string; providerName: string; level: "user" | "project" },
|
|
32
|
-
): Promise<
|
|
60
|
+
): Promise<LoadToolResult> {
|
|
33
61
|
const resolvedPath = resolvePath(toolPath, cwd);
|
|
34
62
|
|
|
35
63
|
// Skip declarative tool files (.md, .json) - these are metadata only, not executable modules
|
|
36
64
|
if (resolvedPath.endsWith(".md") || resolvedPath.endsWith(".json")) {
|
|
37
65
|
return {
|
|
38
|
-
tools:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
66
|
+
tools: [],
|
|
67
|
+
errors: [
|
|
68
|
+
{
|
|
69
|
+
path: toolPath,
|
|
70
|
+
error: "Declarative tool files (.md, .json) cannot be loaded as executable modules",
|
|
71
|
+
source,
|
|
72
|
+
},
|
|
73
|
+
],
|
|
44
74
|
};
|
|
45
75
|
}
|
|
46
76
|
|
|
@@ -49,23 +79,32 @@ async function loadTool(
|
|
|
49
79
|
const factory = (module.default ?? module) as CustomToolFactory;
|
|
50
80
|
|
|
51
81
|
if (typeof factory !== "function") {
|
|
52
|
-
return { tools:
|
|
82
|
+
return { tools: [], errors: [{ path: toolPath, error: "Tool must export a default function", source }] };
|
|
53
83
|
}
|
|
54
84
|
|
|
55
|
-
const toolResult = await withExitGuard(async () => factory(sharedApi));
|
|
85
|
+
const toolResult: unknown = await withExitGuard(async () => factory(sharedApi));
|
|
56
86
|
const toolsArray = Array.isArray(toolResult) ? toolResult : [toolResult];
|
|
57
87
|
|
|
58
|
-
const loadedTools: LoadedCustomTool[] =
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
tool
|
|
62
|
-
|
|
63
|
-
|
|
88
|
+
const loadedTools: LoadedCustomTool[] = [];
|
|
89
|
+
const errors: ToolLoadError[] = [];
|
|
90
|
+
for (const [index, tool] of toolsArray.entries()) {
|
|
91
|
+
if (!isLoadableCustomTool(tool)) {
|
|
92
|
+
errors.push(invalidToolError(toolPath, index, source));
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
loadedTools.push({
|
|
97
|
+
path: toolPath,
|
|
98
|
+
resolvedPath,
|
|
99
|
+
tool,
|
|
100
|
+
source,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
64
103
|
|
|
65
|
-
return { tools: loadedTools,
|
|
104
|
+
return { tools: loadedTools, errors };
|
|
66
105
|
} catch (err) {
|
|
67
106
|
const message = err instanceof Error ? err.message : String(err);
|
|
68
|
-
return { tools:
|
|
107
|
+
return { tools: [], errors: [{ path: toolPath, error: `Failed to load tool: ${message}`, source }] };
|
|
69
108
|
}
|
|
70
109
|
}
|
|
71
110
|
|
|
@@ -129,28 +168,22 @@ export class CustomToolLoader {
|
|
|
129
168
|
|
|
130
169
|
async load(pathsWithSources: ToolPathWithSource[]): Promise<void> {
|
|
131
170
|
for (const { path: toolPath, source } of pathsWithSources) {
|
|
132
|
-
const { tools: loadedTools,
|
|
171
|
+
const { tools: loadedTools, errors } = await loadTool(toolPath, this.#sharedApi.cwd, this.#sharedApi, source);
|
|
172
|
+
this.errors.push(...errors);
|
|
133
173
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
this.errors.push({
|
|
144
|
-
path: toolPath,
|
|
145
|
-
error: `Tool name "${loadedTool.tool.name}" conflicts with existing tool`,
|
|
146
|
-
source,
|
|
147
|
-
});
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
this.#seenNames.add(loadedTool.tool.name);
|
|
152
|
-
this.tools.push(loadedTool);
|
|
174
|
+
for (const loadedTool of loadedTools) {
|
|
175
|
+
// Check for name conflicts
|
|
176
|
+
if (this.#seenNames.has(loadedTool.tool.name)) {
|
|
177
|
+
this.errors.push({
|
|
178
|
+
path: toolPath,
|
|
179
|
+
error: `Tool name "${loadedTool.tool.name}" conflicts with existing tool`,
|
|
180
|
+
source,
|
|
181
|
+
});
|
|
182
|
+
continue;
|
|
153
183
|
}
|
|
184
|
+
|
|
185
|
+
this.#seenNames.add(loadedTool.tool.name);
|
|
186
|
+
this.tools.push(loadedTool);
|
|
154
187
|
}
|
|
155
188
|
}
|
|
156
189
|
}
|
|
@@ -113,6 +113,46 @@ export interface ExtensionUISelectOption {
|
|
|
113
113
|
|
|
114
114
|
export type ExtensionUISelectItem = string | ExtensionUISelectOption;
|
|
115
115
|
|
|
116
|
+
export interface ExtensionAskDialogOption {
|
|
117
|
+
label: string;
|
|
118
|
+
description?: string;
|
|
119
|
+
preview?: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface ExtensionAskDialogQuestion {
|
|
123
|
+
id: string;
|
|
124
|
+
question: string;
|
|
125
|
+
header?: string;
|
|
126
|
+
options: ExtensionAskDialogOption[];
|
|
127
|
+
multi?: boolean;
|
|
128
|
+
recommended?: number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface ExtensionAskDialogResultItem {
|
|
132
|
+
id: string;
|
|
133
|
+
question: string;
|
|
134
|
+
options: string[];
|
|
135
|
+
multi: boolean;
|
|
136
|
+
selectedOptions: string[];
|
|
137
|
+
customInput?: string;
|
|
138
|
+
note?: string;
|
|
139
|
+
timedOut?: boolean;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface ExtensionAskDialogSubmitResult {
|
|
143
|
+
kind: "submit";
|
|
144
|
+
results: ExtensionAskDialogResultItem[];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Chat-redirect result: the user chose "Chat about this" instead of
|
|
148
|
+
* answering. Distinct from `undefined` (cancel) so AskTool can hand off to
|
|
149
|
+
* the chat loop rather than aborting. */
|
|
150
|
+
export interface ExtensionAskDialogChatResult {
|
|
151
|
+
kind: "chat";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export type ExtensionAskDialogResult = ExtensionAskDialogSubmitResult | ExtensionAskDialogChatResult;
|
|
155
|
+
|
|
116
156
|
export function getExtensionUISelectOptionLabel(option: ExtensionUISelectItem): string {
|
|
117
157
|
return typeof option === "string" ? option : option.label;
|
|
118
158
|
}
|
|
@@ -195,6 +235,12 @@ export interface ExtensionUIContext {
|
|
|
195
235
|
/** Show a text input dialog. */
|
|
196
236
|
input(title: string, placeholder?: string, dialogOptions?: ExtensionUIDialogOptions): Promise<string | undefined>;
|
|
197
237
|
|
|
238
|
+
/** Show the rich ask dialog when the interactive TUI surface is available. */
|
|
239
|
+
askDialog?(
|
|
240
|
+
questions: ExtensionAskDialogQuestion[],
|
|
241
|
+
dialogOptions?: ExtensionUIDialogOptions,
|
|
242
|
+
): Promise<ExtensionAskDialogResult | undefined>;
|
|
243
|
+
|
|
198
244
|
/** Show a notification to the user. */
|
|
199
245
|
notify(message: string, type?: "info" | "warning" | "error"): void;
|
|
200
246
|
|
package/src/irc/bus.ts
CHANGED
|
@@ -104,8 +104,19 @@ export class IrcBus {
|
|
|
104
104
|
): Promise<IrcDeliveryReceipt> {
|
|
105
105
|
const message: IrcMessage = { ...msg, id: Snowflake.next(), ts: Date.now() };
|
|
106
106
|
const ref = this.#registry.get(message.to);
|
|
107
|
-
if (!ref
|
|
108
|
-
return {
|
|
107
|
+
if (!ref) {
|
|
108
|
+
return {
|
|
109
|
+
to: message.to,
|
|
110
|
+
outcome: "failed",
|
|
111
|
+
error: `Unknown agent "${message.to}" — check \`irc list\` for live peers.`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
if (ref.status === "aborted") {
|
|
115
|
+
return {
|
|
116
|
+
to: message.to,
|
|
117
|
+
outcome: "failed",
|
|
118
|
+
error: `Agent "${message.to}" was hard-aborted and cannot be messaged or revived. Its transcript remains readable at history://${message.to}.`,
|
|
119
|
+
};
|
|
109
120
|
}
|
|
110
121
|
// Advisor refs are observability-only transcripts, never messageable peers.
|
|
111
122
|
if (ref.kind === "advisor") {
|
|
@@ -175,7 +186,7 @@ export class IrcBus {
|
|
|
175
186
|
filter: { from?: string },
|
|
176
187
|
timeoutMs: number,
|
|
177
188
|
signal?: AbortSignal,
|
|
178
|
-
options?: { drainPending?: boolean },
|
|
189
|
+
options?: { drainPending?: boolean; liveness?: { registry: AgentRegistry; senderId: string } },
|
|
179
190
|
): Promise<IrcMessage | null> {
|
|
180
191
|
if (signal?.aborted) {
|
|
181
192
|
throw signal.reason instanceof Error ? signal.reason : new Error("IRC wait aborted");
|
|
@@ -190,35 +201,49 @@ export class IrcBus {
|
|
|
190
201
|
const { promise, resolve, reject } = Promise.withResolvers<IrcMessage | null>();
|
|
191
202
|
let timer: NodeJS.Timeout | undefined;
|
|
192
203
|
let onAbort: (() => void) | undefined;
|
|
204
|
+
let unsubscribeLiveness: (() => void) | undefined;
|
|
193
205
|
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
206
|
+
const liveness = options?.liveness;
|
|
207
|
+
const livenessReason = filter.from
|
|
208
|
+
? `IRC wait aborted: agent "${filter.from}" is not running`
|
|
209
|
+
: "IRC wait aborted: no running peers remain";
|
|
210
|
+
|
|
211
|
+
const settle = (
|
|
212
|
+
outcome: { kind: "message"; msg: IrcMessage } | { kind: "timeout" } | { kind: "abort"; error: Error },
|
|
213
|
+
): void => {
|
|
214
|
+
cleanup();
|
|
215
|
+
if (outcome.kind === "message") {
|
|
216
|
+
resolve(outcome.msg);
|
|
217
|
+
} else if (outcome.kind === "timeout") {
|
|
218
|
+
resolve(null);
|
|
219
|
+
} else {
|
|
220
|
+
reject(outcome.error);
|
|
221
|
+
}
|
|
203
222
|
};
|
|
223
|
+
|
|
204
224
|
const cleanup = (): void => {
|
|
205
225
|
this.#removeWaiter(agentId, waiter);
|
|
206
226
|
clearTimeout(timer);
|
|
207
227
|
if (signal && onAbort) signal.removeEventListener("abort", onAbort);
|
|
228
|
+
unsubscribeLiveness?.();
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const waiter: IrcWaiter = {
|
|
232
|
+
from: filter.from,
|
|
233
|
+
resolve: msg => settle({ kind: "message", msg }),
|
|
234
|
+
cancel: () => cleanup(),
|
|
208
235
|
};
|
|
209
236
|
|
|
210
237
|
if (signal) {
|
|
211
|
-
onAbort = () =>
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
238
|
+
onAbort = () =>
|
|
239
|
+
settle({
|
|
240
|
+
kind: "abort",
|
|
241
|
+
error: signal.reason instanceof Error ? signal.reason : new Error("IRC wait aborted"),
|
|
242
|
+
});
|
|
215
243
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
216
244
|
}
|
|
217
245
|
if (timeoutMs > 0) {
|
|
218
|
-
timer = setTimeout(() => {
|
|
219
|
-
cleanup();
|
|
220
|
-
resolve(null);
|
|
221
|
-
}, timeoutMs);
|
|
246
|
+
timer = setTimeout(() => settle({ kind: "timeout" }), timeoutMs);
|
|
222
247
|
timer.unref?.();
|
|
223
248
|
}
|
|
224
249
|
|
|
@@ -228,6 +253,22 @@ export class IrcBus {
|
|
|
228
253
|
this.#waiters.set(agentId, waiters);
|
|
229
254
|
}
|
|
230
255
|
waiters.push(waiter);
|
|
256
|
+
|
|
257
|
+
if (liveness) {
|
|
258
|
+
const { registry, senderId } = liveness;
|
|
259
|
+
const hasRunningSender = (from?: string): boolean =>
|
|
260
|
+
registry.listVisibleTo(senderId).some(ref => ref.status === "running" && (!from || ref.id === from));
|
|
261
|
+
const check = filter.from ? () => hasRunningSender(filter.from) : () => hasRunningSender();
|
|
262
|
+
unsubscribeLiveness = registry.onChange(() => {
|
|
263
|
+
if (!check()) {
|
|
264
|
+
settle({ kind: "abort", error: new Error(livenessReason) });
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
if (!check()) {
|
|
268
|
+
settle({ kind: "abort", error: new Error(livenessReason) });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
231
272
|
return promise;
|
|
232
273
|
}
|
|
233
274
|
|