@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,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI renderer for the `subagent` tool.
|
|
3
|
+
*
|
|
4
|
+
* The await panel surfaces each awaited subagent's live streaming status at
|
|
5
|
+
* parity with the inline `task` panel by reusing `renderSubagentLiveProgress`.
|
|
6
|
+
* Falls back to a `running, no activity yet` placeholder when a live producer
|
|
7
|
+
* exists but has not emitted yet, and to a static status line when no live
|
|
8
|
+
* producer is available (resumed-from-disk or backward-compat records).
|
|
9
|
+
*/
|
|
10
|
+
import type { Component } from "@gajae-code/tui";
|
|
11
|
+
import { Text } from "@gajae-code/tui";
|
|
12
|
+
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
13
|
+
import type { Theme } from "../modes/theme/theme";
|
|
14
|
+
import { renderSubagentLiveProgress } from "../task/render";
|
|
15
|
+
import { Ellipsis, Hasher, type RenderCache, renderStatusLine } from "../tui";
|
|
16
|
+
import {
|
|
17
|
+
formatDuration,
|
|
18
|
+
formatStatusIcon,
|
|
19
|
+
getPreviewLines,
|
|
20
|
+
replaceTabs,
|
|
21
|
+
type ToolUIStatus,
|
|
22
|
+
truncateToWidth,
|
|
23
|
+
} from "./render-utils";
|
|
24
|
+
import type { SubagentSnapshot, SubagentToolDetails } from "./subagent";
|
|
25
|
+
|
|
26
|
+
const PREVIEW_LINES_COLLAPSED = 1;
|
|
27
|
+
const PREVIEW_LINES_EXPANDED = 4;
|
|
28
|
+
const PREVIEW_LINE_WIDTH = 80;
|
|
29
|
+
|
|
30
|
+
function statusIconKind(status: SubagentSnapshot["status"]): ToolUIStatus {
|
|
31
|
+
switch (status) {
|
|
32
|
+
case "completed":
|
|
33
|
+
case "already_completed":
|
|
34
|
+
return "success";
|
|
35
|
+
case "failed":
|
|
36
|
+
return "error";
|
|
37
|
+
case "cancelled":
|
|
38
|
+
case "not_found":
|
|
39
|
+
return "warning";
|
|
40
|
+
case "queued":
|
|
41
|
+
return "pending";
|
|
42
|
+
default:
|
|
43
|
+
return "info";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function renderSubagentSnapshot(
|
|
48
|
+
snapshot: SubagentSnapshot,
|
|
49
|
+
expanded: boolean,
|
|
50
|
+
theme: Theme,
|
|
51
|
+
spinnerFrame: number | undefined,
|
|
52
|
+
): string[] {
|
|
53
|
+
const lines: string[] = [];
|
|
54
|
+
const icon = formatStatusIcon(
|
|
55
|
+
statusIconKind(snapshot.status),
|
|
56
|
+
theme,
|
|
57
|
+
snapshot.status === "running" ? spinnerFrame : undefined,
|
|
58
|
+
);
|
|
59
|
+
const id = theme.fg("muted", snapshot.id);
|
|
60
|
+
const status = theme.fg("dim", snapshot.status);
|
|
61
|
+
const duration = theme.fg("dim", formatDuration(snapshot.durationMs));
|
|
62
|
+
lines.push(`${icon} ${id} ${status} ${duration}`);
|
|
63
|
+
|
|
64
|
+
// Static receipt fields (parity with the markdown content for non-await actions).
|
|
65
|
+
if (snapshot.jobId !== snapshot.id) lines.push(` ${theme.fg("dim", `Job: ${snapshot.jobId}`)}`);
|
|
66
|
+
if (snapshot.agent && snapshot.agent !== "unknown") {
|
|
67
|
+
lines.push(` ${theme.fg("dim", `Agent: ${snapshot.agent} (${snapshot.agentSource})`)}`);
|
|
68
|
+
}
|
|
69
|
+
if (snapshot.description) lines.push(` ${theme.fg("dim", `Description: ${snapshot.description}`)}`);
|
|
70
|
+
if (snapshot.outputRef) lines.push(` ${theme.fg("dim", `Output: ${snapshot.outputRef}`)}`);
|
|
71
|
+
if (snapshot.assignment) {
|
|
72
|
+
lines.push(` ${theme.fg("dim", "Assignment:")}`);
|
|
73
|
+
for (const al of snapshot.assignment.split("\n")) lines.push(` ${theme.fg("toolOutput", replaceTabs(al))}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
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) {
|
|
81
|
+
// Live streaming panel (full task-panel parity), indented under the header.
|
|
82
|
+
for (const pl of renderSubagentLiveProgress(snapshot.progress, expanded, theme, spinnerFrame)) {
|
|
83
|
+
lines.push(` ${pl}`);
|
|
84
|
+
}
|
|
85
|
+
} else if (snapshot.liveProgressAvailable && (snapshot.status === "running" || snapshot.status === "queued")) {
|
|
86
|
+
lines.push(` ${theme.fg("dim", "running, no activity yet")}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const preview = snapshot.errorText?.trim() || snapshot.resultText?.trim();
|
|
90
|
+
if (preview) {
|
|
91
|
+
const maxLines = expanded ? PREVIEW_LINES_EXPANDED : PREVIEW_LINES_COLLAPSED;
|
|
92
|
+
const tone = snapshot.errorText ? "error" : "dim";
|
|
93
|
+
for (const pl of getPreviewLines(preview, maxLines, PREVIEW_LINE_WIDTH, Ellipsis.Unicode)) {
|
|
94
|
+
lines.push(` ${theme.fg(tone, replaceTabs(pl))}`);
|
|
95
|
+
}
|
|
96
|
+
if (snapshot.truncated) {
|
|
97
|
+
lines.push(
|
|
98
|
+
` ${theme.fg("dim", "Preview truncated; use the output ref or explicit ids with `verbosity=full` for more.")}`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (snapshot.guidance) lines.push(` ${theme.fg("dim", snapshot.guidance)}`);
|
|
104
|
+
return lines;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const subagentToolRenderer = {
|
|
108
|
+
inline: true,
|
|
109
|
+
|
|
110
|
+
renderCall(_args: unknown, _options: RenderResultOptions, theme: Theme): Component {
|
|
111
|
+
return new Text(renderStatusLine({ icon: "pending", title: "Subagent" }, theme), 0, 0);
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
renderResult(
|
|
115
|
+
result: { content: Array<{ type: string; text?: string }>; details?: SubagentToolDetails },
|
|
116
|
+
options: RenderResultOptions,
|
|
117
|
+
theme: Theme,
|
|
118
|
+
): Component {
|
|
119
|
+
const subagents = result.details?.subagents ?? [];
|
|
120
|
+
if (subagents.length === 0) {
|
|
121
|
+
const fallback = result.content.find(c => c.type === "text")?.text || "No subagents";
|
|
122
|
+
return new Text(theme.fg("dim", truncateToWidth(fallback, 100)), 0, 0);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const runningCount = subagents.filter(s => s.status === "running").length;
|
|
126
|
+
|
|
127
|
+
let cached: RenderCache | undefined;
|
|
128
|
+
return {
|
|
129
|
+
render(width: number): string[] {
|
|
130
|
+
const expanded = options.expanded;
|
|
131
|
+
const spinnerFrame = options.spinnerFrame ?? 0;
|
|
132
|
+
const key = new Hasher().bool(expanded).u32(width).u32(spinnerFrame).digest();
|
|
133
|
+
if (cached?.key === key) return cached.lines;
|
|
134
|
+
|
|
135
|
+
const header = renderStatusLine(
|
|
136
|
+
{
|
|
137
|
+
icon: runningCount > 0 ? "info" : "success",
|
|
138
|
+
spinnerFrame: runningCount > 0 ? options.spinnerFrame : undefined,
|
|
139
|
+
title: "Subagent",
|
|
140
|
+
description:
|
|
141
|
+
runningCount > 0
|
|
142
|
+
? `awaiting ${runningCount} of ${subagents.length}`
|
|
143
|
+
: `${subagents.length} ${subagents.length === 1 ? "subagent" : "subagents"}`,
|
|
144
|
+
},
|
|
145
|
+
theme,
|
|
146
|
+
);
|
|
147
|
+
|
|
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
|
+
}
|
|
154
|
+
for (const snapshot of subagents) {
|
|
155
|
+
lines.push(...renderSubagentSnapshot(snapshot, expanded, theme, options.spinnerFrame));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const out = lines.map(l => (l.length > 0 ? truncateToWidth(l, width, Ellipsis.Omit) : ""));
|
|
159
|
+
cached = { key, lines: out };
|
|
160
|
+
return out;
|
|
161
|
+
},
|
|
162
|
+
invalidate() {
|
|
163
|
+
cached = undefined;
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
mergeCallAndResult: true,
|
|
169
|
+
};
|
package/src/tools/subagent.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { prompt } from "@gajae-code/utils";
|
|
|
4
4
|
import * as z from "zod/v4";
|
|
5
5
|
import { type AsyncJob, AsyncJobManager, type SubagentRecord } from "../async";
|
|
6
6
|
import subagentDescription from "../prompts/tools/subagent.md" with { type: "text" };
|
|
7
|
-
import type { AgentSource } from "../task/types";
|
|
7
|
+
import type { AgentProgress, AgentSource } from "../task/types";
|
|
8
8
|
import { Ellipsis, truncateToWidth } from "../tui";
|
|
9
9
|
import type { ToolSession } from "./index";
|
|
10
10
|
import { replaceTabs } from "./render-utils";
|
|
@@ -63,6 +63,10 @@ export interface SubagentSnapshot {
|
|
|
63
63
|
outputRef?: string;
|
|
64
64
|
truncated?: boolean;
|
|
65
65
|
guidance?: string;
|
|
66
|
+
/** Live streaming progress for the awaited subagent (await panel only; UI detail). */
|
|
67
|
+
progress?: AgentProgress;
|
|
68
|
+
/** True when a live in-session progress producer exists for this subagent. */
|
|
69
|
+
liveProgressAvailable?: boolean;
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
export interface SubagentToolDetails {
|
|
@@ -322,10 +326,10 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
|
|
|
322
326
|
manager.watchJobs(watchedJobIds);
|
|
323
327
|
const progressTimer = onUpdate
|
|
324
328
|
? setInterval(() => {
|
|
325
|
-
onUpdate(this.#progressResult(manager, records));
|
|
329
|
+
onUpdate(this.#progressResult(manager, records, true));
|
|
326
330
|
}, 500)
|
|
327
331
|
: undefined;
|
|
328
|
-
onUpdate?.(this.#progressResult(manager, records));
|
|
332
|
+
onUpdate?.(this.#progressResult(manager, records, true));
|
|
329
333
|
|
|
330
334
|
let timedOut = false;
|
|
331
335
|
try {
|
|
@@ -355,6 +359,7 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
|
|
|
355
359
|
notFoundIds,
|
|
356
360
|
timedOut,
|
|
357
361
|
verbosity: params.verbosity ?? "receipt",
|
|
362
|
+
attachLiveProgress: true,
|
|
358
363
|
});
|
|
359
364
|
}
|
|
360
365
|
|
|
@@ -450,17 +455,29 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
|
|
|
450
455
|
return ids.filter(id => !this.#findVisibleRecord(manager, id, ownerFilter));
|
|
451
456
|
}
|
|
452
457
|
|
|
453
|
-
#progressResult(
|
|
458
|
+
#progressResult(
|
|
459
|
+
manager: AsyncJobManager,
|
|
460
|
+
records: SubagentRecord[],
|
|
461
|
+
attachLiveProgress = false,
|
|
462
|
+
): AgentToolResult<SubagentToolDetails> {
|
|
454
463
|
return {
|
|
455
464
|
content: [{ type: "text", text: "" }],
|
|
456
|
-
details: {
|
|
465
|
+
details: {
|
|
466
|
+
subagents: this.#recordSnapshots(manager, records, false, "receipt", new Set(), attachLiveProgress),
|
|
467
|
+
},
|
|
457
468
|
};
|
|
458
469
|
}
|
|
459
470
|
|
|
460
471
|
async #buildRecordResult(
|
|
461
472
|
manager: AsyncJobManager,
|
|
462
473
|
records: SubagentRecord[],
|
|
463
|
-
options: {
|
|
474
|
+
options: {
|
|
475
|
+
title: string;
|
|
476
|
+
notFoundIds?: string[];
|
|
477
|
+
timedOut?: boolean;
|
|
478
|
+
verbosity?: SubagentParams["verbosity"];
|
|
479
|
+
attachLiveProgress?: boolean;
|
|
480
|
+
},
|
|
464
481
|
): Promise<AgentToolResult<SubagentToolDetails>> {
|
|
465
482
|
const verifiedOutputIds = await this.#verifiedOutputIds(records);
|
|
466
483
|
const snapshots = this.#recordSnapshots(
|
|
@@ -469,6 +486,7 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
|
|
|
469
486
|
options.timedOut,
|
|
470
487
|
options.verbosity ?? "receipt",
|
|
471
488
|
verifiedOutputIds,
|
|
489
|
+
options.attachLiveProgress ?? false,
|
|
472
490
|
);
|
|
473
491
|
for (const id of options.notFoundIds ?? []) {
|
|
474
492
|
snapshots.push(this.#missingSnapshot(id, "not_found", "No visible detached subagent matches this id."));
|
|
@@ -513,8 +531,28 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
|
|
|
513
531
|
timedOut = false,
|
|
514
532
|
verbosity: SubagentParams["verbosity"] = "receipt",
|
|
515
533
|
verifiedOutputIds: ReadonlySet<string>,
|
|
534
|
+
attachLiveProgress = false,
|
|
516
535
|
): SubagentSnapshot[] {
|
|
517
|
-
return records.map(record =>
|
|
536
|
+
return records.map(record =>
|
|
537
|
+
this.#recordSnapshot(manager, record, timedOut, verbosity, verifiedOutputIds, attachLiveProgress),
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
#liveProgressFields(
|
|
542
|
+
manager: AsyncJobManager,
|
|
543
|
+
record: SubagentRecord,
|
|
544
|
+
attachLiveProgress: boolean,
|
|
545
|
+
): Pick<SubagentSnapshot, "progress" | "liveProgressAvailable"> {
|
|
546
|
+
if (!attachLiveProgress) return {};
|
|
547
|
+
const liveProgressAvailable = manager.hasLiveSubagent(record.subagentId);
|
|
548
|
+
// Only surface progress when a live producer exists; stale/retained progress
|
|
549
|
+
// for a record with no live producer must degrade to a static snapshot (AC5).
|
|
550
|
+
if (!liveProgressAvailable) return { liveProgressAvailable: false };
|
|
551
|
+
const progress = manager.getSubagentProgress(record.subagentId);
|
|
552
|
+
return {
|
|
553
|
+
liveProgressAvailable: true,
|
|
554
|
+
...(progress ? { progress } : {}),
|
|
555
|
+
};
|
|
518
556
|
}
|
|
519
557
|
|
|
520
558
|
#recordSnapshot(
|
|
@@ -523,7 +561,9 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
|
|
|
523
561
|
timedOut = false,
|
|
524
562
|
verbosity: SubagentParams["verbosity"] = "receipt",
|
|
525
563
|
verifiedOutputIds: ReadonlySet<string>,
|
|
564
|
+
attachLiveProgress = false,
|
|
526
565
|
): SubagentSnapshot {
|
|
566
|
+
const liveFields = this.#liveProgressFields(manager, record, attachLiveProgress);
|
|
527
567
|
const job = record.currentJobId ? manager.getJob(record.currentJobId) : undefined;
|
|
528
568
|
if (job) {
|
|
529
569
|
return {
|
|
@@ -531,6 +571,7 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
|
|
|
531
571
|
id: record.subagentId,
|
|
532
572
|
jobId: record.currentJobId ?? job.id,
|
|
533
573
|
status: record.status,
|
|
574
|
+
...liveFields,
|
|
534
575
|
};
|
|
535
576
|
}
|
|
536
577
|
return {
|
|
@@ -542,6 +583,7 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
|
|
|
542
583
|
agentSource: "bundled",
|
|
543
584
|
durationMs: 0,
|
|
544
585
|
...(verifiedOutputIds.has(record.subagentId) ? { outputRef: `agent://${record.subagentId}` } : {}),
|
|
586
|
+
...liveFields,
|
|
545
587
|
};
|
|
546
588
|
}
|
|
547
589
|
|
|
@@ -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
|
/**
|