@gajae-code/coding-agent 0.6.5 → 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 +29 -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/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/notify.d.ts +41 -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/state-writer.d.ts +2 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +15 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +14 -0
- 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-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/rlm/index.d.ts +12 -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/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/notify-cli.ts +274 -0
- package/src/cli/setup-cli.ts +173 -84
- package/src/cli.ts +2 -0
- package/src/commands/daemon.ts +47 -0
- package/src/commands/notify.ts +61 -0
- package/src/commands/setup.ts +11 -1
- 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/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/skills/ultragoal/SKILL.md +16 -0
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +8 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +12 -4
- package/src/gjc-runtime/state-runtime.ts +18 -4
- package/src/gjc-runtime/state-writer.ts +8 -8
- package/src/gjc-runtime/ultragoal-guard.ts +57 -2
- package/src/gjc-runtime/ultragoal-runtime.ts +105 -19
- package/src/gjc-runtime/workflow-manifest.generated.json +27 -2
- package/src/gjc-runtime/workflow-manifest.ts +11 -1
- package/src/goals/tools/goal-tool.ts +11 -2
- package/src/internal-urls/docs-index.generated.ts +6 -5
- package/src/main.ts +30 -0
- package/src/modes/acp/acp-event-mapper.ts +1 -0
- 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/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-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/rlm/index.ts +19 -0
- package/src/sdk.ts +16 -0
- package/src/session/agent-session.ts +113 -3
- 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/host-plugin-setup.ts +142 -0
- package/src/slash-commands/builtin-registry.ts +4 -1
- package/src/task/executor.ts +5 -1
- package/src/tools/ask-answer-registry.ts +25 -0
- package/src/tools/ask.ts +74 -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
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host plugin setup for `gjc setup claude` and `gjc setup codex`.
|
|
3
|
+
*
|
|
4
|
+
* Renders install guidance and a fail-closed coordinator MCP config preview for
|
|
5
|
+
* the canonical generated plugin bundle under `plugins/`. This is intentionally
|
|
6
|
+
* render-only and fail-closed: the workdir allowlist is scoped to the project
|
|
7
|
+
* root and no mutation class is enabled until the user opts in.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as fs from "node:fs";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import { getProjectDir } from "@gajae-code/utils";
|
|
13
|
+
|
|
14
|
+
export type HostPluginKind = "claude" | "codex";
|
|
15
|
+
|
|
16
|
+
export interface HostPluginSetupFlags {
|
|
17
|
+
json?: boolean;
|
|
18
|
+
check?: boolean;
|
|
19
|
+
root?: string[];
|
|
20
|
+
repo?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface HostPluginSetupResult {
|
|
24
|
+
ok: true;
|
|
25
|
+
host: HostPluginKind;
|
|
26
|
+
mode: "render";
|
|
27
|
+
gated: boolean;
|
|
28
|
+
pluginPath: string;
|
|
29
|
+
manifestPath: string;
|
|
30
|
+
marketplacePath: string;
|
|
31
|
+
installGuidance: string[];
|
|
32
|
+
coordinatorConfigPreview: {
|
|
33
|
+
command: string;
|
|
34
|
+
args: string[];
|
|
35
|
+
env: Record<string, string>;
|
|
36
|
+
};
|
|
37
|
+
mutationPolicy: string;
|
|
38
|
+
notes: string[];
|
|
39
|
+
check?: { ok: boolean; checked: string[]; missing: string[] };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const NAMESPACE_LABEL = "gajae-code-plugin";
|
|
43
|
+
|
|
44
|
+
function resolveProjectRoot(flags: HostPluginSetupFlags): string {
|
|
45
|
+
const explicit = flags.root?.find(root => root.trim().length > 0);
|
|
46
|
+
return explicit ? path.resolve(explicit) : getProjectDir();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function verifyBundleFiles(files: string[]): { ok: boolean; checked: string[]; missing: string[] } {
|
|
50
|
+
const missing = files.filter(file => !fs.existsSync(file));
|
|
51
|
+
return { ok: missing.length === 0, checked: files, missing };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildHostPluginSetup(host: HostPluginKind, flags: HostPluginSetupFlags = {}): HostPluginSetupResult {
|
|
55
|
+
const projectRoot = resolveProjectRoot(flags);
|
|
56
|
+
const marketplaceRoot = path.join(projectRoot, "plugins");
|
|
57
|
+
const pluginDir = path.join(marketplaceRoot, "gajae-code");
|
|
58
|
+
const repo = flags.repo && flags.repo.trim().length > 0 ? flags.repo.trim() : NAMESPACE_LABEL;
|
|
59
|
+
|
|
60
|
+
// Concrete, fail-closed env: workdir allowlist is the project root, no mutations.
|
|
61
|
+
const env: Record<string, string> = {
|
|
62
|
+
GJC_COORDINATOR_MCP_WORKDIR_ROOTS: projectRoot,
|
|
63
|
+
GJC_COORDINATOR_MCP_REPO: repo,
|
|
64
|
+
GJC_COORDINATOR_MCP_SESSION_COMMAND: "gjc --worktree",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (host === "claude") {
|
|
68
|
+
const manifestPath = path.join(pluginDir, ".claude-plugin", "plugin.json");
|
|
69
|
+
const marketplacePath = path.join(marketplaceRoot, ".claude-plugin", "marketplace.json");
|
|
70
|
+
return {
|
|
71
|
+
ok: true,
|
|
72
|
+
host,
|
|
73
|
+
mode: "render",
|
|
74
|
+
gated: false,
|
|
75
|
+
pluginPath: marketplaceRoot,
|
|
76
|
+
manifestPath,
|
|
77
|
+
marketplacePath,
|
|
78
|
+
installGuidance: [
|
|
79
|
+
`Add the local marketplace: /plugin marketplace add ${marketplaceRoot}`,
|
|
80
|
+
"Install the plugin: /plugin install gajae-code",
|
|
81
|
+
"Then call gjc_delegate_plan / gjc_delegate_execute / gjc_delegate_team from Claude Code.",
|
|
82
|
+
],
|
|
83
|
+
coordinatorConfigPreview: { command: "gjc", args: ["mcp-serve", "coordinator"], env },
|
|
84
|
+
mutationPolicy:
|
|
85
|
+
"Fail-closed: delegation is read-only until you set GJC_COORDINATOR_MCP_MUTATIONS=sessions and pass allow_mutation:true per call.",
|
|
86
|
+
notes: [],
|
|
87
|
+
...(flags.check
|
|
88
|
+
? { check: verifyBundleFiles([manifestPath, marketplacePath, path.join(pluginDir, ".mcp.json")]) }
|
|
89
|
+
: {}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Codex: verified installable on Codex CLI 0.139.0 via the local marketplace smoke.
|
|
94
|
+
const manifestPath = path.join(pluginDir, ".codex-plugin", "plugin.json");
|
|
95
|
+
const marketplacePath = path.join(marketplaceRoot, ".agents", "plugins", "marketplace.json");
|
|
96
|
+
return {
|
|
97
|
+
ok: true,
|
|
98
|
+
host,
|
|
99
|
+
mode: "render",
|
|
100
|
+
gated: false,
|
|
101
|
+
pluginPath: marketplaceRoot,
|
|
102
|
+
manifestPath,
|
|
103
|
+
marketplacePath,
|
|
104
|
+
installGuidance: [
|
|
105
|
+
`Add the local marketplace: codex plugin marketplace add ${marketplaceRoot}`,
|
|
106
|
+
"Install the plugin: codex plugin add gajae-code@gajae-code-local",
|
|
107
|
+
"Then call gjc_delegate_plan / gjc_delegate_execute / gjc_delegate_team from Codex.",
|
|
108
|
+
],
|
|
109
|
+
coordinatorConfigPreview: { command: "gjc", args: ["mcp-serve", "coordinator"], env },
|
|
110
|
+
mutationPolicy:
|
|
111
|
+
"Fail-closed: delegation is read-only until you set GJC_COORDINATOR_MCP_MUTATIONS=sessions and pass allow_mutation:true per call.",
|
|
112
|
+
notes: [
|
|
113
|
+
"Verified on Codex CLI 0.139.0: marketplace add + plugin add install the plugin (enabled) and `codex mcp list` registers gjc-coordinator with the fail-closed env.",
|
|
114
|
+
"The bundled .codex.mcp.json workdir root is host-neutral; `gjc setup codex` renders a concrete root, and operators should re-run the local marketplace smoke on their target Codex version.",
|
|
115
|
+
],
|
|
116
|
+
...(flags.check
|
|
117
|
+
? {
|
|
118
|
+
check: verifyBundleFiles([
|
|
119
|
+
manifestPath,
|
|
120
|
+
marketplacePath,
|
|
121
|
+
path.join(pluginDir, ".codex.mcp.json"),
|
|
122
|
+
path.join(pluginDir, "skills", "gjc-delegation", "SKILL.md"),
|
|
123
|
+
]),
|
|
124
|
+
}
|
|
125
|
+
: {}),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function formatHostPluginSetup(result: HostPluginSetupResult): string {
|
|
130
|
+
const lines: string[] = [];
|
|
131
|
+
lines.push(`host: ${result.host}${result.gated ? " (gated on versioned smoke)" : ""}`);
|
|
132
|
+
lines.push(`plugin: ${result.pluginPath}`);
|
|
133
|
+
lines.push("install:");
|
|
134
|
+
for (const step of result.installGuidance) lines.push(` - ${step}`);
|
|
135
|
+
lines.push(`mcp: ${result.coordinatorConfigPreview.command} ${result.coordinatorConfigPreview.args.join(" ")}`);
|
|
136
|
+
lines.push(
|
|
137
|
+
` GJC_COORDINATOR_MCP_WORKDIR_ROOTS=${result.coordinatorConfigPreview.env.GJC_COORDINATOR_MCP_WORKDIR_ROOTS}`,
|
|
138
|
+
);
|
|
139
|
+
lines.push(result.mutationPolicy);
|
|
140
|
+
for (const note of result.notes) lines.push(`note: ${note}`);
|
|
141
|
+
return lines.join("\n");
|
|
142
|
+
}
|
|
@@ -797,7 +797,10 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
797
797
|
return;
|
|
798
798
|
}
|
|
799
799
|
|
|
800
|
-
runtime.ctx.
|
|
800
|
+
runtime.ctx.showOAuthSelector("login", undefined, {
|
|
801
|
+
allowExternalCredentialDiscovery: true,
|
|
802
|
+
trigger: "bare-login",
|
|
803
|
+
});
|
|
801
804
|
runtime.ctx.editor.setText("");
|
|
802
805
|
},
|
|
803
806
|
},
|
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
|
});
|
|
@@ -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
|
@@ -430,8 +430,18 @@ async function askSingleQuestion(
|
|
|
430
430
|
// If input was dismissed (undefined), keep prior selectedOptions/customInput intact
|
|
431
431
|
}
|
|
432
432
|
} else {
|
|
433
|
-
|
|
434
|
-
|
|
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
|
+
}
|
|
435
445
|
}
|
|
436
446
|
if (navigation?.allowForward) {
|
|
437
447
|
return { selectedOptions, customInput, timedOut, navigation: "forward" };
|
|
@@ -551,11 +561,71 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
551
561
|
const ui: UIContext = {
|
|
552
562
|
select: (prompt, options, dialogOptions) => {
|
|
553
563
|
if (!extensionUi) throw new ToolAbortError("Ask tool requires interactive mode");
|
|
554
|
-
|
|
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]);
|
|
555
601
|
},
|
|
556
602
|
editor: (title, prefill, dialogOptions, editorOptions) => {
|
|
557
603
|
if (!extensionUi) throw new ToolAbortError("Ask tool requires interactive mode");
|
|
558
|
-
|
|
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]);
|
|
559
629
|
},
|
|
560
630
|
};
|
|
561
631
|
|
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
|
package/src/tools/subagent.ts
CHANGED
|
@@ -17,6 +17,8 @@ const MAX_LIST_LIMIT = 50;
|
|
|
17
17
|
const RECEIPT_PREVIEW_WIDTH = 280;
|
|
18
18
|
const PREVIEW_WIDTH = 2_000;
|
|
19
19
|
const FULL_PREVIEW_WIDTH = 12_000;
|
|
20
|
+
const STEER_QUEUED_GUIDANCE =
|
|
21
|
+
"The steer message is queued for the subagent's next steering boundary and has not necessarily taken effect yet.";
|
|
20
22
|
|
|
21
23
|
const subagentSchema = z.object({
|
|
22
24
|
action: z
|
|
@@ -63,6 +65,9 @@ export interface SubagentSnapshot {
|
|
|
63
65
|
outputRef?: string;
|
|
64
66
|
truncated?: boolean;
|
|
65
67
|
guidance?: string;
|
|
68
|
+
steerMessage?: string;
|
|
69
|
+
steerState?: "queued" | "resume_queued" | "resume_started";
|
|
70
|
+
steerPauseRequested?: boolean;
|
|
66
71
|
/** Live streaming progress for the awaited subagent (await panel only; UI detail). */
|
|
67
72
|
progress?: AgentProgress;
|
|
68
73
|
/** True when a live in-session progress producer exists for this subagent. */
|
|
@@ -240,6 +245,7 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
|
|
|
240
245
|
}
|
|
241
246
|
const records: SubagentRecord[] = [];
|
|
242
247
|
const missing: SubagentSnapshot[] = [];
|
|
248
|
+
const steerStates = new Map<string, NonNullable<SubagentSnapshot["steerState"]>>();
|
|
243
249
|
const record = this.#findVisibleRecord(manager, id, ownerFilter);
|
|
244
250
|
const verifiedOutputIds = await this.#verifiedOutputIds(record ? [record] : []);
|
|
245
251
|
if (!record) {
|
|
@@ -249,23 +255,41 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
|
|
|
249
255
|
if (record.status === "running") {
|
|
250
256
|
const handle = manager.getLiveHandle(record.subagentId);
|
|
251
257
|
if (!handle) throw new ToolError(`Subagent ${record.subagentId} has no live handle.`);
|
|
252
|
-
|
|
258
|
+
const fromAgentId = this.session.getAgentId?.() ?? undefined;
|
|
259
|
+
await handle.injectMessage(message, "steer", { fromAgentId });
|
|
253
260
|
if (params.pause === true) manager.pauseSubagent(record.subagentId, ownerFilter);
|
|
261
|
+
records.push(manager.getSubagentRecord(record.subagentId, ownerFilter) ?? record);
|
|
262
|
+
steerStates.set(record.subagentId, "queued");
|
|
254
263
|
} else {
|
|
255
264
|
const result = manager.resumeSubagent(record.subagentId, ownerFilter, message);
|
|
256
|
-
if (!result.ok && result.reason === "context_unavailable") throw new ToolError("context unavailable");
|
|
257
265
|
if (!result.ok && result.reason === "not_found") {
|
|
258
266
|
missing.push(this.#missingSnapshot(id, "not_found", "No visible detached subagent matches this id."));
|
|
267
|
+
} else if (!result.ok) {
|
|
268
|
+
throw new ToolError(`Failed to resume subagent ${record.subagentId}: ${result.reason ?? "unknown"}.`);
|
|
259
269
|
} else {
|
|
260
|
-
|
|
270
|
+
const snapshotRecord = manager.getSubagentRecord(record.subagentId, ownerFilter) ?? record;
|
|
271
|
+
records.push(snapshotRecord);
|
|
272
|
+
steerStates.set(
|
|
273
|
+
snapshotRecord.subagentId,
|
|
274
|
+
result.queued === true || result.status === "queued" ? "resume_queued" : "resume_started",
|
|
275
|
+
);
|
|
261
276
|
}
|
|
262
277
|
}
|
|
263
|
-
if (record.status === "running")
|
|
264
|
-
records.push(manager.getSubagentRecord(record.subagentId, ownerFilter) ?? record);
|
|
265
278
|
}
|
|
266
279
|
return this.#buildSnapshotResult(
|
|
267
280
|
[
|
|
268
|
-
...records.map(record =>
|
|
281
|
+
...records.map(record => {
|
|
282
|
+
const snapshot = this.#recordSnapshot(manager, record, false, verbosity, verifiedOutputIds);
|
|
283
|
+
return {
|
|
284
|
+
...snapshot,
|
|
285
|
+
steerMessage: message,
|
|
286
|
+
steerState: steerStates.get(record.subagentId) ?? "queued",
|
|
287
|
+
steerPauseRequested: params.pause === true,
|
|
288
|
+
guidance: snapshot.guidance
|
|
289
|
+
? `${snapshot.guidance} ${STEER_QUEUED_GUIDANCE}`
|
|
290
|
+
: STEER_QUEUED_GUIDANCE,
|
|
291
|
+
};
|
|
292
|
+
}),
|
|
269
293
|
...missing,
|
|
270
294
|
],
|
|
271
295
|
"Subagent steer",
|
|
@@ -288,7 +312,7 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
|
|
|
288
312
|
if (ids.length === 1) return ids[0]!;
|
|
289
313
|
if (ids.length > 1) {
|
|
290
314
|
throw new ToolError(
|
|
291
|
-
`\`${action}\` accepts exactly one target because \`message\`
|
|
315
|
+
`\`${action}\` accepts exactly one target because \`message\` can be queued for only one subagent.`,
|
|
292
316
|
);
|
|
293
317
|
}
|
|
294
318
|
throw new ToolError(`\`${action}\` requires a single subagent id via \`id\`.`);
|
|
@@ -533,6 +557,10 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
|
|
|
533
557
|
if (snapshot.description) lines.push(`Description: ${snapshot.description}`);
|
|
534
558
|
if (snapshot.outputRef) lines.push(`Output: ${snapshot.outputRef}`);
|
|
535
559
|
if (snapshot.assignment) lines.push("Assignment:", "```", snapshot.assignment, "```");
|
|
560
|
+
if (snapshot.steerMessage) {
|
|
561
|
+
lines.push(`Steer (${snapshot.steerState ?? "queued"}):`, "```", snapshot.steerMessage, "```");
|
|
562
|
+
lines.push(STEER_QUEUED_GUIDANCE);
|
|
563
|
+
}
|
|
536
564
|
if (snapshot.resultPreview) {
|
|
537
565
|
lines.push(snapshot.errorText ? "Error preview:" : "Result preview:", "```", snapshot.resultPreview, "```");
|
|
538
566
|
if (snapshot.truncated)
|
|
@@ -770,6 +798,9 @@ function canonicalizeSnapshotForSignature(snapshot: SubagentSnapshot): unknown {
|
|
|
770
798
|
outputRef: snapshot.outputRef ?? null,
|
|
771
799
|
truncated: snapshot.truncated ?? false,
|
|
772
800
|
guidance: snapshot.guidance ?? null,
|
|
801
|
+
steerMessage: snapshot.steerMessage ?? null,
|
|
802
|
+
steerState: snapshot.steerState ?? null,
|
|
803
|
+
steerPauseRequested: snapshot.steerPauseRequested ?? false,
|
|
773
804
|
liveProgressAvailable: snapshot.liveProgressAvailable ?? null,
|
|
774
805
|
effectiveModel: snapshot.effectiveModel ?? null,
|
|
775
806
|
requestedModel: snapshot.requestedModel ?? null,
|