@gajae-code/coding-agent 0.4.4 → 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 +40 -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 +3 -0
- package/dist/types/commands/setup.d.ts +6 -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 +1 -1
- package/dist/types/coordinator-mcp/server.d.ts +8 -2
- 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/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 +7 -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/roi-reconciliation.d.ts +27 -0
- package/dist/types/task/types.d.ts +10 -0
- 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 +107 -16
- package/src/commands/coordinator.ts +44 -1
- package/src/commands/harness.ts +92 -9
- package/src/commands/mcp-serve.ts +3 -2
- package/src/commands/setup.ts +4 -0
- package/src/config/models-config-schema.ts +1 -0
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/server.ts +385 -182
- package/src/cursor.ts +30 -2
- package/src/gjc-runtime/launch-worktree.ts +12 -1
- package/src/gjc-runtime/session-state-sidecar.ts +38 -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 -5
- package/src/main.ts +7 -3
- package/src/modes/components/status-line.ts +6 -6
- package/src/modes/controllers/event-controller.ts +5 -4
- 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/sdk.ts +9 -4
- package/src/session/agent-session.ts +16 -5
- package/src/session/session-manager.ts +20 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +3 -2
- package/src/setup/hermes-setup.ts +63 -8
- package/src/task/fork-context-advisory.ts +99 -0
- package/src/task/index.ts +31 -2
- package/src/task/receipt.ts +2 -0
- package/src/task/roi-reconciliation.ts +90 -0
- package/src/task/types.ts +7 -0
- package/src/tools/index.ts +2 -2
- package/src/tools/subagent-render.ts +10 -1
- 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({
|
|
@@ -1341,7 +1362,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1341
1362
|
parentTelemetry: this.session.getTelemetry?.(),
|
|
1342
1363
|
forkContextSeed,
|
|
1343
1364
|
});
|
|
1344
|
-
return
|
|
1365
|
+
return { ...result, ...(forkContext ? { forkContext } : {}), forkContextAdvisory };
|
|
1345
1366
|
}
|
|
1346
1367
|
|
|
1347
1368
|
const taskStart = Date.now();
|
|
@@ -1402,7 +1423,11 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1402
1423
|
parentTelemetry: this.session.getTelemetry?.(),
|
|
1403
1424
|
forkContextSeed,
|
|
1404
1425
|
});
|
|
1405
|
-
const resultWithForkContext =
|
|
1426
|
+
const resultWithForkContext = {
|
|
1427
|
+
...result,
|
|
1428
|
+
...(forkContext ? { forkContext } : {}),
|
|
1429
|
+
forkContextAdvisory,
|
|
1430
|
+
};
|
|
1406
1431
|
if (mergeMode === "branch" && resultWithForkContext.exitCode === 0) {
|
|
1407
1432
|
try {
|
|
1408
1433
|
const commitMsg =
|
|
@@ -1697,6 +1722,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1697
1722
|
|
|
1698
1723
|
const receipts = results.map(buildTaskReceipt);
|
|
1699
1724
|
const roiSummary = buildTaskRoiSummary(receipts);
|
|
1725
|
+
const roiReconciliation = executionOverrides?.suppressRoiReconciliation
|
|
1726
|
+
? undefined
|
|
1727
|
+
: reconcileSpawnRoi(params.spawnPlan, receipts);
|
|
1700
1728
|
const summaries = receipts.map(r => {
|
|
1701
1729
|
const status = r.status === "merge_failed" ? "merge failed" : r.status;
|
|
1702
1730
|
return {
|
|
@@ -1739,6 +1767,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1739
1767
|
usage: hasAggregatedUsage ? aggregatedUsage : undefined,
|
|
1740
1768
|
forkContextClonedTokens: forkContextClonedTokens > 0 ? forkContextClonedTokens : undefined,
|
|
1741
1769
|
roiSummary,
|
|
1770
|
+
roiReconciliation,
|
|
1742
1771
|
};
|
|
1743
1772
|
assertNoRawTaskFields(details, "task.return.details");
|
|
1744
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
|
}
|
|
@@ -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/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;
|
|
@@ -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
|
}
|
|
@@ -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
|
/**
|