@gajae-code/coding-agent 0.6.4 → 0.7.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 +51 -0
- package/dist/types/async/job-manager.d.ts +3 -1
- package/dist/types/cli/daemon-cli.d.ts +25 -0
- package/dist/types/cli/migrate-cli.d.ts +20 -0
- package/dist/types/cli/notify-cli.d.ts +23 -0
- package/dist/types/cli/setup-cli.d.ts +20 -1
- package/dist/types/commands/daemon.d.ts +41 -0
- package/dist/types/commands/migrate.d.ts +33 -0
- package/dist/types/commands/notify.d.ts +41 -0
- package/dist/types/config/keybindings.d.ts +4 -0
- package/dist/types/config/model-profile-activation.d.ts +12 -0
- package/dist/types/config/model-profiles.d.ts +2 -1
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +38 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/daemon/builtin.d.ts +20 -0
- package/dist/types/daemon/control-types.d.ts +57 -0
- package/dist/types/daemon/runtime.d.ts +25 -0
- package/dist/types/extensibility/extensions/types.d.ts +8 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -2
- package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
- package/dist/types/gjc-runtime/session-layout.d.ts +59 -0
- package/dist/types/gjc-runtime/session-resolution.d.ts +47 -0
- package/dist/types/gjc-runtime/state-graph.d.ts +1 -1
- package/dist/types/gjc-runtime/state-runtime.d.ts +5 -4
- package/dist/types/gjc-runtime/state-schema.d.ts +2 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +38 -7
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +15 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +21 -4
- package/dist/types/gjc-runtime/workflow-command-ref.d.ts +1 -1
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +1 -1
- package/dist/types/harness-control-plane/storage.d.ts +2 -1
- package/dist/types/hooks/skill-state.d.ts +12 -4
- package/dist/types/migrate/action-planner.d.ts +11 -0
- package/dist/types/migrate/adapters/claude-code.d.ts +2 -0
- package/dist/types/migrate/adapters/codex.d.ts +5 -0
- package/dist/types/migrate/adapters/index.d.ts +45 -0
- package/dist/types/migrate/adapters/opencode.d.ts +2 -0
- package/dist/types/migrate/executor.d.ts +2 -0
- package/dist/types/migrate/mcp-mapper.d.ts +20 -0
- package/dist/types/migrate/report.d.ts +18 -0
- package/dist/types/migrate/skill-normalizer.d.ts +27 -0
- package/dist/types/migrate/types.d.ts +126 -0
- package/dist/types/modes/components/custom-editor.d.ts +1 -1
- package/dist/types/modes/components/oauth-selector.d.ts +2 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -2
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
- package/dist/types/modes/types.d.ts +7 -1
- package/dist/types/notifications/config-commands.d.ts +26 -0
- package/dist/types/notifications/config.d.ts +61 -0
- package/dist/types/notifications/helpers.d.ts +55 -0
- package/dist/types/notifications/html-format.d.ts +62 -0
- package/dist/types/notifications/index.d.ts +28 -0
- package/dist/types/notifications/rate-limit-pool.d.ts +93 -0
- package/dist/types/notifications/telegram-cli.d.ts +19 -0
- package/dist/types/notifications/telegram-daemon-cli.d.ts +11 -0
- package/dist/types/notifications/telegram-daemon-control.d.ts +56 -0
- package/dist/types/notifications/telegram-daemon.d.ts +276 -0
- package/dist/types/notifications/telegram-reference.d.ts +111 -0
- package/dist/types/notifications/threaded-inbound.d.ts +58 -0
- package/dist/types/notifications/threaded-render.d.ts +66 -0
- package/dist/types/notifications/topic-registry.d.ts +67 -0
- package/dist/types/research-plan/index.d.ts +1 -0
- package/dist/types/research-plan/ledger.d.ts +33 -0
- package/dist/types/rlm/artifacts.d.ts +1 -1
- package/dist/types/rlm/index.d.ts +12 -0
- package/dist/types/runtime-mcp/config-writer.d.ts +26 -0
- package/dist/types/session/agent-session.d.ts +39 -2
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/setup/credential-auto-import.d.ts +63 -0
- package/dist/types/setup/credential-import.d.ts +3 -0
- package/dist/types/setup/host-plugin-setup.d.ts +39 -0
- package/dist/types/skill-state/active-state.d.ts +6 -11
- package/dist/types/skill-state/canonical-skills.d.ts +3 -0
- package/dist/types/skill-state/workflow-hud.d.ts +2 -0
- package/dist/types/task/spawn-gate.d.ts +1 -10
- package/dist/types/tools/ask-answer-registry.d.ts +13 -0
- package/dist/types/tools/index.d.ts +18 -0
- package/dist/types/tools/subagent.d.ts +3 -0
- package/package.json +7 -7
- package/scripts/build-binary.ts +3 -0
- package/src/async/job-manager.ts +5 -1
- package/src/cli/daemon-cli.ts +122 -0
- package/src/cli/migrate-cli.ts +106 -0
- package/src/cli/notify-cli.ts +274 -0
- package/src/cli/setup-cli.ts +173 -84
- package/src/cli.ts +3 -0
- package/src/commands/daemon.ts +47 -0
- package/src/commands/deep-interview.ts +2 -2
- package/src/commands/migrate.ts +46 -0
- package/src/commands/notify.ts +61 -0
- package/src/commands/setup.ts +11 -1
- package/src/commands/state.ts +2 -1
- package/src/commands/team.ts +7 -3
- package/src/config/model-profile-activation.ts +74 -5
- package/src/config/model-profiles.ts +7 -4
- package/src/config/model-registry.ts +6 -3
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +29 -0
- package/src/coordinator/contract.ts +3 -0
- package/src/coordinator-mcp/policy.ts +10 -2
- package/src/coordinator-mcp/server.ts +270 -1
- package/src/daemon/builtin.ts +46 -0
- package/src/daemon/control-types.ts +65 -0
- package/src/daemon/runtime.ts +51 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +0 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +28 -24
- package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -4
- package/src/defaults/gjc/skills/team/SKILL.md +51 -47
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +33 -13
- package/src/extensibility/custom-commands/loader.ts +0 -7
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +8 -0
- package/src/extensibility/gjc-plugins/injection.ts +23 -4
- package/src/extensibility/gjc-plugins/state.ts +16 -1
- package/src/gjc-runtime/deep-interview-recorder.ts +51 -18
- package/src/gjc-runtime/deep-interview-runtime.ts +49 -23
- package/src/gjc-runtime/goal-mode-request.ts +26 -11
- package/src/gjc-runtime/launch-tmux.ts +6 -1
- package/src/gjc-runtime/ralplan-runtime.ts +79 -50
- package/src/gjc-runtime/session-layout.ts +180 -0
- package/src/gjc-runtime/session-resolution.ts +217 -0
- package/src/gjc-runtime/state-graph.ts +1 -2
- package/src/gjc-runtime/state-migrations.ts +1 -0
- package/src/gjc-runtime/state-runtime.ts +247 -124
- package/src/gjc-runtime/state-schema.ts +2 -0
- package/src/gjc-runtime/state-writer.ts +289 -41
- package/src/gjc-runtime/team-runtime.ts +43 -19
- package/src/gjc-runtime/tmux-sessions.ts +7 -1
- package/src/gjc-runtime/ultragoal-guard.ts +102 -4
- package/src/gjc-runtime/ultragoal-runtime.ts +226 -60
- package/src/gjc-runtime/workflow-command-ref.ts +1 -2
- package/src/gjc-runtime/workflow-manifest.generated.json +27 -2
- package/src/gjc-runtime/workflow-manifest.ts +12 -3
- package/src/goals/tools/goal-tool.ts +11 -2
- package/src/harness-control-plane/storage.ts +14 -4
- package/src/hooks/native-skill-hook.ts +38 -12
- package/src/hooks/skill-state.ts +178 -83
- package/src/internal-urls/docs-index.generated.ts +9 -6
- package/src/main.ts +30 -0
- package/src/migrate/action-planner.ts +318 -0
- package/src/migrate/adapters/claude-code.ts +39 -0
- package/src/migrate/adapters/codex.ts +70 -0
- package/src/migrate/adapters/index.ts +277 -0
- package/src/migrate/adapters/opencode.ts +52 -0
- package/src/migrate/executor.ts +81 -0
- package/src/migrate/mcp-mapper.ts +152 -0
- package/src/migrate/report.ts +104 -0
- package/src/migrate/skill-normalizer.ts +80 -0
- package/src/migrate/types.ts +163 -0
- package/src/modes/acp/acp-event-mapper.ts +1 -0
- package/src/modes/bridge/bridge-mode.ts +2 -2
- package/src/modes/components/custom-editor.ts +30 -20
- package/src/modes/components/hook-editor.ts +7 -2
- package/src/modes/components/oauth-selector.ts +19 -0
- package/src/modes/controllers/event-controller.ts +20 -0
- package/src/modes/controllers/selector-controller.ts +80 -17
- package/src/modes/interactive-mode.ts +6 -2
- package/src/modes/rpc/rpc-mode.ts +2 -2
- package/src/modes/runtime-init.ts +1 -0
- package/src/modes/shared/agent-wire/event-contract.ts +1 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +1 -0
- package/src/modes/shared/agent-wire/event-observation.ts +16 -0
- package/src/modes/shared/agent-wire/unattended-audit.ts +3 -2
- package/src/modes/shared/agent-wire/unattended-session.ts +22 -0
- package/src/modes/types.ts +7 -1
- package/src/modes/utils/ui-helpers.ts +23 -0
- package/src/notifications/config-commands.ts +50 -0
- package/src/notifications/config.ts +107 -0
- package/src/notifications/helpers.ts +135 -0
- package/src/notifications/html-format.ts +389 -0
- package/src/notifications/index.ts +663 -0
- package/src/notifications/rate-limit-pool.ts +179 -0
- package/src/notifications/telegram-cli.ts +194 -0
- package/src/notifications/telegram-daemon-cli.ts +74 -0
- package/src/notifications/telegram-daemon-control.ts +370 -0
- package/src/notifications/telegram-daemon.ts +1370 -0
- package/src/notifications/telegram-reference.ts +335 -0
- package/src/notifications/threaded-inbound.ts +80 -0
- package/src/notifications/threaded-render.ts +155 -0
- package/src/notifications/topic-registry.ts +133 -0
- package/src/prompts/agents/init.md +1 -1
- package/src/prompts/system/plan-mode-active.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/task.md +1 -2
- package/src/research-plan/index.ts +1 -0
- package/src/research-plan/ledger.ts +177 -0
- package/src/rlm/artifacts.ts +12 -3
- package/src/rlm/index.ts +26 -0
- package/src/runtime-mcp/config-writer.ts +46 -0
- package/src/sdk.ts +16 -0
- package/src/session/agent-session.ts +128 -24
- package/src/session/auth-storage.ts +3 -0
- package/src/session/session-dump-format.ts +43 -2
- package/src/session/session-manager.ts +39 -5
- package/src/setup/credential-auto-import.ts +258 -0
- package/src/setup/credential-import.ts +17 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +10 -0
- package/src/setup/hermes-setup.ts +1 -1
- package/src/setup/host-plugin-setup.ts +142 -0
- package/src/skill-state/active-state.ts +72 -108
- package/src/skill-state/canonical-skills.ts +4 -0
- package/src/skill-state/deep-interview-mutation-guard.ts +28 -109
- package/src/skill-state/workflow-hud.ts +4 -2
- package/src/skill-state/workflow-state-contract.ts +3 -3
- package/src/slash-commands/builtin-registry.ts +4 -1
- package/src/task/agents.ts +1 -22
- package/src/task/executor.ts +5 -1
- package/src/task/index.ts +1 -41
- package/src/task/spawn-gate.ts +1 -38
- package/src/task/types.ts +1 -1
- package/src/tools/ask-answer-registry.ts +25 -0
- package/src/tools/ask.ts +108 -16
- package/src/tools/computer.ts +58 -4
- package/src/tools/image-gen.ts +5 -8
- package/src/tools/index.ts +19 -0
- package/src/tools/inspect-image.ts +16 -11
- package/src/tools/subagent-render.ts +7 -0
- package/src/tools/subagent.ts +38 -7
- package/dist/types/extensibility/custom-commands/bundled/review/index.d.ts +0 -10
- package/src/extensibility/custom-commands/bundled/review/index.ts +0 -456
- package/src/prompts/agents/explore.md +0 -58
- package/src/prompts/agents/plan.md +0 -49
- package/src/prompts/agents/reviewer.md +0 -141
- package/src/prompts/agents/task.md +0 -16
- package/src/prompts/review-request.md +0 -70
package/src/task/agents.ts
CHANGED
|
@@ -3,20 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Agents are embedded at build time via Bun's import with { type: "text" }.
|
|
5
5
|
*/
|
|
6
|
-
import { Effort } from "@gajae-code/ai";
|
|
7
6
|
import { parseFrontmatter, prompt } from "@gajae-code/utils";
|
|
8
7
|
import { parseAgentFields } from "../discovery/helpers";
|
|
8
|
+
// Embed agent markdown files at build time
|
|
9
9
|
import architectMd from "../prompts/agents/architect.md" with { type: "text" };
|
|
10
10
|
import criticMd from "../prompts/agents/critic.md" with { type: "text" };
|
|
11
11
|
import executorMd from "../prompts/agents/executor.md" with { type: "text" };
|
|
12
|
-
import exploreMd from "../prompts/agents/explore.md" with { type: "text" };
|
|
13
|
-
// Embed agent markdown files at build time
|
|
14
12
|
import agentFrontmatterTemplate from "../prompts/agents/frontmatter.md" with { type: "text" };
|
|
15
|
-
|
|
16
|
-
import planMd from "../prompts/agents/plan.md" with { type: "text" };
|
|
17
13
|
import plannerMd from "../prompts/agents/planner.md" with { type: "text" };
|
|
18
|
-
import reviewerMd from "../prompts/agents/reviewer.md" with { type: "text" };
|
|
19
|
-
import taskMd from "../prompts/agents/task.md" with { type: "text" };
|
|
20
14
|
|
|
21
15
|
import type { AgentDefinition, AgentSource } from "./types";
|
|
22
16
|
|
|
@@ -50,21 +44,6 @@ const EMBEDDED_AGENT_DEFS: EmbeddedAgentDef[] = [
|
|
|
50
44
|
{ fileName: "architect.md", template: architectMd },
|
|
51
45
|
{ fileName: "planner.md", template: plannerMd },
|
|
52
46
|
{ fileName: "critic.md", template: criticMd },
|
|
53
|
-
{ fileName: "explore.md", template: exploreMd },
|
|
54
|
-
{ fileName: "plan.md", template: planMd },
|
|
55
|
-
{ fileName: "reviewer.md", template: reviewerMd },
|
|
56
|
-
{
|
|
57
|
-
fileName: "task.md",
|
|
58
|
-
frontmatter: {
|
|
59
|
-
name: "task",
|
|
60
|
-
description: "General-purpose subagent with full capabilities for delegated multi-step tasks",
|
|
61
|
-
spawns: "*",
|
|
62
|
-
model: "pi/default",
|
|
63
|
-
thinkingLevel: Effort.Medium,
|
|
64
|
-
hide: true,
|
|
65
|
-
},
|
|
66
|
-
template: taskMd,
|
|
67
|
-
},
|
|
68
47
|
];
|
|
69
48
|
|
|
70
49
|
// Computed lazily on first loadBundledAgents() call to avoid eager prompt.render at module load.
|
package/src/task/executor.ts
CHANGED
|
@@ -1298,11 +1298,15 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1298
1298
|
requestPause: () => {
|
|
1299
1299
|
pauseRequested = true;
|
|
1300
1300
|
},
|
|
1301
|
-
injectMessage: async (content, deliverAs) => {
|
|
1301
|
+
injectMessage: async (content, deliverAs, opts) => {
|
|
1302
1302
|
if (deliverAs === "nextTurn") {
|
|
1303
1303
|
await session.prompt(content, { attribution: "agent" });
|
|
1304
1304
|
return;
|
|
1305
1305
|
}
|
|
1306
|
+
if (deliverAs === "steer") {
|
|
1307
|
+
const from = opts?.fromAgentId ?? manager.getSubagentRecord(liveSubagentId)?.ownerId ?? "?";
|
|
1308
|
+
session.emitSubagentSteerObservation({ from, to: liveSubagentId, body: content });
|
|
1309
|
+
}
|
|
1306
1310
|
await session.sendUserMessage(content, { deliverAs });
|
|
1307
1311
|
},
|
|
1308
1312
|
});
|
package/src/task/index.ts
CHANGED
|
@@ -55,7 +55,7 @@ import { assertNoRawTaskFields, buildTaskReceipt, buildTaskRoiSummary } from "./
|
|
|
55
55
|
import { renderResult, renderCall as renderTaskCall } from "./render";
|
|
56
56
|
import { reconcileSpawnRoi } from "./roi-reconciliation";
|
|
57
57
|
import { getTaskSimpleModeCapabilities, type TaskSimpleMode } from "./simple-mode";
|
|
58
|
-
import { DEFAULT_SPAWN_THRESHOLD,
|
|
58
|
+
import { DEFAULT_SPAWN_THRESHOLD, evaluateSpawnGate } from "./spawn-gate";
|
|
59
59
|
import {
|
|
60
60
|
applyNestedPatches,
|
|
61
61
|
captureBaseline,
|
|
@@ -359,7 +359,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
359
359
|
readonly renderResult = renderResult;
|
|
360
360
|
readonly #discoveredAgents: AgentDefinition[];
|
|
361
361
|
readonly #blockedAgent: string | undefined;
|
|
362
|
-
readonly #spawningAgentType: string | undefined;
|
|
363
362
|
|
|
364
363
|
get parameters(): TaskToolSchemaInstance {
|
|
365
364
|
const isolationEnabled = this.session.settings.get("task.isolation.mode") !== "none";
|
|
@@ -391,7 +390,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
391
390
|
discoveredAgents: AgentDefinition[],
|
|
392
391
|
) {
|
|
393
392
|
this.#blockedAgent = $env.PI_BLOCKED_AGENT;
|
|
394
|
-
this.#spawningAgentType = session.currentAgentType;
|
|
395
393
|
this.#discoveredAgents = discoveredAgents;
|
|
396
394
|
}
|
|
397
395
|
|
|
@@ -478,23 +476,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
478
476
|
};
|
|
479
477
|
}
|
|
480
478
|
|
|
481
|
-
const reviewerExploreDecision = evaluateReviewerExploreGate({
|
|
482
|
-
spawningAgentType: this.#spawningAgentType,
|
|
483
|
-
targetAgent: params.agent,
|
|
484
|
-
plan: params.spawnPlan,
|
|
485
|
-
});
|
|
486
|
-
if (reviewerExploreDecision.outcome === "rejected") {
|
|
487
|
-
return {
|
|
488
|
-
content: [
|
|
489
|
-
{
|
|
490
|
-
type: "text",
|
|
491
|
-
text: `Task spawn gate rejected reviewer->explore: ${reviewerExploreDecision.reason}. Provide spawnPlan fields: ${reviewerExploreDecision.missingFields.join(", ")}.`,
|
|
492
|
-
},
|
|
493
|
-
],
|
|
494
|
-
details: { projectAgentsDir: null, results: [], totalDurationMs: 0 },
|
|
495
|
-
};
|
|
496
|
-
}
|
|
497
|
-
|
|
498
479
|
const manager = AsyncJobManager.instance();
|
|
499
480
|
if (!manager) {
|
|
500
481
|
return {
|
|
@@ -1083,27 +1064,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1083
1064
|
};
|
|
1084
1065
|
}
|
|
1085
1066
|
|
|
1086
|
-
const reviewerExploreDecision = evaluateReviewerExploreGate({
|
|
1087
|
-
spawningAgentType: this.#spawningAgentType,
|
|
1088
|
-
targetAgent: agentName,
|
|
1089
|
-
plan: params.spawnPlan,
|
|
1090
|
-
});
|
|
1091
|
-
if (reviewerExploreDecision.outcome === "rejected") {
|
|
1092
|
-
return {
|
|
1093
|
-
content: [
|
|
1094
|
-
{
|
|
1095
|
-
type: "text",
|
|
1096
|
-
text: `Task spawn gate rejected reviewer->explore: ${reviewerExploreDecision.reason}. Provide spawnPlan fields: ${reviewerExploreDecision.missingFields.join(", ")}.`,
|
|
1097
|
-
},
|
|
1098
|
-
],
|
|
1099
|
-
details: {
|
|
1100
|
-
projectAgentsDir,
|
|
1101
|
-
results: [],
|
|
1102
|
-
totalDurationMs: Date.now() - startTime,
|
|
1103
|
-
},
|
|
1104
|
-
};
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
1067
|
let repoRoot: string | null = null;
|
|
1108
1068
|
let baseline: WorktreeBaseline | null = null;
|
|
1109
1069
|
if (isIsolated) {
|
package/src/task/spawn-gate.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** The hard, locked batch threshold enforced by the runtime gate. */
|
|
2
2
|
export const DEFAULT_SPAWN_THRESHOLD = 4;
|
|
3
3
|
|
|
4
|
-
/** The justification a large batch
|
|
4
|
+
/** The justification a large batch must supply to pass the hard gate. */
|
|
5
5
|
export interface SpawnPlanReceipt {
|
|
6
6
|
whyParallel: string;
|
|
7
7
|
whyNotLocal: string;
|
|
@@ -17,15 +17,6 @@ export interface SpawnGateRequest {
|
|
|
17
17
|
plan?: SpawnPlanReceipt;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export interface ReviewerExploreGateRequest {
|
|
21
|
-
/** Agent type/name doing the spawning, when known. */
|
|
22
|
-
spawningAgentType?: string | null;
|
|
23
|
-
/** Target agent type/name requested by the task call. */
|
|
24
|
-
targetAgent: string;
|
|
25
|
-
/** The spawn-plan receipt, when provided. */
|
|
26
|
-
plan?: SpawnPlanReceipt;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
20
|
export type SpawnGateOutcome = "allowed" | "rejected";
|
|
30
21
|
|
|
31
22
|
export interface SpawnGateDecision {
|
|
@@ -102,31 +93,3 @@ export function decide(childCount: number, threshold: number, plan: SpawnPlanRec
|
|
|
102
93
|
export function evaluateSpawnGate(request: SpawnGateRequest): SpawnGateDecision {
|
|
103
94
|
return decide(request.childCount, DEFAULT_SPAWN_THRESHOLD, request.plan);
|
|
104
95
|
}
|
|
105
|
-
|
|
106
|
-
export function evaluateReviewerExploreGate(request: ReviewerExploreGateRequest): SpawnGateDecision {
|
|
107
|
-
if (request.spawningAgentType !== "reviewer" || request.targetAgent !== "explore") {
|
|
108
|
-
return {
|
|
109
|
-
outcome: "allowed",
|
|
110
|
-
reason: "reviewer->explore gate does not apply",
|
|
111
|
-
planRequired: false,
|
|
112
|
-
missingFields: [],
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const missingFields = findMissingPlanFields(request.plan);
|
|
117
|
-
if (missingFields.length > 0) {
|
|
118
|
-
return {
|
|
119
|
-
outcome: "rejected",
|
|
120
|
-
reason: `reviewer->explore spawn requires a complete spawn-plan receipt (${missingFields.join(", ")})`,
|
|
121
|
-
planRequired: true,
|
|
122
|
-
missingFields,
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return {
|
|
127
|
-
outcome: "allowed",
|
|
128
|
-
reason: "reviewer->explore spawn has a complete spawn-plan receipt",
|
|
129
|
-
planRequired: true,
|
|
130
|
-
missingFields: [],
|
|
131
|
-
};
|
|
132
|
-
}
|
package/src/task/types.ts
CHANGED
|
@@ -72,7 +72,7 @@ const spawnPlanSchema = z
|
|
|
72
72
|
expectedReceiptShape: z.string(),
|
|
73
73
|
maxInlineTokens: z.number(),
|
|
74
74
|
})
|
|
75
|
-
.describe("justification required before spawning more than four tasks
|
|
75
|
+
.describe("justification required before spawning more than four tasks");
|
|
76
76
|
|
|
77
77
|
const createTaskItemSchema = (_contextEnabled: boolean) =>
|
|
78
78
|
z.object({
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-wide registry mapping a session id to its active {@link AskAnswerSource}.
|
|
3
|
+
*
|
|
4
|
+
* Decouples the `ask` tool (which reads the source via `AgentSession`) from the
|
|
5
|
+
* notifications extension (which registers one), without threading a new method
|
|
6
|
+
* through the extension/runner/controller wiring. A session has at most one
|
|
7
|
+
* source; registering returns a disposer.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { AskAnswerSource } from "./index";
|
|
11
|
+
|
|
12
|
+
const sources = new Map<string, AskAnswerSource>();
|
|
13
|
+
|
|
14
|
+
/** Register `source` for `sessionId`. Returns a disposer that clears it. */
|
|
15
|
+
export function registerAskAnswerSource(sessionId: string, source: AskAnswerSource): () => void {
|
|
16
|
+
sources.set(sessionId, source);
|
|
17
|
+
return () => {
|
|
18
|
+
if (sources.get(sessionId) === source) sources.delete(sessionId);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The answer source for `sessionId`, if one is registered. */
|
|
23
|
+
export function getAskAnswerSource(sessionId: string): AskAnswerSource | undefined {
|
|
24
|
+
return sources.get(sessionId);
|
|
25
|
+
}
|
package/src/tools/ask.ts
CHANGED
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
renderDeepInterviewAskQuestion,
|
|
35
35
|
} from "../deep-interview/render-middleware";
|
|
36
36
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
37
|
-
import { appendOrMergeDeepInterviewRound } from "../gjc-runtime/deep-interview-recorder";
|
|
37
|
+
import { appendOrMergeDeepInterviewRound, syncDeepInterviewRecorderHud } from "../gjc-runtime/deep-interview-recorder";
|
|
38
38
|
import { deepInterviewStatePath } from "../gjc-runtime/deep-interview-runtime";
|
|
39
39
|
import { gateAnswerToResult, questionToGate } from "../modes/shared/agent-wire/deep-interview-gate";
|
|
40
40
|
import { getMarkdownTheme, type Theme, theme } from "../modes/theme/theme";
|
|
@@ -104,6 +104,30 @@ export interface AskToolDetails {
|
|
|
104
104
|
const OTHER_OPTION = "Other (type your own)";
|
|
105
105
|
const RECOMMENDED_SUFFIX = " (Recommended)";
|
|
106
106
|
const DEEP_INTERVIEW_SELECTOR_SCROLL_TITLE_ROWS = Number.MAX_SAFE_INTEGER;
|
|
107
|
+
const DEEP_INTERVIEW_RECORDER_AWAIT_TIMEOUT_MS = 250;
|
|
108
|
+
|
|
109
|
+
function errorMessage(error: unknown): string {
|
|
110
|
+
return error instanceof Error ? error.message : String(error);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function awaitDeepInterviewRecorderPersistence(persistence: Promise<void>): Promise<void> {
|
|
114
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
115
|
+
try {
|
|
116
|
+
await Promise.race([
|
|
117
|
+
persistence,
|
|
118
|
+
new Promise<never>((_resolve, reject) => {
|
|
119
|
+
timeout = setTimeout(
|
|
120
|
+
() => reject(new Error(`timed out after ${DEEP_INTERVIEW_RECORDER_AWAIT_TIMEOUT_MS}ms`)),
|
|
121
|
+
DEEP_INTERVIEW_RECORDER_AWAIT_TIMEOUT_MS,
|
|
122
|
+
);
|
|
123
|
+
}),
|
|
124
|
+
]);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
logger.warn(`ask: deep-interview round recording failed: ${errorMessage(error)}`);
|
|
127
|
+
} finally {
|
|
128
|
+
if (timeout) clearTimeout(timeout);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
107
131
|
|
|
108
132
|
function getDoneOptionLabel(): string {
|
|
109
133
|
return `${theme.status.success} Done selecting`;
|
|
@@ -406,8 +430,18 @@ async function askSingleQuestion(
|
|
|
406
430
|
// If input was dismissed (undefined), keep prior selectedOptions/customInput intact
|
|
407
431
|
}
|
|
408
432
|
} else {
|
|
409
|
-
|
|
410
|
-
|
|
433
|
+
const stripped = stripRecommendedSuffix(choice);
|
|
434
|
+
if (optionLabels.includes(stripped)) {
|
|
435
|
+
selectedOptions = [stripped];
|
|
436
|
+
customInput = undefined;
|
|
437
|
+
} else {
|
|
438
|
+
// A remote answer (e.g. a typed Telegram reply) that is not one of the
|
|
439
|
+
// listed options is the "provide my own" custom input — recorded the same
|
|
440
|
+
// as picking Other and typing it. The local selector can only ever return
|
|
441
|
+
// a listed entry, so this branch is reached only for free-text answers.
|
|
442
|
+
customInput = choice;
|
|
443
|
+
selectedOptions = [];
|
|
444
|
+
}
|
|
411
445
|
}
|
|
412
446
|
if (navigation?.allowForward) {
|
|
413
447
|
return { selectedOptions, customInput, timedOut, navigation: "forward" };
|
|
@@ -481,11 +515,11 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
481
515
|
): Promise<void> {
|
|
482
516
|
const meta = q.deepInterview;
|
|
483
517
|
if (!meta) return;
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
518
|
+
const cwd = this.session.cwd;
|
|
519
|
+
const sessionId = this.session.getSessionId?.() ?? undefined;
|
|
520
|
+
const statePath = deepInterviewStatePath(cwd, sessionId);
|
|
521
|
+
await awaitDeepInterviewRecorderPersistence(
|
|
522
|
+
appendOrMergeDeepInterviewRound(
|
|
489
523
|
cwd,
|
|
490
524
|
statePath,
|
|
491
525
|
{
|
|
@@ -500,12 +534,10 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
500
534
|
customInput,
|
|
501
535
|
},
|
|
502
536
|
{ sessionId },
|
|
503
|
-
)
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
);
|
|
508
|
-
}
|
|
537
|
+
).then(async () => {
|
|
538
|
+
await syncDeepInterviewRecorderHud(cwd, statePath, sessionId);
|
|
539
|
+
}),
|
|
540
|
+
);
|
|
509
541
|
}
|
|
510
542
|
|
|
511
543
|
async execute(
|
|
@@ -529,11 +561,71 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
529
561
|
const ui: UIContext = {
|
|
530
562
|
select: (prompt, options, dialogOptions) => {
|
|
531
563
|
if (!extensionUi) throw new ToolAbortError("Ask tool requires interactive mode");
|
|
532
|
-
|
|
564
|
+
const source = this.session.getAskAnswerSource?.();
|
|
565
|
+
if (!source) return extensionUi.select(prompt, options, dialogOptions);
|
|
566
|
+
// Race the local UI against a remote answer (e.g. a Telegram reply via the
|
|
567
|
+
// notifications SDK) so asks can be answered without RPC mode. When the
|
|
568
|
+
// local UI wins, abort the remote source so it stops waiting and marks the
|
|
569
|
+
// action resolved-locally. First valid answer wins.
|
|
570
|
+
// Race the local UI against a remote answer (e.g. a Telegram reply via the
|
|
571
|
+
// notifications SDK) so asks can be answered without RPC mode. First valid
|
|
572
|
+
// answer wins; the loser is aborted so neither side is left hanging:
|
|
573
|
+
// - local wins -> abort the remote source (marks the action resolved-locally)
|
|
574
|
+
// - remote wins -> abort the local selector so the TUI dialog actually closes
|
|
575
|
+
const remoteController = new AbortController();
|
|
576
|
+
const localController = new AbortController();
|
|
577
|
+
// Propagate an external cancel (the tool's signal) to the local selector too.
|
|
578
|
+
const toolSignal = dialogOptions?.signal;
|
|
579
|
+
if (toolSignal) {
|
|
580
|
+
if (toolSignal.aborted) localController.abort();
|
|
581
|
+
else toolSignal.addEventListener("abort", () => localController.abort(), { once: true });
|
|
582
|
+
}
|
|
583
|
+
const remote = source.awaitAnswer(prompt, options, remoteController.signal).then(answer => {
|
|
584
|
+
// undefined is not a valid remote answer (registration failed, or the local
|
|
585
|
+
// UI already won and aborted us): never settle the race, let the local
|
|
586
|
+
// selector decide instead of cancelling the ask.
|
|
587
|
+
if (answer === undefined) return new Promise<string | undefined>(() => {});
|
|
588
|
+
localController.abort();
|
|
589
|
+
return answer;
|
|
590
|
+
});
|
|
591
|
+
const local = extensionUi
|
|
592
|
+
.select(prompt, options, { ...dialogOptions, signal: localController.signal })
|
|
593
|
+
.then(answer => {
|
|
594
|
+
remoteController.abort();
|
|
595
|
+
return answer;
|
|
596
|
+
});
|
|
597
|
+
// The losing selector may reject when aborted after the race already settled;
|
|
598
|
+
// swallow that so it is not an unhandled rejection (the race result is unaffected).
|
|
599
|
+
void local.catch(() => undefined);
|
|
600
|
+
return Promise.race([local, remote]);
|
|
533
601
|
},
|
|
534
602
|
editor: (title, prefill, dialogOptions, editorOptions) => {
|
|
535
603
|
if (!extensionUi) throw new ToolAbortError("Ask tool requires interactive mode");
|
|
536
|
-
|
|
604
|
+
const source = this.session.getAskAnswerSource?.();
|
|
605
|
+
if (!source) return extensionUi.editor(title, prefill, dialogOptions, editorOptions);
|
|
606
|
+
// Race the local editor against a remote free-text answer so "Other / type
|
|
607
|
+
// your own" custom input can be provided remotely (e.g. a typed Telegram
|
|
608
|
+
// reply) instead of blocking on the local-only editor. Mirrors `select`.
|
|
609
|
+
const remoteController = new AbortController();
|
|
610
|
+
const localController = new AbortController();
|
|
611
|
+
const toolSignal = dialogOptions?.signal;
|
|
612
|
+
if (toolSignal) {
|
|
613
|
+
if (toolSignal.aborted) localController.abort();
|
|
614
|
+
else toolSignal.addEventListener("abort", () => localController.abort(), { once: true });
|
|
615
|
+
}
|
|
616
|
+
const remote = source.awaitAnswer(title, [], remoteController.signal).then(answer => {
|
|
617
|
+
if (answer === undefined) return new Promise<string | undefined>(() => {});
|
|
618
|
+
localController.abort();
|
|
619
|
+
return answer;
|
|
620
|
+
});
|
|
621
|
+
const local = extensionUi
|
|
622
|
+
.editor(title, prefill, { ...(dialogOptions ?? {}), signal: localController.signal }, editorOptions)
|
|
623
|
+
.then(answer => {
|
|
624
|
+
remoteController.abort();
|
|
625
|
+
return answer;
|
|
626
|
+
});
|
|
627
|
+
void local.catch(() => undefined);
|
|
628
|
+
return Promise.race([local, remote]);
|
|
537
629
|
},
|
|
538
630
|
};
|
|
539
631
|
|
package/src/tools/computer.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { ImageContent } from "@gajae-code/ai";
|
|
|
6
6
|
import { prompt } from "@gajae-code/utils";
|
|
7
7
|
import * as z from "zod/v4";
|
|
8
8
|
import computerDescription from "../prompts/tools/computer.md" with { type: "text" };
|
|
9
|
+
import { resizeImage } from "../utils/image-resize";
|
|
9
10
|
import type { ToolSession } from "./index";
|
|
10
11
|
import type { OutputMeta } from "./output-meta";
|
|
11
12
|
import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
|
|
@@ -174,6 +175,11 @@ let platformOverrideForTests: NodeJS.Platform | undefined;
|
|
|
174
175
|
let archOverrideForTests: NodeJS.Architecture | undefined;
|
|
175
176
|
const screenshotFallbackDirs = new WeakMap<ToolSession, Promise<string>>();
|
|
176
177
|
|
|
178
|
+
const COMPUTER_INLINE_SCREENSHOT_MAX_WIDTH = 1568;
|
|
179
|
+
const COMPUTER_INLINE_SCREENSHOT_MAX_HEIGHT = 1568;
|
|
180
|
+
const COMPUTER_INLINE_SCREENSHOT_PROVIDER_MAX_BYTES = 5 * 1024 * 1024;
|
|
181
|
+
const COMPUTER_INLINE_SCREENSHOT_JPEG_QUALITY = 70;
|
|
182
|
+
|
|
177
183
|
export function setComputerControllerFactoryForTests(factory: ComputerControllerFactory | undefined): void {
|
|
178
184
|
controllerFactory = factory ?? createNativeComputerController;
|
|
179
185
|
}
|
|
@@ -278,6 +284,9 @@ export class ComputerTool implements AgentTool<typeof computerSchema, ComputerTo
|
|
|
278
284
|
if (batchResult.failedStep) {
|
|
279
285
|
details.code = batchResult.failedStep.code;
|
|
280
286
|
details.message = batchResult.failedStep.message;
|
|
287
|
+
if (batchResult.screenshotSource !== undefined) {
|
|
288
|
+
await persistScreenshotFallback(batchResult.screenshotSource, details.screenshot, this.session);
|
|
289
|
+
}
|
|
281
290
|
await writeComputerAuditLog(this.session, details);
|
|
282
291
|
return {
|
|
283
292
|
...toolResult(details).text(`${details.code}: ${details.message}`).done(),
|
|
@@ -285,11 +294,11 @@ export class ComputerTool implements AgentTool<typeof computerSchema, ComputerTo
|
|
|
285
294
|
};
|
|
286
295
|
}
|
|
287
296
|
details.message = describeComputerSuccess(details);
|
|
288
|
-
const image = imageContentFromNativeResult(batchResult.screenshotSource);
|
|
289
297
|
if (batchResult.screenshotSource !== undefined) {
|
|
290
298
|
await persistScreenshotFallback(batchResult.screenshotSource, details.screenshot, this.session);
|
|
291
299
|
details.message = describeComputerSuccess(details);
|
|
292
300
|
}
|
|
301
|
+
const image = await inlineImageContentFromNativeResult(batchResult.screenshotSource, details, this.session);
|
|
293
302
|
await writeComputerAuditLog(this.session, details);
|
|
294
303
|
return image
|
|
295
304
|
? toolResult(details)
|
|
@@ -302,11 +311,11 @@ export class ComputerTool implements AgentTool<typeof computerSchema, ComputerTo
|
|
|
302
311
|
if (screenshot) details.screenshot = screenshot;
|
|
303
312
|
details.status = "success";
|
|
304
313
|
details.message = describeComputerSuccess(details);
|
|
305
|
-
const image = imageContentFromNativeResult(result);
|
|
306
314
|
if (screenshot) {
|
|
307
315
|
await persistScreenshotFallback(result, details.screenshot, this.session);
|
|
308
316
|
details.message = describeComputerSuccess(details);
|
|
309
317
|
}
|
|
318
|
+
const image = await inlineImageContentFromNativeResult(result, details, this.session);
|
|
310
319
|
await writeComputerAuditLog(this.session, details);
|
|
311
320
|
return image
|
|
312
321
|
? toolResult(details)
|
|
@@ -472,7 +481,7 @@ function normalizeScreenshot(value: unknown): ComputerScreenshotDetails | undefi
|
|
|
472
481
|
};
|
|
473
482
|
}
|
|
474
483
|
|
|
475
|
-
function
|
|
484
|
+
function fullResolutionImageContentFromNativeResult(value: unknown): ImageContent | undefined {
|
|
476
485
|
const candidate =
|
|
477
486
|
value && typeof value === "object" && "screenshot" in value
|
|
478
487
|
? (value as { screenshot?: unknown }).screenshot
|
|
@@ -483,13 +492,42 @@ function imageContentFromNativeResult(value: unknown): ImageContent | undefined
|
|
|
483
492
|
return data ? { type: "image", data, mimeType: "image/png" } : undefined;
|
|
484
493
|
}
|
|
485
494
|
|
|
495
|
+
async function inlineImageContentFromNativeResult(
|
|
496
|
+
value: unknown,
|
|
497
|
+
details: ComputerToolDetails,
|
|
498
|
+
session: ToolSession,
|
|
499
|
+
): Promise<ImageContent | undefined> {
|
|
500
|
+
const image = fullResolutionImageContentFromNativeResult(value);
|
|
501
|
+
if (!image) return undefined;
|
|
502
|
+
const maxBytes = getInlineScreenshotMaxBytes(session);
|
|
503
|
+
const originalBytes = Buffer.byteLength(image.data, "base64");
|
|
504
|
+
if (originalBytes <= maxBytes) return image;
|
|
505
|
+
|
|
506
|
+
try {
|
|
507
|
+
const resized = await resizeImage(image, {
|
|
508
|
+
maxWidth: COMPUTER_INLINE_SCREENSHOT_MAX_WIDTH,
|
|
509
|
+
maxHeight: COMPUTER_INLINE_SCREENSHOT_MAX_HEIGHT,
|
|
510
|
+
maxBytes,
|
|
511
|
+
jpegQuality: COMPUTER_INLINE_SCREENSHOT_JPEG_QUALITY,
|
|
512
|
+
});
|
|
513
|
+
if (resized.buffer.length <= maxBytes) {
|
|
514
|
+
return { type: "image", data: resized.data, mimeType: resized.mimeType };
|
|
515
|
+
}
|
|
516
|
+
} catch {
|
|
517
|
+
// Keep the action successful and rely on the full-resolution artifact path below.
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
details.message = `${details.message} Inline screenshot omitted because it could not be bounded below ${formatByteCount(maxBytes)}; use the saved screenshot artifact instead.`;
|
|
521
|
+
return undefined;
|
|
522
|
+
}
|
|
523
|
+
|
|
486
524
|
async function persistScreenshotFallback(
|
|
487
525
|
value: unknown,
|
|
488
526
|
screenshot: ComputerScreenshotDetails | undefined,
|
|
489
527
|
session: ToolSession,
|
|
490
528
|
): Promise<void> {
|
|
491
529
|
if (!screenshot || screenshot.path) return;
|
|
492
|
-
const image =
|
|
530
|
+
const image = fullResolutionImageContentFromNativeResult(value);
|
|
493
531
|
if (!image) return;
|
|
494
532
|
const dir = await getScreenshotFallbackDir(session);
|
|
495
533
|
const filePath = path.join(dir, `computer-${Date.now()}-${Math.random().toString(36).slice(2)}.png`);
|
|
@@ -526,6 +564,22 @@ function getPngByteLength(png: NativeScreenshot["png"]): number | undefined {
|
|
|
526
564
|
return png.byteLength;
|
|
527
565
|
}
|
|
528
566
|
|
|
567
|
+
function getInlineScreenshotMaxBytes(session: Pick<ToolSession, "settings">): number {
|
|
568
|
+
const configured = Number(session.settings.get("computer.screenshotMaxBytes"));
|
|
569
|
+
const finiteConfigured =
|
|
570
|
+
Number.isFinite(configured) && configured > 0
|
|
571
|
+
? Math.floor(configured)
|
|
572
|
+
: COMPUTER_INLINE_SCREENSHOT_PROVIDER_MAX_BYTES;
|
|
573
|
+
return Math.min(finiteConfigured, COMPUTER_INLINE_SCREENSHOT_PROVIDER_MAX_BYTES);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function formatByteCount(bytes: number): string {
|
|
577
|
+
if (bytes < 1024) return `${bytes} bytes`;
|
|
578
|
+
const kib = bytes / 1024;
|
|
579
|
+
if (kib < 1024) return `${Math.round(kib)} KiB`;
|
|
580
|
+
return `${(kib / 1024).toFixed(1)} MiB`;
|
|
581
|
+
}
|
|
582
|
+
|
|
529
583
|
function mapComputerError(error: unknown, hotkey?: string): { code: string; message: string } {
|
|
530
584
|
if (error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError")) {
|
|
531
585
|
return {
|
package/src/tools/image-gen.ts
CHANGED
|
@@ -472,20 +472,17 @@ async function findImageApiKey(
|
|
|
472
472
|
const openAI = await findOpenAIHostedImageCredentials(modelRegistry, activeModel, sessionId);
|
|
473
473
|
if (openAI) return openAI;
|
|
474
474
|
// Fall through to auto-detect if preferred provider key not found.
|
|
475
|
-
} else if (preferredImageProvider === "antigravity"
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
// Fall through to auto-detect if preferred provider key not found.
|
|
475
|
+
} else if (preferredImageProvider === "antigravity") {
|
|
476
|
+
if (!modelRegistry) return null;
|
|
477
|
+
return await findAntigravityCredentials(modelRegistry, sessionId);
|
|
479
478
|
} else if (preferredImageProvider === "gemini") {
|
|
480
479
|
const geminiKey = getEnvApiKey("google");
|
|
481
480
|
if (geminiKey) return { provider: "gemini", apiKey: geminiKey };
|
|
482
481
|
const googleKey = $env.GOOGLE_API_KEY;
|
|
483
|
-
|
|
484
|
-
// Fall through to auto-detect if preferred provider key not found.
|
|
482
|
+
return googleKey ? { provider: "gemini", apiKey: googleKey } : null;
|
|
485
483
|
} else if (preferredImageProvider === "openrouter") {
|
|
486
484
|
const openRouterKey = getEnvApiKey("openrouter");
|
|
487
|
-
|
|
488
|
-
// Fall through to auto-detect if preferred provider key not found.
|
|
485
|
+
return openRouterKey ? { provider: "openrouter", apiKey: openRouterKey } : null;
|
|
489
486
|
}
|
|
490
487
|
|
|
491
488
|
// Auto-detect: GPT hosted image generation, then Antigravity, OpenRouter, Gemini.
|
package/src/tools/index.ts
CHANGED
|
@@ -118,6 +118,18 @@ export type {
|
|
|
118
118
|
DiscoverableToolSource,
|
|
119
119
|
} from "../tool-discovery/tool-index";
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Source of remote answers for interactive asks (e.g. a Telegram reply routed
|
|
123
|
+
* through the notifications SDK). Lets a pending ask resolve without RPC mode.
|
|
124
|
+
*/
|
|
125
|
+
export interface AskAnswerSource {
|
|
126
|
+
/**
|
|
127
|
+
* Race a remote answer against the local UI for one question. Resolves with the
|
|
128
|
+
* chosen option label or free-text answer, or `undefined` to defer to local UI.
|
|
129
|
+
*/
|
|
130
|
+
awaitAnswer(question: string, options: string[], signal?: AbortSignal): Promise<string | undefined>;
|
|
131
|
+
}
|
|
132
|
+
|
|
121
133
|
/** Session context for tool factories */
|
|
122
134
|
export interface ToolSession {
|
|
123
135
|
/** Current working directory */
|
|
@@ -214,6 +226,13 @@ export interface ToolSession {
|
|
|
214
226
|
getGoalModeState?: () => GoalModeState | undefined;
|
|
215
227
|
/** Unattended workflow-gate emitter (present only when unattended mode is negotiated). */
|
|
216
228
|
getWorkflowGateEmitter?: () => WorkflowGateEmitter | undefined;
|
|
229
|
+
/**
|
|
230
|
+
* Optional remote answer source for interactive asks. When present, the ask
|
|
231
|
+
* tool races the local UI selection against a remote answer (e.g. a Telegram
|
|
232
|
+
* reply via the notifications SDK) so asks can be answered without RPC mode.
|
|
233
|
+
* No-op when undefined: the ask path behaves exactly as before.
|
|
234
|
+
*/
|
|
235
|
+
getAskAnswerSource?: () => AskAnswerSource | undefined;
|
|
217
236
|
/** Optional per-session restriction for goal tool operations. */
|
|
218
237
|
goalToolAllowedOps?: readonly ("create" | "get" | "complete" | "resume" | "drop" | "pause")[];
|
|
219
238
|
/** Goal runtime for the active agent session. */
|
|
@@ -78,21 +78,26 @@ export class InspectImageTool implements AgentTool<typeof inspectImageSchema, In
|
|
|
78
78
|
};
|
|
79
79
|
|
|
80
80
|
const activeModelPattern = this.session.getActiveModelString?.() ?? this.session.getModelString?.();
|
|
81
|
-
|
|
81
|
+
const configuredVisionPattern = this.session.settings.getModelRole("vision")?.trim();
|
|
82
|
+
const configuredVisionModel = configuredVisionPattern ? resolvePattern("pi/vision") : undefined;
|
|
83
|
+
if (configuredVisionPattern && !configuredVisionModel) {
|
|
84
|
+
throw new ToolError(
|
|
85
|
+
`Configured modelRoles.vision (${configuredVisionPattern}) did not resolve to an available model. Configure modelRoles.vision with a vision-capable model.`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const model = configuredVisionModel ?? resolvePattern("pi/default") ?? resolvePattern(activeModelPattern);
|
|
82
89
|
if (!model) {
|
|
83
|
-
throw new ToolError(
|
|
90
|
+
throw new ToolError(
|
|
91
|
+
"Unable to resolve a model for inspect_image. Configure modelRoles.vision with a vision-capable model or select a vision-capable active/default model.",
|
|
92
|
+
);
|
|
84
93
|
}
|
|
85
94
|
|
|
86
|
-
// inspect_image requires image input
|
|
87
|
-
//
|
|
95
|
+
// inspect_image requires image input. A text-only selected model must be
|
|
96
|
+
// paired with an explicit vision role so the model/cost boundary is visible.
|
|
88
97
|
if (!model.input.includes("image")) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
`Resolved model ${model.provider}/${model.id} does not support image input, and no vision-capable model is available. Configure a vision-capable model.`,
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
model = visionModel;
|
|
98
|
+
throw new ToolError(
|
|
99
|
+
`Resolved model ${model.provider}/${model.id} does not support image input. Configure modelRoles.vision with a vision-capable model.`,
|
|
100
|
+
);
|
|
96
101
|
}
|
|
97
102
|
|
|
98
103
|
const apiKey = await modelRegistry.getApiKey(model);
|
|
@@ -160,6 +160,13 @@ function renderSubagentSnapshotBody(snapshot: SubagentSnapshot, expanded: boolea
|
|
|
160
160
|
lines.push(` ${theme.fg("dim", "Assignment:")}`);
|
|
161
161
|
for (const al of snapshot.assignment.split("\n")) lines.push(` ${theme.fg("toolOutput", replaceTabs(al))}`);
|
|
162
162
|
}
|
|
163
|
+
if (snapshot.steerMessage) {
|
|
164
|
+
lines.push(` ${theme.fg("accent", `Steer (${snapshot.steerState ?? "queued"})`)}`);
|
|
165
|
+
const maxLines = expanded ? PREVIEW_LINES_EXPANDED : PREVIEW_LINES_COLLAPSED;
|
|
166
|
+
for (const pl of getPreviewLines(snapshot.steerMessage, maxLines, PREVIEW_LINE_WIDTH, Ellipsis.Unicode)) {
|
|
167
|
+
lines.push(` ${theme.fg("toolOutput", replaceTabs(pl))}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
163
170
|
|
|
164
171
|
// Defense in depth: the producer only attaches `progress` when a live producer
|
|
165
172
|
// exists (subagent.ts #liveProgressFields), but the renderer also honors an
|