@gajae-code/coding-agent 0.4.3 → 0.4.5
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 +42 -0
- package/dist/types/async/job-manager.d.ts +19 -1
- package/dist/types/cli/fast-help.d.ts +1 -0
- package/dist/types/cli/setup-cli.d.ts +16 -1
- package/dist/types/commands/coordinator.d.ts +19 -0
- package/dist/types/commands/harness.d.ts +3 -0
- package/dist/types/commands/mcp-serve.d.ts +24 -0
- package/dist/types/commands/setup.d.ts +47 -0
- package/dist/types/config/model-registry.d.ts +3 -0
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/coordinator/contract.d.ts +4 -0
- package/dist/types/coordinator-mcp/policy.d.ts +24 -0
- package/dist/types/coordinator-mcp/safety.d.ts +26 -0
- package/dist/types/coordinator-mcp/server.d.ts +58 -0
- package/dist/types/extensibility/extensions/types.d.ts +13 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
- package/dist/types/harness-control-plane/finalize.d.ts +5 -0
- 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/receipts.d.ts +46 -0
- package/dist/types/harness-control-plane/rpc-adapter.d.ts +3 -0
- package/dist/types/harness-control-plane/types.d.ts +9 -1
- package/dist/types/main.d.ts +2 -2
- package/dist/types/modes/components/hook-selector.d.ts +11 -0
- package/dist/types/modes/utils/abort-message.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +8 -0
- package/dist/types/setup/hermes-setup.d.ts +78 -0
- package/dist/types/task/fork-context-advisory.d.ts +13 -0
- package/dist/types/task/receipt.d.ts +1 -0
- package/dist/types/task/render.d.ts +7 -1
- package/dist/types/task/roi-reconciliation.d.ts +27 -0
- package/dist/types/task/types.d.ts +10 -0
- package/dist/types/tools/subagent-render.d.ts +25 -0
- package/dist/types/tools/subagent.d.ts +5 -1
- package/package.json +8 -7
- package/scripts/build-binary.ts +4 -0
- package/src/async/job-manager.ts +43 -1
- package/src/cli/fast-help.ts +80 -0
- package/src/cli/setup-cli.ts +95 -2
- package/src/cli.ts +109 -16
- package/src/commands/coordinator.ts +113 -0
- package/src/commands/harness.ts +92 -9
- package/src/commands/mcp-serve.ts +63 -0
- package/src/commands/setup.ts +34 -1
- package/src/config/models-config-schema.ts +1 -0
- package/src/coordinator/contract.ts +21 -0
- package/src/coordinator-mcp/policy.ts +160 -0
- package/src/coordinator-mcp/safety.ts +80 -0
- package/src/coordinator-mcp/server.ts +1519 -0
- package/src/cursor.ts +30 -2
- package/src/extensibility/extensions/types.ts +13 -0
- package/src/gjc-runtime/launch-worktree.ts +12 -1
- package/src/gjc-runtime/session-state-sidecar.ts +117 -0
- package/src/harness-control-plane/finalize.ts +39 -5
- package/src/harness-control-plane/owner.ts +9 -1
- 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/receipts.ts +229 -1
- package/src/harness-control-plane/rpc-adapter.ts +8 -0
- package/src/harness-control-plane/types.ts +29 -1
- package/src/internal-urls/docs-index.generated.ts +6 -4
- package/src/main.ts +7 -3
- package/src/modes/components/hook-selector.ts +109 -5
- package/src/modes/components/status-line.ts +6 -6
- package/src/modes/controllers/event-controller.ts +5 -4
- package/src/modes/controllers/extension-ui-controller.ts +16 -1
- package/src/modes/interactive-mode.ts +4 -5
- package/src/modes/print-mode.ts +1 -1
- 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/prompts/agents/architect.md +6 -0
- package/src/prompts/agents/critic.md +6 -0
- package/src/prompts/agents/planner.md +8 -1
- package/src/sdk.ts +9 -4
- package/src/session/agent-session.ts +22 -5
- package/src/session/session-manager.ts +20 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +30 -0
- package/src/setup/hermes-setup.ts +484 -0
- package/src/task/fork-context-advisory.ts +99 -0
- package/src/task/index.ts +33 -2
- package/src/task/receipt.ts +2 -0
- package/src/task/render.ts +14 -0
- package/src/task/roi-reconciliation.ts +90 -0
- package/src/task/types.ts +7 -0
- package/src/tools/ask.ts +30 -10
- package/src/tools/index.ts +2 -2
- package/src/tools/renderers.ts +2 -0
- package/src/tools/subagent-render.ts +169 -0
- package/src/tools/subagent.ts +49 -7
- package/src/utils/title-generator.ts +16 -2
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { ForkContextMode } from "./types";
|
|
2
|
+
|
|
3
|
+
export interface ForkContextAdvisory {
|
|
4
|
+
recommendedMode: ForkContextMode;
|
|
5
|
+
reasons: string[];
|
|
6
|
+
estimatedClonedTokens: Record<ForkContextMode, number>;
|
|
7
|
+
callerModeRespected: true;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Per-mode clone budget ceilings (advisory estimates only). These bound how
|
|
12
|
+
* many parent-context tokens each mode may clone into the child; the actual
|
|
13
|
+
* cloned amount can never exceed the parent context itself.
|
|
14
|
+
*/
|
|
15
|
+
const CLONE_BUDGET_BY_MODE = {
|
|
16
|
+
none: 0,
|
|
17
|
+
receipt: 2000,
|
|
18
|
+
"last-turn": 4000,
|
|
19
|
+
bounded: 8000,
|
|
20
|
+
full: 15000,
|
|
21
|
+
} as const satisfies Record<ForkContextMode, number>;
|
|
22
|
+
|
|
23
|
+
const RECEIPT_TRIGGERS = [
|
|
24
|
+
{ pattern: /as discussed/i, reason: "prior-session-reference:as-discussed" },
|
|
25
|
+
{ pattern: /as decided/i, reason: "prior-session-reference:as-decided" },
|
|
26
|
+
{ pattern: /earlier in this session/i, reason: "prior-session-reference:earlier-in-this-session" },
|
|
27
|
+
{ pattern: /per the plan above/i, reason: "prior-session-reference:per-the-plan-above" },
|
|
28
|
+
{ pattern: /the previous review/i, reason: "prior-session-reference:the-previous-review" },
|
|
29
|
+
{ pattern: /\.gjc\/plans\//i, reason: "prior-session-reference:gjc-plans-path" },
|
|
30
|
+
{ pattern: /\.gjc\/specs\//i, reason: "prior-session-reference:gjc-specs-path" },
|
|
31
|
+
] as const;
|
|
32
|
+
|
|
33
|
+
const LAST_TURN_TRIGGERS = [
|
|
34
|
+
{ pattern: /the last message/i, reason: "last-turn-reference:the-last-message" },
|
|
35
|
+
{ pattern: /the previous turn/i, reason: "last-turn-reference:the-previous-turn" },
|
|
36
|
+
{ pattern: /see above/i, reason: "last-turn-reference:see-above" },
|
|
37
|
+
] as const;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Estimated tokens cloned into the child per mode: the per-mode budget
|
|
41
|
+
* ceiling, capped by the actual parent context (you can never clone more
|
|
42
|
+
* than exists). Negative parent contexts are normalized to 0.
|
|
43
|
+
*/
|
|
44
|
+
function estimateClonedTokens(parentContextTokens: number): Record<ForkContextMode, number> {
|
|
45
|
+
const parent = Math.max(0, parentContextTokens);
|
|
46
|
+
return {
|
|
47
|
+
none: Math.min(parent, CLONE_BUDGET_BY_MODE.none),
|
|
48
|
+
receipt: Math.min(parent, CLONE_BUDGET_BY_MODE.receipt),
|
|
49
|
+
"last-turn": Math.min(parent, CLONE_BUDGET_BY_MODE["last-turn"]),
|
|
50
|
+
bounded: Math.min(parent, CLONE_BUDGET_BY_MODE.bounded),
|
|
51
|
+
full: Math.min(parent, CLONE_BUDGET_BY_MODE.full),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function adviseForkContextMode(input: {
|
|
56
|
+
assignment: string;
|
|
57
|
+
context?: string;
|
|
58
|
+
explicitMode?: ForkContextMode;
|
|
59
|
+
parentContextTokens?: number;
|
|
60
|
+
}): ForkContextAdvisory {
|
|
61
|
+
const parentContextTokens = input.parentContextTokens ?? 0;
|
|
62
|
+
const estimatedClonedTokens = estimateClonedTokens(parentContextTokens);
|
|
63
|
+
|
|
64
|
+
if (input.explicitMode !== undefined) {
|
|
65
|
+
return {
|
|
66
|
+
recommendedMode: input.explicitMode,
|
|
67
|
+
reasons: ["explicit-caller-mode"],
|
|
68
|
+
estimatedClonedTokens,
|
|
69
|
+
callerModeRespected: true,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const text = `${input.assignment}\n${input.context ?? ""}`;
|
|
74
|
+
const reasons: string[] = [];
|
|
75
|
+
let recommendedMode: ForkContextMode = "none";
|
|
76
|
+
|
|
77
|
+
for (const trigger of LAST_TURN_TRIGGERS) {
|
|
78
|
+
if (trigger.pattern.test(text)) {
|
|
79
|
+
reasons.push(trigger.reason);
|
|
80
|
+
recommendedMode = "last-turn";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const trigger of RECEIPT_TRIGGERS) {
|
|
85
|
+
if (trigger.pattern.test(text)) {
|
|
86
|
+
reasons.push(trigger.reason);
|
|
87
|
+
if (recommendedMode === "none") {
|
|
88
|
+
recommendedMode = "receipt";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
recommendedMode,
|
|
95
|
+
reasons,
|
|
96
|
+
estimatedClonedTokens,
|
|
97
|
+
callerModeRespected: true,
|
|
98
|
+
};
|
|
99
|
+
}
|
package/src/task/index.ts
CHANGED
|
@@ -47,11 +47,13 @@ import { generateCommitMessage } from "../utils/commit-message-generator";
|
|
|
47
47
|
import * as git from "../utils/git";
|
|
48
48
|
import { discoverAgents, filterVisibleAgents, getAgent } from "./discovery";
|
|
49
49
|
import { runSubprocess } from "./executor";
|
|
50
|
+
import { adviseForkContextMode } from "./fork-context-advisory";
|
|
50
51
|
import { getTaskIdValidationError, validateAllocatedTaskId } from "./id";
|
|
51
52
|
import { AgentOutputManager } from "./output-manager";
|
|
52
53
|
import { mapWithConcurrencyLimit, Semaphore } from "./parallel";
|
|
53
54
|
import { assertNoRawTaskFields, buildTaskReceipt, buildTaskRoiSummary } from "./receipt";
|
|
54
55
|
import { renderResult, renderCall as renderTaskCall } from "./render";
|
|
56
|
+
import { reconcileSpawnRoi } from "./roi-reconciliation";
|
|
55
57
|
import { getTaskSimpleModeCapabilities, type TaskSimpleMode } from "./simple-mode";
|
|
56
58
|
import { DEFAULT_SPAWN_THRESHOLD, evaluateReviewerExploreGate, evaluateSpawnGate } from "./spawn-gate";
|
|
57
59
|
import {
|
|
@@ -578,6 +580,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
578
580
|
runMode: message ? "message" : "resume",
|
|
579
581
|
resumeMessage: message,
|
|
580
582
|
sessionFiles: new Map([[descriptor.task.id, descriptor.sessionFile]]),
|
|
583
|
+
suppressRoiReconciliation: true,
|
|
581
584
|
},
|
|
582
585
|
);
|
|
583
586
|
const finalText = result.content.find(part => part.type === "text")?.text ?? "(no output)";
|
|
@@ -675,6 +678,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
675
678
|
frozenForkSeeds,
|
|
676
679
|
{
|
|
677
680
|
sessionFiles: new Map([[uniqueId, subtaskSessionFile]]),
|
|
681
|
+
suppressRoiReconciliation: true,
|
|
678
682
|
},
|
|
679
683
|
);
|
|
680
684
|
const finalText = result.content.find(part => part.type === "text")?.text ?? "(no output)";
|
|
@@ -874,6 +878,12 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
874
878
|
runMode?: "initial" | "resume" | "message";
|
|
875
879
|
resumeMessage?: string;
|
|
876
880
|
sessionFiles?: ReadonlyMap<string, string | null>;
|
|
881
|
+
/**
|
|
882
|
+
* Set for per-child async runs: the spawnPlan is carried for gate
|
|
883
|
+
* consistency, but batch-level ROI reconciliation must not be computed
|
|
884
|
+
* against a single child's receipts.
|
|
885
|
+
*/
|
|
886
|
+
suppressRoiReconciliation?: boolean;
|
|
877
887
|
},
|
|
878
888
|
): Promise<AgentToolResult<TaskToolDetails>> {
|
|
879
889
|
const startTime = Date.now();
|
|
@@ -1293,6 +1303,17 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1293
1303
|
const forkContext = requestsForkContext(task)
|
|
1294
1304
|
? { mode: task.inheritContext, clonedTokens: forkContextSeed?.metadata.approximateTokens ?? 0 }
|
|
1295
1305
|
: undefined;
|
|
1306
|
+
// Advisory-only recommendation (logged on the receipt); never overrides
|
|
1307
|
+
// the caller's explicit inheritContext mode.
|
|
1308
|
+
const advisory = adviseForkContextMode({
|
|
1309
|
+
assignment: task.assignment,
|
|
1310
|
+
context: sharedContext,
|
|
1311
|
+
explicitMode: task.inheritContext,
|
|
1312
|
+
});
|
|
1313
|
+
const forkContextAdvisory = {
|
|
1314
|
+
recommendedMode: advisory.recommendedMode,
|
|
1315
|
+
reasons: advisory.reasons,
|
|
1316
|
+
};
|
|
1296
1317
|
const taskSessionFile = overrides?.sessionFile ?? executionOverrides?.sessionFiles?.get(task.id) ?? null;
|
|
1297
1318
|
if (!isIsolated) {
|
|
1298
1319
|
const result = await runSubprocess({
|
|
@@ -1324,6 +1345,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1324
1345
|
progressMap.set(index, {
|
|
1325
1346
|
...structuredClone(progress),
|
|
1326
1347
|
});
|
|
1348
|
+
AsyncJobManager.instance()?.recordSubagentProgress(task.id, progress);
|
|
1327
1349
|
emitProgress();
|
|
1328
1350
|
},
|
|
1329
1351
|
authStorage: this.session.authStorage,
|
|
@@ -1340,7 +1362,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1340
1362
|
parentTelemetry: this.session.getTelemetry?.(),
|
|
1341
1363
|
forkContextSeed,
|
|
1342
1364
|
});
|
|
1343
|
-
return
|
|
1365
|
+
return { ...result, ...(forkContext ? { forkContext } : {}), forkContextAdvisory };
|
|
1344
1366
|
}
|
|
1345
1367
|
|
|
1346
1368
|
const taskStart = Date.now();
|
|
@@ -1384,6 +1406,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1384
1406
|
progressMap.set(index, {
|
|
1385
1407
|
...structuredClone(progress),
|
|
1386
1408
|
});
|
|
1409
|
+
AsyncJobManager.instance()?.recordSubagentProgress(task.id, progress);
|
|
1387
1410
|
emitProgress();
|
|
1388
1411
|
},
|
|
1389
1412
|
authStorage: this.session.authStorage,
|
|
@@ -1400,7 +1423,11 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1400
1423
|
parentTelemetry: this.session.getTelemetry?.(),
|
|
1401
1424
|
forkContextSeed,
|
|
1402
1425
|
});
|
|
1403
|
-
const resultWithForkContext =
|
|
1426
|
+
const resultWithForkContext = {
|
|
1427
|
+
...result,
|
|
1428
|
+
...(forkContext ? { forkContext } : {}),
|
|
1429
|
+
forkContextAdvisory,
|
|
1430
|
+
};
|
|
1404
1431
|
if (mergeMode === "branch" && resultWithForkContext.exitCode === 0) {
|
|
1405
1432
|
try {
|
|
1406
1433
|
const commitMsg =
|
|
@@ -1695,6 +1722,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1695
1722
|
|
|
1696
1723
|
const receipts = results.map(buildTaskReceipt);
|
|
1697
1724
|
const roiSummary = buildTaskRoiSummary(receipts);
|
|
1725
|
+
const roiReconciliation = executionOverrides?.suppressRoiReconciliation
|
|
1726
|
+
? undefined
|
|
1727
|
+
: reconcileSpawnRoi(params.spawnPlan, receipts);
|
|
1698
1728
|
const summaries = receipts.map(r => {
|
|
1699
1729
|
const status = r.status === "merge_failed" ? "merge failed" : r.status;
|
|
1700
1730
|
return {
|
|
@@ -1737,6 +1767,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1737
1767
|
usage: hasAggregatedUsage ? aggregatedUsage : undefined,
|
|
1738
1768
|
forkContextClonedTokens: forkContextClonedTokens > 0 ? forkContextClonedTokens : undefined,
|
|
1739
1769
|
roiSummary,
|
|
1770
|
+
roiReconciliation,
|
|
1740
1771
|
};
|
|
1741
1772
|
assertNoRawTaskFields(details, "task.return.details");
|
|
1742
1773
|
return {
|
package/src/task/receipt.ts
CHANGED
|
@@ -46,6 +46,7 @@ export interface TaskResultReceipt {
|
|
|
46
46
|
};
|
|
47
47
|
extractedToolCounts?: Record<string, number>;
|
|
48
48
|
forkContext?: SingleResult["forkContext"];
|
|
49
|
+
forkContextAdvisory?: SingleResult["forkContextAdvisory"];
|
|
49
50
|
roi?: TaskRoi;
|
|
50
51
|
}
|
|
51
52
|
|
|
@@ -234,6 +235,7 @@ export function buildTaskReceipt(raw: SingleResult): TaskResultReceipt {
|
|
|
234
235
|
review: buildReview(raw),
|
|
235
236
|
extractedToolCounts,
|
|
236
237
|
forkContext: raw.forkContext,
|
|
238
|
+
forkContextAdvisory: raw.forkContextAdvisory,
|
|
237
239
|
roi: buildTaskRoi(raw),
|
|
238
240
|
};
|
|
239
241
|
}
|
package/src/task/render.ts
CHANGED
|
@@ -690,6 +690,20 @@ function renderAgentProgress(
|
|
|
690
690
|
return lines;
|
|
691
691
|
}
|
|
692
692
|
|
|
693
|
+
/**
|
|
694
|
+
* Public wrapper to render a single subagent's live `AgentProgress` for the
|
|
695
|
+
* `subagent` await panel. Reuses the internal task-progress renderer so the
|
|
696
|
+
* await panel stays at parity with the inline task panel.
|
|
697
|
+
*/
|
|
698
|
+
export function renderSubagentLiveProgress(
|
|
699
|
+
progress: AgentProgress,
|
|
700
|
+
expanded: boolean,
|
|
701
|
+
theme: Theme,
|
|
702
|
+
spinnerFrame?: number,
|
|
703
|
+
): string[] {
|
|
704
|
+
return renderAgentProgress(progress, true, expanded, theme, spinnerFrame);
|
|
705
|
+
}
|
|
706
|
+
|
|
693
707
|
/**
|
|
694
708
|
* Render review result with combined verdict + findings in tree structure.
|
|
695
709
|
*/
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { TaskResultReceipt } from "./receipt";
|
|
2
|
+
import type { SpawnPlanReceipt } from "./spawn-gate";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pure, advisory-only reconciliation between a spawn plan's inline-token promise
|
|
6
|
+
* and receipt-safe child outputs. These signals never change task success/failure
|
|
7
|
+
* semantics or runtime behavior; they only describe budget/ROI observations for
|
|
8
|
+
* model-facing summaries.
|
|
9
|
+
*/
|
|
10
|
+
export interface SpawnRoiChildReconciliation {
|
|
11
|
+
id: string;
|
|
12
|
+
inlineTokens: number;
|
|
13
|
+
maxInlineTokens: number;
|
|
14
|
+
overBudget: boolean;
|
|
15
|
+
overageTokens: number;
|
|
16
|
+
lowRoi: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SpawnRoiReconciliation {
|
|
20
|
+
childCount: number;
|
|
21
|
+
promisedMaxInlineTokens: number;
|
|
22
|
+
children: SpawnRoiChildReconciliation[];
|
|
23
|
+
overBudgetChildIds: string[];
|
|
24
|
+
lowRoiChildIds: string[];
|
|
25
|
+
totalInlineTokens: number;
|
|
26
|
+
totalOverageTokens: number;
|
|
27
|
+
advisoryFlags: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function estimateTextTokens(text: string | undefined): number {
|
|
31
|
+
if (!text) return 0;
|
|
32
|
+
return Math.ceil(text.length / 4);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Estimate model-facing inline cost from receipt-safe fields only. The proxy uses
|
|
37
|
+
* one token per four characters, rounded up independently for the preview and
|
|
38
|
+
* review summaries that may be inlined for the parent model.
|
|
39
|
+
*/
|
|
40
|
+
function estimateInlineTokens(receipt: TaskResultReceipt): number {
|
|
41
|
+
const review = receipt.review;
|
|
42
|
+
const reviewSummaryChars =
|
|
43
|
+
(review?.overallCorrectness?.length ?? 0) +
|
|
44
|
+
(review?.findings?.reduce((total, finding) => total + finding.summary.length, 0) ?? 0);
|
|
45
|
+
return estimateTextTokens(receipt.preview) + Math.ceil(reviewSummaryChars / 4);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function reconcileSpawnRoi(
|
|
49
|
+
plan: SpawnPlanReceipt | undefined,
|
|
50
|
+
receipts: readonly TaskResultReceipt[],
|
|
51
|
+
): SpawnRoiReconciliation | undefined {
|
|
52
|
+
if (!plan) return undefined;
|
|
53
|
+
|
|
54
|
+
const children = receipts.map(receipt => {
|
|
55
|
+
const inlineTokens = estimateInlineTokens(receipt);
|
|
56
|
+
const overageTokens = Math.max(0, inlineTokens - plan.maxInlineTokens);
|
|
57
|
+
return {
|
|
58
|
+
id: receipt.id,
|
|
59
|
+
inlineTokens,
|
|
60
|
+
maxInlineTokens: plan.maxInlineTokens,
|
|
61
|
+
overBudget: overageTokens > 0,
|
|
62
|
+
overageTokens,
|
|
63
|
+
lowRoi: Boolean(receipt.roi?.lowRoi),
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
const overBudgetChildIds = children
|
|
67
|
+
.filter(child => child.overBudget)
|
|
68
|
+
.map(child => child.id)
|
|
69
|
+
.toSorted();
|
|
70
|
+
const lowRoiChildIds = children
|
|
71
|
+
.filter(child => child.lowRoi)
|
|
72
|
+
.map(child => child.id)
|
|
73
|
+
.toSorted();
|
|
74
|
+
const advisoryFlags = [
|
|
75
|
+
...(overBudgetChildIds.length > 0 ? ["over-inline-budget"] : []),
|
|
76
|
+
...(lowRoiChildIds.length > 0 ? ["low-roi-children"] : []),
|
|
77
|
+
...(children.length > 0 && lowRoiChildIds.length === children.length ? ["all-children-low-roi"] : []),
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
childCount: children.length,
|
|
82
|
+
promisedMaxInlineTokens: plan.maxInlineTokens,
|
|
83
|
+
children,
|
|
84
|
+
overBudgetChildIds,
|
|
85
|
+
lowRoiChildIds,
|
|
86
|
+
totalInlineTokens: children.reduce((total, child) => total + child.inlineTokens, 0),
|
|
87
|
+
totalOverageTokens: children.reduce((total, child) => total + child.overageTokens, 0),
|
|
88
|
+
advisoryFlags,
|
|
89
|
+
};
|
|
90
|
+
}
|
package/src/task/types.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { $env } from "@gajae-code/utils";
|
|
|
4
4
|
import * as z from "zod/v4";
|
|
5
5
|
import { isValidTaskId, TASK_ID_DESCRIPTION } from "./id";
|
|
6
6
|
import type { TaskResultReceipt } from "./receipt";
|
|
7
|
+
import type { SpawnRoiReconciliation } from "./roi-reconciliation";
|
|
7
8
|
import { getTaskSimpleModeCapabilities, type TaskSimpleMode } from "./simple-mode";
|
|
8
9
|
import type { SpawnPlanReceipt } from "./spawn-gate";
|
|
9
10
|
import type { NestedRepoPatch } from "./worktree";
|
|
@@ -337,6 +338,11 @@ export interface SingleResult {
|
|
|
337
338
|
outputMeta?: { lineCount: number; charCount: number; byteSize?: number; sha256?: string };
|
|
338
339
|
/** Fork-context seed accounting for this subagent, when inherited parent context was cloned. */
|
|
339
340
|
forkContext?: { mode: ForkContextMode; clonedTokens: number };
|
|
341
|
+
/**
|
|
342
|
+
* Advisory fork-context mode recommendation for this task (logged only;
|
|
343
|
+
* never changes the actual mode selection).
|
|
344
|
+
*/
|
|
345
|
+
forkContextAdvisory?: { recommendedMode: ForkContextMode; reasons: string[] };
|
|
340
346
|
}
|
|
341
347
|
|
|
342
348
|
/** Tool details for TUI rendering */
|
|
@@ -356,6 +362,7 @@ export interface TaskToolDetails {
|
|
|
356
362
|
/** Advisory ids for terminal children that spent tokens without detectable output/review/changes. */
|
|
357
363
|
lowRoiChildIds: string[];
|
|
358
364
|
};
|
|
365
|
+
roiReconciliation?: SpawnRoiReconciliation;
|
|
359
366
|
progress?: AgentProgress[];
|
|
360
367
|
async?: {
|
|
361
368
|
state: "running" | "paused" | "queued" | "completed" | "failed";
|
package/src/tools/ask.ts
CHANGED
|
@@ -175,6 +175,7 @@ interface UIContext {
|
|
|
175
175
|
onLeft?: () => void;
|
|
176
176
|
onRight?: () => void;
|
|
177
177
|
helpText?: string;
|
|
178
|
+
customInput?: { optionLabel: string; onSubmit: (text: string) => void };
|
|
178
179
|
},
|
|
179
180
|
): Promise<string | undefined>;
|
|
180
181
|
editor(
|
|
@@ -194,6 +195,7 @@ async function askSingleQuestion(
|
|
|
194
195
|
): Promise<SelectionResult> {
|
|
195
196
|
const { recommended, timeout, signal, initialSelection, navigation, scrollTitleRows } = options;
|
|
196
197
|
const doneLabel = getDoneOptionLabel();
|
|
198
|
+
const otherOptionLabel = options.otherOptionLabel ?? OTHER_OPTION;
|
|
197
199
|
let selectedOptions = [...(initialSelection?.selectedOptions ?? [])];
|
|
198
200
|
let customInput = initialSelection?.customInput;
|
|
199
201
|
let timedOut = false;
|
|
@@ -202,11 +204,20 @@ async function askSingleQuestion(
|
|
|
202
204
|
prompt: string,
|
|
203
205
|
optionsToShow: string[],
|
|
204
206
|
initialIndex?: number,
|
|
205
|
-
): Promise<{
|
|
207
|
+
): Promise<{
|
|
208
|
+
choice: string | undefined;
|
|
209
|
+
timedOut: boolean;
|
|
210
|
+
navigation?: "back" | "forward";
|
|
211
|
+
inlineInput?: string;
|
|
212
|
+
}> => {
|
|
206
213
|
let timeoutTriggered = false;
|
|
207
214
|
const onTimeout = () => {
|
|
208
215
|
timeoutTriggered = true;
|
|
209
216
|
};
|
|
217
|
+
// Inline custom input: the TUI selector keeps the question and option
|
|
218
|
+
// list on screen and collects the "Other" text below the list, instead
|
|
219
|
+
// of swapping to a separate editor screen that hides the question.
|
|
220
|
+
let inlineInput: string | undefined;
|
|
210
221
|
let navigationAction: "back" | "forward" | undefined;
|
|
211
222
|
const baseHelpText = navigation
|
|
212
223
|
? "up/down navigate enter select ←/→ question esc cancel"
|
|
@@ -222,6 +233,12 @@ async function askSingleQuestion(
|
|
|
222
233
|
scrollTitleRows,
|
|
223
234
|
onTimeout,
|
|
224
235
|
helpText,
|
|
236
|
+
customInput: {
|
|
237
|
+
optionLabel: otherOptionLabel,
|
|
238
|
+
onSubmit: (text: string) => {
|
|
239
|
+
inlineInput = text;
|
|
240
|
+
},
|
|
241
|
+
},
|
|
225
242
|
onLeft: navigation?.allowBack
|
|
226
243
|
? () => {
|
|
227
244
|
navigationAction = "back";
|
|
@@ -240,16 +257,17 @@ async function askSingleQuestion(
|
|
|
240
257
|
if (!timeoutTriggered && choice === undefined && typeof timeout === "number") {
|
|
241
258
|
timeoutTriggered = Date.now() - startMs >= timeout;
|
|
242
259
|
}
|
|
243
|
-
return { choice, timedOut: timeoutTriggered, navigation: navigationAction };
|
|
260
|
+
return { choice, timedOut: timeoutTriggered, navigation: navigationAction, inlineInput };
|
|
244
261
|
};
|
|
245
262
|
|
|
263
|
+
// Fallback for UI contexts that don't support inline custom input (they
|
|
264
|
+
// resolve the "Other" label without invoking customInput.onSubmit).
|
|
246
265
|
const promptForCustomInput = async (): Promise<{ input: string | undefined }> => {
|
|
247
266
|
const dialogOptions = signal ? { signal } : undefined;
|
|
248
267
|
const showCustomInput = () => ui.editor("Enter your response:", undefined, dialogOptions, { promptStyle: true });
|
|
249
268
|
const input = signal ? await untilAborted(signal, showCustomInput) : await showCustomInput();
|
|
250
269
|
return { input };
|
|
251
270
|
};
|
|
252
|
-
const otherOptionLabel = options.otherOptionLabel ?? OTHER_OPTION;
|
|
253
271
|
|
|
254
272
|
const promptWithProgress = navigation?.progressText ? `${question} (${navigation.progressText})` : question;
|
|
255
273
|
if (multi) {
|
|
@@ -278,6 +296,7 @@ async function askSingleQuestion(
|
|
|
278
296
|
choice,
|
|
279
297
|
timedOut: selectTimedOut,
|
|
280
298
|
navigation: arrowNavigation,
|
|
299
|
+
inlineInput,
|
|
281
300
|
} = await selectOption(`${prefix}${promptWithProgress}`, opts, cursorIndex);
|
|
282
301
|
|
|
283
302
|
if (arrowNavigation) {
|
|
@@ -297,11 +316,11 @@ async function askSingleQuestion(
|
|
|
297
316
|
timedOut = true;
|
|
298
317
|
break;
|
|
299
318
|
}
|
|
300
|
-
const
|
|
301
|
-
if (
|
|
319
|
+
const input = inlineInput !== undefined ? inlineInput : (await promptForCustomInput()).input;
|
|
320
|
+
if (input === undefined) {
|
|
302
321
|
break;
|
|
303
322
|
}
|
|
304
|
-
customInput =
|
|
323
|
+
customInput = input;
|
|
305
324
|
break;
|
|
306
325
|
}
|
|
307
326
|
|
|
@@ -353,6 +372,7 @@ async function askSingleQuestion(
|
|
|
353
372
|
choice,
|
|
354
373
|
timedOut: selectTimedOut,
|
|
355
374
|
navigation: arrowNavigation,
|
|
375
|
+
inlineInput,
|
|
356
376
|
} = await selectOption(promptWithProgress, optionsWithNavigation, initialIndex);
|
|
357
377
|
timedOut = selectTimedOut;
|
|
358
378
|
|
|
@@ -365,12 +385,12 @@ async function askSingleQuestion(
|
|
|
365
385
|
}
|
|
366
386
|
} else if (choice === otherOptionLabel) {
|
|
367
387
|
if (!selectTimedOut) {
|
|
368
|
-
const
|
|
369
|
-
if (
|
|
370
|
-
customInput =
|
|
388
|
+
const input = inlineInput !== undefined ? inlineInput : (await promptForCustomInput()).input;
|
|
389
|
+
if (input !== undefined) {
|
|
390
|
+
customInput = input;
|
|
371
391
|
selectedOptions = [];
|
|
372
392
|
}
|
|
373
|
-
// If
|
|
393
|
+
// If input was dismissed (undefined), keep prior selectedOptions/customInput intact
|
|
374
394
|
}
|
|
375
395
|
} else {
|
|
376
396
|
selectedOptions = [stripRecommendedSuffix(choice)];
|
package/src/tools/index.ts
CHANGED
|
@@ -403,7 +403,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
403
403
|
toolNames && toolNames.length > 0 ? [...new Set(toolNames.map(name => name.toLowerCase()))] : undefined;
|
|
404
404
|
const goalEnabled = session.settings.get("goal.enabled");
|
|
405
405
|
const goalStateToolNames = [...GOAL_MODE_TOOL_NAMES];
|
|
406
|
-
if (goalEnabled &&
|
|
406
|
+
if (goalEnabled && requestedTools && !requestedTools.includes("goal")) {
|
|
407
407
|
requestedTools = [...requestedTools, "goal"];
|
|
408
408
|
}
|
|
409
409
|
if (goalEnabled && requestedTools) {
|
|
@@ -482,7 +482,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
482
482
|
allToolsByRequestName.set(name.toLowerCase(), [name, factory]);
|
|
483
483
|
}
|
|
484
484
|
const isToolAllowed = (name: string) => {
|
|
485
|
-
if (name === "goal") return goalEnabled
|
|
485
|
+
if (name === "goal") return goalEnabled;
|
|
486
486
|
if (goalStateToolNames.includes(name as (typeof GOAL_MODE_TOOL_NAMES)[number])) return goalEnabled;
|
|
487
487
|
if (name === "lsp") return enableLsp && session.settings.get("lsp.enabled");
|
|
488
488
|
if (name === "bash") return true;
|
package/src/tools/renderers.ts
CHANGED
|
@@ -29,6 +29,7 @@ import { resolveToolRenderer } from "./resolve";
|
|
|
29
29
|
import { searchToolRenderer } from "./search";
|
|
30
30
|
import { searchToolBm25Renderer } from "./search-tool-bm25";
|
|
31
31
|
import { sshToolRenderer } from "./ssh";
|
|
32
|
+
import { subagentToolRenderer } from "./subagent-render";
|
|
32
33
|
import { todoWriteToolRenderer } from "./todo-write";
|
|
33
34
|
import { writeToolRenderer } from "./write";
|
|
34
35
|
|
|
@@ -66,6 +67,7 @@ export const toolRenderers: Record<string, ToolRenderer> = {
|
|
|
66
67
|
resolve: resolveToolRenderer as ToolRenderer,
|
|
67
68
|
search_tool_bm25: searchToolBm25Renderer as ToolRenderer,
|
|
68
69
|
ssh: sshToolRenderer as ToolRenderer,
|
|
70
|
+
subagent: subagentToolRenderer as ToolRenderer,
|
|
69
71
|
task: taskToolRenderer as ToolRenderer,
|
|
70
72
|
todo_write: todoWriteToolRenderer as ToolRenderer,
|
|
71
73
|
github: githubToolRenderer as ToolRenderer,
|