@gajae-code/coding-agent 0.4.4 → 0.5.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 +83 -0
- package/dist/types/cli/fast-help.d.ts +1 -0
- package/dist/types/cli/setup-cli.d.ts +2 -0
- package/dist/types/commands/harness.d.ts +6 -0
- package/dist/types/commands/setup.d.ts +6 -0
- package/dist/types/config/model-profile-activation.d.ts +11 -2
- package/dist/types/config/model-profiles.d.ts +7 -0
- package/dist/types/config/model-registry.d.ts +6 -0
- package/dist/types/config/model-resolver.d.ts +2 -0
- package/dist/types/config/models-config-schema.d.ts +35 -0
- package/dist/types/config/settings-schema.d.ts +4 -3
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/server.d.ts +8 -2
- package/dist/types/gjc-runtime/team-runtime.d.ts +0 -1
- package/dist/types/gjc-runtime/tmux-common.d.ts +3 -0
- package/dist/types/harness-control-plane/finalize.d.ts +5 -0
- package/dist/types/harness-control-plane/owner.d.ts +1 -1
- package/dist/types/harness-control-plane/phase-rollup.d.ts +23 -0
- package/dist/types/harness-control-plane/receipt-ingest.d.ts +19 -0
- package/dist/types/harness-control-plane/receipt-spool.d.ts +19 -0
- package/dist/types/harness-control-plane/receipts.d.ts +46 -0
- package/dist/types/harness-control-plane/rpc-adapter.d.ts +3 -0
- package/dist/types/harness-control-plane/state-machine.d.ts +6 -1
- package/dist/types/harness-control-plane/types.d.ts +13 -1
- package/dist/types/hindsight/mental-models.d.ts +5 -5
- package/dist/types/main.d.ts +2 -2
- package/dist/types/modes/components/model-selector.d.ts +1 -12
- package/dist/types/modes/rpc/rpc-client.d.ts +2 -2
- package/dist/types/modes/rpc/rpc-types.d.ts +4 -1
- package/dist/types/modes/utils/abort-message.d.ts +4 -0
- package/dist/types/sdk.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/blob-store.d.ts +20 -1
- package/dist/types/session/session-manager.d.ts +32 -6
- package/dist/types/session/streaming-output.d.ts +3 -2
- package/dist/types/session/tool-choice-queue.d.ts +6 -0
- package/dist/types/setup/hermes-setup.d.ts +7 -0
- package/dist/types/task/fork-context-advisory.d.ts +13 -0
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/task/roi-reconciliation.d.ts +27 -0
- package/dist/types/task/types.d.ts +17 -0
- package/dist/types/thinking-metadata.d.ts +16 -0
- package/dist/types/thinking.d.ts +3 -12
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/resolve.d.ts +0 -10
- package/dist/types/utils/tool-choice.d.ts +14 -1
- package/package.json +8 -7
- package/scripts/build-binary.ts +4 -0
- package/src/cli/fast-help.ts +80 -0
- package/src/cli/setup-cli.ts +12 -3
- package/src/cli.ts +112 -17
- package/src/commands/coordinator.ts +44 -1
- package/src/commands/harness.ts +128 -11
- package/src/commands/launch.ts +2 -2
- package/src/commands/mcp-serve.ts +3 -2
- package/src/commands/session.ts +3 -1
- package/src/commands/setup.ts +4 -0
- package/src/config/model-profile-activation.ts +15 -3
- package/src/config/model-profiles.ts +255 -56
- package/src/config/model-resolver.ts +9 -6
- package/src/config/models-config-schema.ts +2 -0
- package/src/config/settings-schema.ts +6 -3
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/server.ts +427 -193
- package/src/cursor.ts +46 -4
- package/src/defaults/gjc/skills/team/SKILL.md +3 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -2
- package/src/export/html/index.ts +13 -9
- package/src/gjc-runtime/launch-worktree.ts +12 -1
- package/src/gjc-runtime/session-state-sidecar.ts +38 -0
- package/src/gjc-runtime/team-runtime.ts +33 -7
- package/src/gjc-runtime/tmux-common.ts +15 -0
- package/src/gjc-runtime/tmux-sessions.ts +19 -11
- package/src/gjc-runtime/ultragoal-runtime.ts +505 -41
- package/src/gjc-runtime/workflow-manifest.generated.json +27 -1
- package/src/gjc-runtime/workflow-manifest.ts +16 -1
- package/src/harness-control-plane/finalize.ts +39 -5
- package/src/harness-control-plane/owner.ts +87 -28
- package/src/harness-control-plane/phase-rollup.ts +96 -0
- package/src/harness-control-plane/receipt-ingest.ts +127 -0
- package/src/harness-control-plane/receipt-spool.ts +128 -0
- package/src/harness-control-plane/receipts.ts +229 -1
- package/src/harness-control-plane/rpc-adapter.ts +8 -0
- package/src/harness-control-plane/state-machine.ts +27 -6
- package/src/harness-control-plane/storage.ts +23 -0
- package/src/harness-control-plane/types.ts +33 -1
- package/src/hindsight/mental-models.ts +17 -16
- package/src/internal-urls/docs-index.generated.ts +8 -7
- package/src/main.ts +7 -3
- package/src/modes/components/assistant-message.ts +26 -14
- package/src/modes/components/diff.ts +97 -0
- package/src/modes/components/model-selector.ts +353 -181
- package/src/modes/components/status-line.ts +6 -6
- package/src/modes/components/tool-execution.ts +30 -13
- package/src/modes/controllers/event-controller.ts +5 -4
- package/src/modes/controllers/selector-controller.ts +33 -42
- package/src/modes/interactive-mode.ts +4 -5
- package/src/modes/print-mode.ts +1 -1
- package/src/modes/rpc/rpc-client.ts +3 -2
- package/src/modes/rpc/rpc-mode.ts +44 -14
- package/src/modes/rpc/rpc-types.ts +5 -2
- package/src/modes/shared/agent-wire/command-dispatch.ts +10 -5
- package/src/modes/shared/agent-wire/command-validation.ts +11 -0
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/utils/abort-message.ts +41 -0
- package/src/modes/utils/context-usage.ts +15 -8
- package/src/modes/utils/ui-helpers.ts +5 -6
- package/src/sdk.ts +38 -6
- package/src/secrets/obfuscator.ts +102 -27
- package/src/session/agent-session.ts +121 -25
- package/src/session/blob-store.ts +89 -3
- package/src/session/session-manager.ts +328 -57
- package/src/session/streaming-output.ts +185 -122
- package/src/session/tool-choice-queue.ts +23 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +3 -2
- package/src/setup/hermes-setup.ts +63 -8
- package/src/task/executor.ts +69 -6
- package/src/task/fork-context-advisory.ts +99 -0
- package/src/task/index.ts +31 -2
- package/src/task/receipt.ts +7 -0
- package/src/task/render.ts +21 -1
- package/src/task/roi-reconciliation.ts +90 -0
- package/src/task/types.ts +15 -0
- package/src/thinking-metadata.ts +51 -0
- package/src/thinking.ts +26 -46
- package/src/tools/bash.ts +1 -1
- package/src/tools/index.ts +4 -2
- package/src/tools/resolve.ts +93 -18
- package/src/tools/subagent-render.ts +10 -1
- package/src/utils/edit-mode.ts +1 -1
- package/src/utils/title-generator.ts +16 -2
- package/src/utils/tool-choice.ts +45 -16
package/src/thinking.ts
CHANGED
|
@@ -2,45 +2,7 @@ import { type ResolvedThinkingLevel, ThinkingLevel } from "@gajae-code/agent-cor
|
|
|
2
2
|
import { clampThinkingLevelForModel, type Effort, THINKING_EFFORTS } from "@gajae-code/ai/model-thinking";
|
|
3
3
|
import type { Model } from "@gajae-code/ai/types";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
* Metadata used to render thinking selector values in the coding-agent UI.
|
|
7
|
-
*/
|
|
8
|
-
export interface ThinkingLevelMetadata {
|
|
9
|
-
value: ThinkingLevel;
|
|
10
|
-
label: string;
|
|
11
|
-
description: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const THINKING_LEVEL_METADATA: Record<ThinkingLevel, ThinkingLevelMetadata> = {
|
|
15
|
-
[ThinkingLevel.Inherit]: {
|
|
16
|
-
value: ThinkingLevel.Inherit,
|
|
17
|
-
label: "inherit",
|
|
18
|
-
description: "Inherit session default",
|
|
19
|
-
},
|
|
20
|
-
[ThinkingLevel.Off]: { value: ThinkingLevel.Off, label: "off", description: "No reasoning" },
|
|
21
|
-
[ThinkingLevel.Minimal]: {
|
|
22
|
-
value: ThinkingLevel.Minimal,
|
|
23
|
-
label: "min",
|
|
24
|
-
description: "Very brief reasoning (~1k tokens)",
|
|
25
|
-
},
|
|
26
|
-
[ThinkingLevel.Low]: { value: ThinkingLevel.Low, label: "low", description: "Light reasoning (~2k tokens)" },
|
|
27
|
-
[ThinkingLevel.Medium]: {
|
|
28
|
-
value: ThinkingLevel.Medium,
|
|
29
|
-
label: "medium",
|
|
30
|
-
description: "Moderate reasoning (~8k tokens)",
|
|
31
|
-
},
|
|
32
|
-
[ThinkingLevel.High]: { value: ThinkingLevel.High, label: "high", description: "Deep reasoning (~16k tokens)" },
|
|
33
|
-
[ThinkingLevel.XHigh]: {
|
|
34
|
-
value: ThinkingLevel.XHigh,
|
|
35
|
-
label: "xhigh",
|
|
36
|
-
description: "Maximum reasoning (~32k tokens)",
|
|
37
|
-
},
|
|
38
|
-
[ThinkingLevel.Max]: {
|
|
39
|
-
value: ThinkingLevel.Max,
|
|
40
|
-
label: "max",
|
|
41
|
-
description: "Opus maximum reasoning",
|
|
42
|
-
},
|
|
43
|
-
};
|
|
5
|
+
export { getThinkingLevelMetadata, type ThinkingLevelMetadata } from "./thinking-metadata";
|
|
44
6
|
|
|
45
7
|
const THINKING_LEVELS = new Set<string>([ThinkingLevel.Inherit, ThinkingLevel.Off, ...THINKING_EFFORTS]);
|
|
46
8
|
const EFFORT_LEVELS = new Set<string>(THINKING_EFFORTS);
|
|
@@ -59,13 +21,6 @@ export function parseThinkingLevel(value: string | null | undefined): ThinkingLe
|
|
|
59
21
|
return value !== undefined && value !== null && THINKING_LEVELS.has(value) ? (value as ThinkingLevel) : undefined;
|
|
60
22
|
}
|
|
61
23
|
|
|
62
|
-
/**
|
|
63
|
-
* Returns display metadata for a thinking selector.
|
|
64
|
-
*/
|
|
65
|
-
export function getThinkingLevelMetadata(level: ThinkingLevel): ThinkingLevelMetadata {
|
|
66
|
-
return THINKING_LEVEL_METADATA[level];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
24
|
/**
|
|
70
25
|
* Converts an agent-local selector into the effort sent to providers.
|
|
71
26
|
*/
|
|
@@ -91,3 +46,28 @@ export function resolveThinkingLevelForModel(
|
|
|
91
46
|
}
|
|
92
47
|
return clampThinkingLevelForModel(model, level);
|
|
93
48
|
}
|
|
49
|
+
|
|
50
|
+
export function clampExplicitThinkingLevelForModel(
|
|
51
|
+
model: Model | undefined,
|
|
52
|
+
level: ThinkingLevel | undefined,
|
|
53
|
+
): ThinkingLevel | undefined {
|
|
54
|
+
if (level === undefined || level === ThinkingLevel.Inherit || level === ThinkingLevel.Off) {
|
|
55
|
+
return level;
|
|
56
|
+
}
|
|
57
|
+
return clampThinkingLevelForModel(model, level);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function formatClampedModelSelector(selector: string, model: Model | undefined): string {
|
|
61
|
+
const slashIdx = selector.indexOf("/");
|
|
62
|
+
if (slashIdx <= 0) return selector;
|
|
63
|
+
const id = selector.slice(slashIdx + 1);
|
|
64
|
+
const colonIdx = id.lastIndexOf(":");
|
|
65
|
+
if (colonIdx === -1) return selector;
|
|
66
|
+
const suffix = id.slice(colonIdx + 1);
|
|
67
|
+
const thinkingLevel = parseThinkingLevel(suffix);
|
|
68
|
+
if (!thinkingLevel) return selector;
|
|
69
|
+
const clamped = clampExplicitThinkingLevelForModel(model, thinkingLevel);
|
|
70
|
+
return clamped && clamped !== ThinkingLevel.Inherit
|
|
71
|
+
? `${selector.slice(0, slashIdx + 1)}${id.slice(0, colonIdx)}:${clamped}`
|
|
72
|
+
: selector.slice(0, slashIdx + 1) + id.slice(0, colonIdx);
|
|
73
|
+
}
|
package/src/tools/bash.ts
CHANGED
|
@@ -51,7 +51,7 @@ async function saveBashOriginalArtifact(session: ToolSession, originalText: stri
|
|
|
51
51
|
const bashSchemaBase = z.object({
|
|
52
52
|
command: z.string().describe("command to execute"),
|
|
53
53
|
env: z.record(z.string().regex(BASH_ENV_NAME_PATTERN), z.string()).optional().describe("extra env vars"),
|
|
54
|
-
timeout: z.number().default(300).describe("timeout in seconds").optional(),
|
|
54
|
+
timeout: z.number().default(300).describe("timeout in seconds, NOT milliseconds (30 = 30s)").optional(),
|
|
55
55
|
cwd: z.string().describe("working directory").optional(),
|
|
56
56
|
pty: z.boolean().describe("run in pty mode").optional(),
|
|
57
57
|
});
|
package/src/tools/index.ts
CHANGED
|
@@ -240,6 +240,8 @@ export interface ToolSession {
|
|
|
240
240
|
getToolChoiceQueue?(): ToolChoiceQueue;
|
|
241
241
|
/** Build a model-provider-specific ToolChoice that targets the named tool, or undefined if unsupported. */
|
|
242
242
|
buildToolChoice?(toolName: string): ToolChoice | undefined;
|
|
243
|
+
/** Build a named tool-choice decision, preserving whether exact named forcing survived capability degradation. */
|
|
244
|
+
buildToolChoiceResult?(toolName: string): import("../utils/tool-choice").NamedToolChoiceResult;
|
|
243
245
|
/** Steer a hidden custom message into the conversation (e.g. a preview reminder). */
|
|
244
246
|
steer?(message: { customType: string; content: string; details?: unknown }): void;
|
|
245
247
|
/** Peek the currently in-flight tool-choice queue directive's invocation handler. Used by the `resolve` tool to dispatch to the pending action. */
|
|
@@ -403,7 +405,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
403
405
|
toolNames && toolNames.length > 0 ? [...new Set(toolNames.map(name => name.toLowerCase()))] : undefined;
|
|
404
406
|
const goalEnabled = session.settings.get("goal.enabled");
|
|
405
407
|
const goalStateToolNames = [...GOAL_MODE_TOOL_NAMES];
|
|
406
|
-
if (goalEnabled &&
|
|
408
|
+
if (goalEnabled && requestedTools && !requestedTools.includes("goal")) {
|
|
407
409
|
requestedTools = [...requestedTools, "goal"];
|
|
408
410
|
}
|
|
409
411
|
if (goalEnabled && requestedTools) {
|
|
@@ -482,7 +484,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
482
484
|
allToolsByRequestName.set(name.toLowerCase(), [name, factory]);
|
|
483
485
|
}
|
|
484
486
|
const isToolAllowed = (name: string) => {
|
|
485
|
-
if (name === "goal") return goalEnabled
|
|
487
|
+
if (name === "goal") return goalEnabled;
|
|
486
488
|
if (goalStateToolNames.includes(name as (typeof GOAL_MODE_TOOL_NAMES)[number])) return goalEnabled;
|
|
487
489
|
if (name === "lsp") return enableLsp && session.settings.get("lsp.enabled");
|
|
488
490
|
if (name === "bash") return true;
|
package/src/tools/resolve.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@gajae-code/agent-core";
|
|
2
|
+
import type { ToolChoice } from "@gajae-code/ai";
|
|
2
3
|
import type { Component } from "@gajae-code/tui";
|
|
3
4
|
import { Text } from "@gajae-code/tui";
|
|
4
5
|
import { prompt, untilAborted } from "@gajae-code/utils";
|
|
@@ -38,6 +39,21 @@ export interface ResolveToolDetails {
|
|
|
38
39
|
* semantics. No session-level abstraction is needed: callers pass their
|
|
39
40
|
* apply/reject functions directly.
|
|
40
41
|
*/
|
|
42
|
+
/**
|
|
43
|
+
* Tags preview-fallback handlers installed in the session's standing-resolve
|
|
44
|
+
* slot so newer previews can replace older ones (latest-preview-wins) without
|
|
45
|
+
* ever displacing a mode-owned handler such as plan mode's approval handler.
|
|
46
|
+
*/
|
|
47
|
+
const previewResolveFallbacks = new WeakSet<object>();
|
|
48
|
+
|
|
49
|
+
function markPreviewResolveFallback(handler: (input: unknown) => Promise<unknown> | unknown): void {
|
|
50
|
+
previewResolveFallbacks.add(handler);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isPreviewResolveFallback(handler: (input: unknown) => Promise<unknown> | unknown): boolean {
|
|
54
|
+
return previewResolveFallbacks.has(handler);
|
|
55
|
+
}
|
|
56
|
+
|
|
41
57
|
export function queueResolveHandler(
|
|
42
58
|
session: ToolSession,
|
|
43
59
|
options: {
|
|
@@ -48,8 +64,6 @@ export function queueResolveHandler(
|
|
|
48
64
|
},
|
|
49
65
|
): void {
|
|
50
66
|
const queue = session.getToolChoiceQueue?.();
|
|
51
|
-
const forced = session.buildToolChoice?.("resolve");
|
|
52
|
-
if (!queue || !forced || typeof forced === "string") return;
|
|
53
67
|
|
|
54
68
|
const steerReminder = (): void => {
|
|
55
69
|
session.steer?.({
|
|
@@ -63,27 +77,88 @@ export function queueResolveHandler(
|
|
|
63
77
|
});
|
|
64
78
|
};
|
|
65
79
|
|
|
66
|
-
|
|
80
|
+
// Re-evaluated on every push (including apply-error re-pushes) so a runtime
|
|
81
|
+
// incapability discovered mid-turn degrades the NEXT push instead of
|
|
82
|
+
// replaying a stale forced choice the model can never satisfy.
|
|
83
|
+
const resolveForcedChoice = (): { forced: ToolChoice | undefined; exactNamed: boolean } => {
|
|
84
|
+
const toolChoiceResult = session.buildToolChoiceResult?.("resolve");
|
|
85
|
+
if (toolChoiceResult !== undefined) {
|
|
86
|
+
return { forced: toolChoiceResult.choice, exactNamed: toolChoiceResult.exactNamed };
|
|
87
|
+
}
|
|
88
|
+
// Legacy bridge fallback: sessions that only provide buildToolChoice
|
|
89
|
+
// (older SDK embedders, test harnesses) keep the previous behavior — a
|
|
90
|
+
// named object choice is treated as exact named forcing.
|
|
91
|
+
const legacyChoice = session.buildToolChoice?.("resolve");
|
|
92
|
+
const isNamedObject = typeof legacyChoice === "object" && legacyChoice !== null;
|
|
93
|
+
return { forced: isNamedObject ? legacyChoice : undefined, exactNamed: isNamedObject };
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const clearFallback = (): void => {
|
|
97
|
+
// Identity-aware: only clear the shared standing slot when it still holds
|
|
98
|
+
// THIS preview's fallback. Plan mode (or a newer preview) may have
|
|
99
|
+
// replaced it in the meantime — leave theirs untouched.
|
|
100
|
+
if (session.peekStandingResolveHandler?.() === invoke) {
|
|
101
|
+
session.setStandingResolveHandler?.(null);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const invoke = async (input: unknown): Promise<AgentToolResult<unknown>> => {
|
|
106
|
+
const result = await runResolveInvocation(input as ResolveParams, {
|
|
107
|
+
sourceToolName: options.sourceToolName,
|
|
108
|
+
label: options.label,
|
|
109
|
+
apply: options.apply,
|
|
110
|
+
reject: options.reject,
|
|
111
|
+
onApplyError: () => {
|
|
112
|
+
// Apply threw (e.g. ast_edit overlapping replacements). Re-push the
|
|
113
|
+
// same directive so the preview remains pending and the model can
|
|
114
|
+
// `discard` or fix-and-retry on the next turn instead of being
|
|
115
|
+
// stranded with no pending action to address. The re-push goes
|
|
116
|
+
// through the exactNamed gate again — degraded models fall back
|
|
117
|
+
// to the reminder alone. The standing fallback stays installed so
|
|
118
|
+
// a voluntary resolve can still reach the pending action.
|
|
119
|
+
pushDirective();
|
|
120
|
+
steerReminder();
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
// Apply succeeded or the preview was discarded: the pending action is
|
|
124
|
+
// finished, so the voluntary-dispatch fallback must not linger.
|
|
125
|
+
clearFallback();
|
|
126
|
+
return result;
|
|
127
|
+
};
|
|
128
|
+
markPreviewResolveFallback(invoke);
|
|
129
|
+
|
|
130
|
+
// Voluntary-dispatch fallback: when forcing is unavailable (statically
|
|
131
|
+
// degraded) or later removed (runtime degradeInFlight drops the queue
|
|
132
|
+
// directive that owns the invoker), the model can still call `resolve`.
|
|
133
|
+
// ResolveTool.execute consults the queue invoker first, so the standing
|
|
134
|
+
// handler only serves degraded paths. Latest preview wins (mirroring the
|
|
135
|
+
// queue's pushOnce now:true semantics): a newer preview's fallback replaces
|
|
136
|
+
// an older preview's, but NEVER clobbers a non-preview standing handler
|
|
137
|
+
// (e.g. plan mode's approval handler).
|
|
138
|
+
const installFallback = (): void => {
|
|
139
|
+
if (!session.setStandingResolveHandler) return;
|
|
140
|
+
const existing = session.peekStandingResolveHandler?.();
|
|
141
|
+
if (existing === invoke) return;
|
|
142
|
+
if (existing !== undefined && !isPreviewResolveFallback(existing)) return;
|
|
143
|
+
session.setStandingResolveHandler(invoke);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const pushDirective = (): boolean => {
|
|
147
|
+
const { forced, exactNamed } = resolveForcedChoice();
|
|
148
|
+
if (!queue || !forced || !exactNamed) {
|
|
149
|
+
installFallback();
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
67
152
|
queue.pushOnce(forced, {
|
|
68
153
|
label: `pending-action:${options.sourceToolName}`,
|
|
69
154
|
now: true,
|
|
70
155
|
onRejected: () => "requeue",
|
|
71
|
-
onInvoked:
|
|
72
|
-
runResolveInvocation(input as ResolveParams, {
|
|
73
|
-
sourceToolName: options.sourceToolName,
|
|
74
|
-
label: options.label,
|
|
75
|
-
apply: options.apply,
|
|
76
|
-
reject: options.reject,
|
|
77
|
-
onApplyError: () => {
|
|
78
|
-
// Apply threw (e.g. ast_edit overlapping replacements). Re-push the
|
|
79
|
-
// same directive so the preview remains pending and the model can
|
|
80
|
-
// `discard` or fix-and-retry on the next turn instead of being
|
|
81
|
-
// stranded with no pending action to address.
|
|
82
|
-
pushDirective();
|
|
83
|
-
steerReminder();
|
|
84
|
-
},
|
|
85
|
-
}),
|
|
156
|
+
onInvoked: invoke,
|
|
86
157
|
});
|
|
158
|
+
// Forced directive may still be degraded mid-turn by a runtime
|
|
159
|
+
// incapability discovery; keep the fallback armed for that case.
|
|
160
|
+
installFallback();
|
|
161
|
+
return true;
|
|
87
162
|
};
|
|
88
163
|
|
|
89
164
|
pushDirective();
|
|
@@ -73,7 +73,11 @@ function renderSubagentSnapshot(
|
|
|
73
73
|
for (const al of snapshot.assignment.split("\n")) lines.push(` ${theme.fg("toolOutput", replaceTabs(al))}`);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
// Defense in depth: the producer only attaches `progress` when a live
|
|
77
|
+
// producer exists (subagent.ts #liveProgressFields), but the renderer
|
|
78
|
+
// also honors an explicit `liveProgressAvailable: false` so stale retained
|
|
79
|
+
// progress can never resurrect a live panel (AC5).
|
|
80
|
+
if (snapshot.progress && snapshot.liveProgressAvailable !== false) {
|
|
77
81
|
// Live streaming panel (full task-panel parity), indented under the header.
|
|
78
82
|
for (const pl of renderSubagentLiveProgress(snapshot.progress, expanded, theme, spinnerFrame)) {
|
|
79
83
|
lines.push(` ${pl}`);
|
|
@@ -142,6 +146,11 @@ export const subagentToolRenderer = {
|
|
|
142
146
|
);
|
|
143
147
|
|
|
144
148
|
const lines: string[] = [header];
|
|
149
|
+
// Discoverability: the inline panel is a bounded preview; the session
|
|
150
|
+
// observer (ctrl+s) streams the full per-subagent message history.
|
|
151
|
+
if (runningCount > 0) {
|
|
152
|
+
lines.push(` ${theme.fg("dim", "(ctrl+s to observe sessions)")}`);
|
|
153
|
+
}
|
|
145
154
|
for (const snapshot of subagents) {
|
|
146
155
|
lines.push(...renderSubagentSnapshot(snapshot, expanded, theme, options.spinnerFrame));
|
|
147
156
|
}
|
package/src/utils/edit-mode.ts
CHANGED
|
@@ -12,7 +12,7 @@ import titleSystemPrompt from "../prompts/system/title-system.md" with { type: "
|
|
|
12
12
|
|
|
13
13
|
const TITLE_SYSTEM_PROMPT = prompt.render(titleSystemPrompt);
|
|
14
14
|
|
|
15
|
-
const DEFAULT_TERMINAL_TITLE = "
|
|
15
|
+
const DEFAULT_TERMINAL_TITLE = "GJC";
|
|
16
16
|
const TERMINAL_TITLE_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
17
17
|
|
|
18
18
|
const MAX_INPUT_CHARS = 2000;
|
|
@@ -20,6 +20,13 @@ const TITLE_MAX_TOKENS = 30;
|
|
|
20
20
|
const REASONING_SAFE_MAX_TOKENS = 1024;
|
|
21
21
|
const SET_TITLE_TOOL_NAME = "set_title";
|
|
22
22
|
|
|
23
|
+
// Some models (notably cursor/composer-*) ignore the forced set_title tool call
|
|
24
|
+
// and instead emit a long free-text narrative. Without the tool call we fall back
|
|
25
|
+
// to the plain text, so cap its length: a real 3-6 word title never exceeds these.
|
|
26
|
+
// Beyond the cap we treat the response as a non-title hallucination and reject it.
|
|
27
|
+
const MAX_TITLE_CHARS = 80;
|
|
28
|
+
const MAX_TITLE_WORDS = 12;
|
|
29
|
+
|
|
23
30
|
const setTitleTool: Tool = {
|
|
24
31
|
name: SET_TITLE_TOOL_NAME,
|
|
25
32
|
description: "Set the generated session title.",
|
|
@@ -169,7 +176,14 @@ function extractGeneratedTitle(contentBlocks: AssistantMessage["content"]): stri
|
|
|
169
176
|
textTitle += content.text;
|
|
170
177
|
}
|
|
171
178
|
}
|
|
172
|
-
|
|
179
|
+
// Plain-text fallback (no set_title tool call): only accept it if it actually
|
|
180
|
+
// looks like a title. A model that ignored the tool and rambled produces a long
|
|
181
|
+
// blob — reject it so the caller falls back rather than persisting the narrative.
|
|
182
|
+
const trimmed = textTitle.trim();
|
|
183
|
+
if (trimmed.length > MAX_TITLE_CHARS || trimmed.split(/\s+/).length > MAX_TITLE_WORDS) {
|
|
184
|
+
return "";
|
|
185
|
+
}
|
|
186
|
+
return trimmed;
|
|
173
187
|
}
|
|
174
188
|
|
|
175
189
|
/**
|
package/src/utils/tool-choice.ts
CHANGED
|
@@ -1,33 +1,62 @@
|
|
|
1
|
-
import type { Api, Model, ToolChoice } from "@gajae-code/ai";
|
|
1
|
+
import type { Api, Model, ResolveToolChoiceResult, ToolChoice } from "@gajae-code/ai";
|
|
2
|
+
import { resolveToolChoice } from "@gajae-code/ai";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Build a provider-aware tool choice that targets one specific tool when supported.
|
|
5
6
|
* Providers that only expose required/any forcing may still honor named choices by
|
|
6
7
|
* narrowing their request tool list before transport.
|
|
7
8
|
*/
|
|
8
|
-
export
|
|
9
|
-
|
|
9
|
+
export interface NamedToolChoiceResult {
|
|
10
|
+
choice: ToolChoice | undefined;
|
|
11
|
+
exactNamed: boolean;
|
|
12
|
+
resolved?: ResolveToolChoiceResult;
|
|
13
|
+
}
|
|
10
14
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
15
|
+
export function buildNamedToolChoiceResult(toolName: string, model?: Model<Api>): NamedToolChoiceResult {
|
|
16
|
+
if (!model) return { choice: undefined, exactNamed: false };
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
let namedChoice: ToolChoice | undefined;
|
|
19
|
+
let namedShape = false;
|
|
20
|
+
|
|
21
|
+
if (model.api === "anthropic-messages" || model.api === "bedrock-converse-stream") {
|
|
22
|
+
namedChoice = { type: "tool", name: toolName };
|
|
23
|
+
namedShape = true;
|
|
24
|
+
} else if (
|
|
16
25
|
model.api === "openai-codex-responses" ||
|
|
17
26
|
model.api === "openai-responses" ||
|
|
18
27
|
model.api === "openai-completions" ||
|
|
19
|
-
model.api === "azure-openai-responses"
|
|
28
|
+
model.api === "azure-openai-responses" ||
|
|
29
|
+
model.api === "ollama-chat"
|
|
30
|
+
) {
|
|
31
|
+
namedChoice = { type: "function", name: toolName };
|
|
32
|
+
namedShape = true;
|
|
33
|
+
} else if (
|
|
34
|
+
model.api === "google-generative-ai" ||
|
|
35
|
+
model.api === "google-gemini-cli" ||
|
|
36
|
+
model.api === "google-vertex"
|
|
20
37
|
) {
|
|
21
|
-
|
|
38
|
+
namedChoice = "required";
|
|
22
39
|
}
|
|
23
40
|
|
|
24
|
-
if (
|
|
25
|
-
return { type: "function", name: toolName };
|
|
26
|
-
}
|
|
41
|
+
if (!namedChoice) return { choice: undefined, exactNamed: false };
|
|
27
42
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
43
|
+
const resolved = resolveToolChoice(model, namedChoice);
|
|
44
|
+
const exactNamed = namedShape && resolved.resolvedLevel === "named" && resolved.targetToolName === toolName;
|
|
45
|
+
return {
|
|
46
|
+
choice: exactNamed ? resolved.resolvedChoice : undefined,
|
|
47
|
+
exactNamed,
|
|
48
|
+
resolved,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
31
51
|
|
|
32
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Legacy capability-aware wrapper. May return a lossy `"required"` when named
|
|
54
|
+
* forcing degrades (e.g. Google APIs, or compat `toolChoiceSupport: "required"`),
|
|
55
|
+
* which forces *some* tool rather than `toolName` specifically. Queue directives
|
|
56
|
+
* that need exact tool identity (resolve / todo_write / yield) MUST use
|
|
57
|
+
* `buildNamedToolChoiceResult` and gate on `exactNamed` instead.
|
|
58
|
+
*/
|
|
59
|
+
export function buildNamedToolChoice(toolName: string, model?: Model<Api>): ToolChoice | undefined {
|
|
60
|
+
const result = buildNamedToolChoiceResult(toolName, model);
|
|
61
|
+
return result.choice ?? result.resolved?.resolvedChoice;
|
|
33
62
|
}
|