@gajae-code/coding-agent 0.18.0 → 0.18.1
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 +35 -0
- package/dist/types/config/settings-schema.d.ts +2 -2
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +1 -0
- package/dist/types/sdk/acp/adapter.d.ts +3 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/client/client.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/task/index.d.ts +0 -1
- package/dist/types/tools/descriptors.d.ts +5 -0
- package/dist/types/tools/eval-backends.d.ts +37 -0
- package/dist/types/tools/eval.d.ts +1 -6
- package/dist/types/tools/index.d.ts +1 -36
- package/dist/types/tools/session-descriptions.d.ts +22 -0
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/src/cli/doctor/managed-link.ts +4 -4
- package/src/cli/doctor/repairs.ts +9 -1
- package/src/config/settings-schema.ts +8 -2
- package/src/exec/bash-shell-guardian.ts +31 -15
- package/src/gjc-runtime/session-state-sidecar.ts +88 -58
- package/src/internal-urls/docs-index.generated.ts +4 -4
- package/src/modes/acp/acp-agent.ts +97 -16
- package/src/modes/interactive-mode.ts +10 -0
- package/src/modes/utils/context-usage.ts +2 -6
- package/src/prompts/tools/task.md +8 -27
- package/src/sdk/acp/adapter.ts +38 -3
- package/src/sdk/broker/doctor-restart.ts +38 -3
- package/src/sdk/bus/index.ts +3 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/client/client.ts +53 -35
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/session/agent-session.ts +65 -15
- package/src/task/index.ts +21 -66
- package/src/tools/descriptors.ts +27 -1
- package/src/tools/eval-backends.ts +95 -0
- package/src/tools/eval.ts +3 -15
- package/src/tools/index.ts +9 -88
- package/src/tools/search.ts +2 -7
- package/src/tools/session-descriptions.ts +135 -0
- package/src/tools/tool-catalog.generated.ts +1 -1
|
@@ -214,6 +214,12 @@ interface PromptWaiter {
|
|
|
214
214
|
/** Whether ANY overlapping cancel attempt for this prompt was acknowledged:
|
|
215
215
|
* a later failed attempt must not erase an earlier success (review thread P2). */
|
|
216
216
|
cancelAcknowledged?: boolean;
|
|
217
|
+
/** Durable cancel intent after a no-effect abort before this waiter starts work; retained until settlement. */
|
|
218
|
+
cancelBeforeAdmission?: boolean;
|
|
219
|
+
/** Abort scope replayed after admission and again at correlated start if no turn was active yet. */
|
|
220
|
+
cancelBeforeAdmissionScope?: AbortScope;
|
|
221
|
+
/** A correlated start may need one final abort after an admitted-but-not-running no-effect response. */
|
|
222
|
+
cancelAfterStartAbortIssued?: boolean;
|
|
217
223
|
/** In-flight cancel attempts for this prompt: cancellation intent must stay
|
|
218
224
|
* set while any attempt can still acknowledge, so a failure only clears
|
|
219
225
|
* the shared flag when no attempt remains pending (review thread P2). */
|
|
@@ -591,6 +597,12 @@ function isAbortAcknowledged(value: unknown, scope: AbortScope): boolean {
|
|
|
591
597
|
if (result.aborted === true) return true;
|
|
592
598
|
return result.ok === true && result.turn === "stopped" && result.selection === scope;
|
|
593
599
|
}
|
|
600
|
+
function isAbortNoEffect(value: unknown): boolean {
|
|
601
|
+
const candidate = object(value);
|
|
602
|
+
const result = object(candidate?.result) ?? candidate;
|
|
603
|
+
return result?.turn === "no_active_turn" || result?.turn === "no_effect" || result?.turn === "no_store";
|
|
604
|
+
}
|
|
605
|
+
|
|
594
606
|
function strictCorrelationFrom(...values: unknown[]): PromptCorrelation | undefined {
|
|
595
607
|
const correlation: PromptCorrelation = {};
|
|
596
608
|
let malformed = false;
|
|
@@ -1495,6 +1507,11 @@ export function acpRequestFailure(error: unknown): unknown {
|
|
|
1495
1507
|
const code = typeof error === "object" && error !== null && "code" in error ? error.code : undefined;
|
|
1496
1508
|
if (typeof code !== "string") return error;
|
|
1497
1509
|
const message = error instanceof Error ? error.message : code;
|
|
1510
|
+
// ACP clients render `data.details` after the JSON-RPC message. The MCP launch
|
|
1511
|
+
// diagnostic is already the complete additional message, so repeating it in
|
|
1512
|
+
// `details` produces `Internal error: X.: X.` on the wire.
|
|
1513
|
+
const duplicateMcpLaunchDetails =
|
|
1514
|
+
code === "unavailable" && message.startsWith("MCP server request failed to start (");
|
|
1498
1515
|
// A prompt terminal additionally publishes its classification (issue #5615). The
|
|
1499
1516
|
// spread is empty for every other error, so their payloads are byte-identical to
|
|
1500
1517
|
// before. `code`/`details` and the JSON-RPC code itself are untouched either way:
|
|
@@ -1502,8 +1519,8 @@ export function acpRequestFailure(error: unknown): unknown {
|
|
|
1502
1519
|
const promptFailure = promptFailureFromDirectError(error);
|
|
1503
1520
|
const data =
|
|
1504
1521
|
promptFailure !== undefined
|
|
1505
|
-
? { code, details: message, ...promptFailureWireData(promptFailure) }
|
|
1506
|
-
: { code, details: message };
|
|
1522
|
+
? { code, ...(duplicateMcpLaunchDetails ? {} : { details: message }), ...promptFailureWireData(promptFailure) }
|
|
1523
|
+
: { code, ...(duplicateMcpLaunchDetails ? {} : { details: message }) };
|
|
1507
1524
|
// An abandoned prompt additionally publishes the plan it never finished (issue #5669).
|
|
1508
1525
|
// Same rule as above: `code`/`details` and the JSON-RPC code are untouched, and an abandon
|
|
1509
1526
|
// that observed no plan adds no keys, so its payload stays byte-identical to before.
|
|
@@ -2362,7 +2379,7 @@ export class AcpAgent implements Agent {
|
|
|
2362
2379
|
this.#releaseRetiredPromptAcknowledgement(params.sessionId, waiter);
|
|
2363
2380
|
return await response;
|
|
2364
2381
|
}
|
|
2365
|
-
if (record.cancelRequested && waiter.cancelAcknowledged) {
|
|
2382
|
+
if (record.cancelRequested && (waiter.cancelAcknowledged || waiter.cancelBeforeAdmission)) {
|
|
2366
2383
|
this.#releaseRetiredPromptAcknowledgement(params.sessionId, waiter);
|
|
2367
2384
|
await this.#settleCancelledPrompt(params.sessionId, record, waiter);
|
|
2368
2385
|
return await response;
|
|
@@ -2385,7 +2402,7 @@ export class AcpAgent implements Agent {
|
|
|
2385
2402
|
this.#releaseRetiredPromptAcknowledgement(params.sessionId, waiter);
|
|
2386
2403
|
return await response;
|
|
2387
2404
|
}
|
|
2388
|
-
if (record.cancelRequested && waiter.cancelAcknowledged) {
|
|
2405
|
+
if (record.cancelRequested && (waiter.cancelAcknowledged || waiter.cancelBeforeAdmission)) {
|
|
2389
2406
|
this.#releaseRetiredPromptAcknowledgement(params.sessionId, waiter);
|
|
2390
2407
|
await this.#settleCancelledPrompt(params.sessionId, record, waiter);
|
|
2391
2408
|
return await response;
|
|
@@ -2415,18 +2432,18 @@ export class AcpAgent implements Agent {
|
|
|
2415
2432
|
record.adapter,
|
|
2416
2433
|
);
|
|
2417
2434
|
}
|
|
2418
|
-
waiter.dispatched = true;
|
|
2419
|
-
// The turn is now dispatched to the host: this prompt owned the session's first turn,
|
|
2420
|
-
// so it — not a preflight rejection that threw before this point — is what settles
|
|
2421
|
-
// `firstPromptDone` in `prompt()` (review P2).
|
|
2422
|
-
if (retryReservation) retryReservation.admitted = true;
|
|
2423
2435
|
if (waiter.settled || record.activePrompt !== waiter) {
|
|
2424
2436
|
return await response;
|
|
2425
2437
|
}
|
|
2426
|
-
if (record.cancelRequested && waiter.cancelAcknowledged) {
|
|
2438
|
+
if (record.cancelRequested && (waiter.cancelAcknowledged || waiter.cancelBeforeAdmission)) {
|
|
2427
2439
|
await this.#settleCancelledPrompt(params.sessionId, record, waiter);
|
|
2428
2440
|
return await response;
|
|
2429
2441
|
}
|
|
2442
|
+
waiter.dispatched = true;
|
|
2443
|
+
// The turn is now dispatched to the host: this prompt owned the session's first turn,
|
|
2444
|
+
// so it — not a preflight rejection that threw before this point — is what settles
|
|
2445
|
+
// `firstPromptDone` in `prompt()` (review P2).
|
|
2446
|
+
if (retryReservation) retryReservation.admitted = true;
|
|
2430
2447
|
|
|
2431
2448
|
waiter.acknowledgementPending = true;
|
|
2432
2449
|
const promptAdapter = record.adapter;
|
|
@@ -2546,6 +2563,19 @@ export class AcpAgent implements Agent {
|
|
|
2546
2563
|
this.#rememberSettledPromptCorrelation(params.sessionId, record, acknowledgementCorrelation);
|
|
2547
2564
|
return await response;
|
|
2548
2565
|
}
|
|
2566
|
+
if (waiter.cancelBeforeAdmission && record.cancelRequested) {
|
|
2567
|
+
const scope = waiter.cancelBeforeAdmissionScope ?? "turn";
|
|
2568
|
+
// Keep the intent until an acknowledged abort or the prompt terminal settles it.
|
|
2569
|
+
void this.cancel({
|
|
2570
|
+
sessionId: params.sessionId,
|
|
2571
|
+
_meta: { gjc: { abortScope: scope } },
|
|
2572
|
+
} as CancelNotification).catch(error =>
|
|
2573
|
+
logger.warn("ACP could not retry a cancel after prompt admission", {
|
|
2574
|
+
sessionId: params.sessionId,
|
|
2575
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2576
|
+
}),
|
|
2577
|
+
);
|
|
2578
|
+
}
|
|
2549
2579
|
// Frames held while ownership was unknown belong to this prompt only when the
|
|
2550
2580
|
// acknowledgement proves their complete correlation matches exactly.
|
|
2551
2581
|
const deferredActivityFrames = waiter.deferredActivityFrames.splice(0);
|
|
@@ -2691,7 +2721,10 @@ export class AcpAgent implements Agent {
|
|
|
2691
2721
|
// return the semantically meaningful `cancelled` stop reason, so that Clients
|
|
2692
2722
|
// can reliably confirm the cancellation." Surfacing the transport's `busy`
|
|
2693
2723
|
// rejection instead would show the user a spurious error for their own cancel.
|
|
2694
|
-
if (
|
|
2724
|
+
if (
|
|
2725
|
+
record.cancelRequested &&
|
|
2726
|
+
(waiter.cancelAcknowledged || (waiter.cancelBeforeAdmission && !waiter.acknowledged && !waiter.dispatched))
|
|
2727
|
+
) {
|
|
2695
2728
|
record.cancelRequested = false;
|
|
2696
2729
|
// The client's turn is settled by the return; the advisory idle publication
|
|
2697
2730
|
// must not gate it and must still be attempted so the running phase is
|
|
@@ -2715,6 +2748,8 @@ export class AcpAgent implements Agent {
|
|
|
2715
2748
|
// `_meta.gjc.abortScope: "owned"` (or `GJC_ACP_ABORT_SCOPE=owned`).
|
|
2716
2749
|
const scope = resolveAcpAbortScope(params._meta, process.env);
|
|
2717
2750
|
const waiter = record.activePrompt;
|
|
2751
|
+
const waiterWasUnacknowledged = waiter !== undefined && !waiter.acknowledged;
|
|
2752
|
+
const waiterWasBeforeActivity = waiter !== undefined && !waiter.observedTurnActivity;
|
|
2718
2753
|
if (waiter) {
|
|
2719
2754
|
// Overlapping cancels must not lose an earlier successful
|
|
2720
2755
|
// acknowledgement: ANY successful attempt resolves the shared
|
|
@@ -2733,9 +2768,27 @@ export class AcpAgent implements Agent {
|
|
|
2733
2768
|
}
|
|
2734
2769
|
}
|
|
2735
2770
|
try {
|
|
2736
|
-
|
|
2771
|
+
let acknowledgement = await record.adapter.cancel(scope);
|
|
2772
|
+
let acknowledgementScope = scope;
|
|
2773
|
+
if (
|
|
2774
|
+
!isAbortAcknowledged(acknowledgement, scope) &&
|
|
2775
|
+
isAbortNoEffect(acknowledgement) &&
|
|
2776
|
+
(waiterWasUnacknowledged || waiterWasBeforeActivity) &&
|
|
2777
|
+
waiter &&
|
|
2778
|
+
record.activePrompt === waiter &&
|
|
2779
|
+
!waiter.settled
|
|
2780
|
+
) {
|
|
2781
|
+
waiter.cancelBeforeAdmission = true;
|
|
2782
|
+
waiter.cancelBeforeAdmissionScope =
|
|
2783
|
+
waiter.cancelBeforeAdmissionScope === "owned" || scope === "owned" ? "owned" : "turn";
|
|
2784
|
+
if (waiter.acknowledged) {
|
|
2785
|
+
// The first abort raced prompt admission. Retry now that the turn is admitted.
|
|
2786
|
+
acknowledgementScope = waiter.cancelBeforeAdmissionScope;
|
|
2787
|
+
acknowledgement = await record.adapter.cancel(acknowledgementScope);
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2737
2790
|
const result = object(object(acknowledgement)?.result) ?? object(acknowledgement);
|
|
2738
|
-
if (!isAbortAcknowledged(acknowledgement,
|
|
2791
|
+
if (!isAbortAcknowledged(acknowledgement, acknowledgementScope))
|
|
2739
2792
|
throw new AcpSdkAdapterError(
|
|
2740
2793
|
"abort_unacknowledged",
|
|
2741
2794
|
"SDK did not acknowledge cancellation of the active prompt.",
|
|
@@ -2846,7 +2899,12 @@ export class AcpAgent implements Agent {
|
|
|
2846
2899
|
} finally {
|
|
2847
2900
|
if (waiter) {
|
|
2848
2901
|
waiter.pendingCancelAttempts = Math.max(0, (waiter.pendingCancelAttempts ?? 1) - 1);
|
|
2849
|
-
if (
|
|
2902
|
+
if (
|
|
2903
|
+
waiter.pendingCancelAttempts === 0 &&
|
|
2904
|
+
!waiter.cancelAcknowledged &&
|
|
2905
|
+
!waiter.cancelBeforeAdmission &&
|
|
2906
|
+
record.activePrompt === waiter
|
|
2907
|
+
) {
|
|
2850
2908
|
record.cancelRequested = false;
|
|
2851
2909
|
}
|
|
2852
2910
|
}
|
|
@@ -2869,8 +2927,9 @@ export class AcpAgent implements Agent {
|
|
|
2869
2927
|
|
|
2870
2928
|
async #settleCancelledPrompt(id: string, record: SessionRecord, waiter: PromptWaiter): Promise<void> {
|
|
2871
2929
|
// The authoritative terminal wins whenever it arrives in time; this only runs
|
|
2872
|
-
//
|
|
2873
|
-
|
|
2930
|
+
// after an acknowledged abort or before turn dispatch for this waiter.
|
|
2931
|
+
const preAdmissionCancellation = waiter.cancelBeforeAdmission && !waiter.acknowledged && !waiter.dispatched;
|
|
2932
|
+
if (!waiter.cancelAcknowledged && !preAdmissionCancellation) return;
|
|
2874
2933
|
if (this.#sessions.get(id) !== record || record.activePrompt !== waiter || waiter.settled) return;
|
|
2875
2934
|
if (waiter.terminalReserved) return;
|
|
2876
2935
|
// An overlapping acknowledged cancel does not supersede the earlier
|
|
@@ -2893,6 +2952,8 @@ export class AcpAgent implements Agent {
|
|
|
2893
2952
|
if (!record.backgroundBusy) record.busy = false;
|
|
2894
2953
|
clearPromptWatchdog(waiter);
|
|
2895
2954
|
record.cancelRequested = false;
|
|
2955
|
+
waiter.cancelBeforeAdmission = false;
|
|
2956
|
+
waiter.cancelBeforeAdmissionScope = undefined;
|
|
2896
2957
|
waiter.settled = true;
|
|
2897
2958
|
this.#fenceRetiredPromptAcknowledgement(id, waiter);
|
|
2898
2959
|
waiter.deferredFrames.length = 0;
|
|
@@ -4605,6 +4666,24 @@ export class AcpAgent implements Agent {
|
|
|
4605
4666
|
}
|
|
4606
4667
|
if (toolCallId && event.type === "tool_execution_end") record.toolArgs.delete(toolCallId);
|
|
4607
4668
|
if (event.type === "agent_start") {
|
|
4669
|
+
if (
|
|
4670
|
+
promptOwner?.cancelBeforeAdmission &&
|
|
4671
|
+
record.cancelRequested &&
|
|
4672
|
+
!promptOwner.cancelAcknowledged &&
|
|
4673
|
+
!promptOwner.cancelAfterStartAbortIssued
|
|
4674
|
+
) {
|
|
4675
|
+
promptOwner.cancelAfterStartAbortIssued = true;
|
|
4676
|
+
const scope = promptOwner.cancelBeforeAdmissionScope ?? "turn";
|
|
4677
|
+
void this.cancel({
|
|
4678
|
+
sessionId: id,
|
|
4679
|
+
_meta: { gjc: { abortScope: scope } },
|
|
4680
|
+
} as CancelNotification).catch(error =>
|
|
4681
|
+
logger.warn("ACP could not retry a cancel after prompt start", {
|
|
4682
|
+
sessionId: id,
|
|
4683
|
+
error: error instanceof Error ? error.message : String(error),
|
|
4684
|
+
}),
|
|
4685
|
+
);
|
|
4686
|
+
}
|
|
4608
4687
|
await this.#publishSessionUpdate(
|
|
4609
4688
|
id,
|
|
4610
4689
|
{
|
|
@@ -4874,6 +4953,8 @@ export class AcpAgent implements Agent {
|
|
|
4874
4953
|
record.activePrompt = undefined;
|
|
4875
4954
|
clearPromptWatchdog(waiter);
|
|
4876
4955
|
waiter.settled = true;
|
|
4956
|
+
waiter.cancelBeforeAdmission = false;
|
|
4957
|
+
waiter.cancelBeforeAdmissionScope = undefined;
|
|
4877
4958
|
const { outcome } = waiter.terminal;
|
|
4878
4959
|
this.#rememberSettledPromptCorrelation(id, record, waiter.correlation);
|
|
4879
4960
|
if (outcome.kind === "stopped") {
|
|
@@ -565,6 +565,13 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
565
565
|
#suspendedActivityIndicator?: Loader;
|
|
566
566
|
#stopped = false;
|
|
567
567
|
#initPromise?: Promise<void>;
|
|
568
|
+
// Until startup's first paint renders the session transcript, chatContainer holds
|
|
569
|
+
// only pre-paint notices. A transcript rebuild before then (e.g. an extension
|
|
570
|
+
// session_start message with display:true) would wipe those notices and leave
|
|
571
|
+
// session components that the first paint preserves and renders again, so every
|
|
572
|
+
// session-derived block would show twice. Rebuilds are no-ops until the first
|
|
573
|
+
// paint, which renders the full session anyway.
|
|
574
|
+
#initialTranscriptPainted = false;
|
|
568
575
|
#stopListeners = new Set<() => void>();
|
|
569
576
|
#eventBus?: EventBus;
|
|
570
577
|
#eventBusUnsubscribers: Array<() => void> = [];
|
|
@@ -1561,6 +1568,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1561
1568
|
}
|
|
1562
1569
|
|
|
1563
1570
|
rebuildChatFromMessages(policy: TranscriptRebuildPolicy): void {
|
|
1571
|
+
if (!this.#initialTranscriptPainted) return;
|
|
1564
1572
|
prepareTranscriptRebuild(this.ui, policy);
|
|
1565
1573
|
this.resetAssistantTextPresentation();
|
|
1566
1574
|
this.chatContainer.clear();
|
|
@@ -2184,11 +2192,13 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2184
2192
|
prebuiltContext?: SessionContext,
|
|
2185
2193
|
options?: { preserveExistingChat?: boolean },
|
|
2186
2194
|
): void {
|
|
2195
|
+
if (!this.#initialTranscriptPainted) return;
|
|
2187
2196
|
prepareTranscriptRebuild(this.ui, policy);
|
|
2188
2197
|
this.#uiHelpers.renderInitialMessages(prebuiltContext, options);
|
|
2189
2198
|
}
|
|
2190
2199
|
renderInitialMessages(prebuiltContext?: SessionContext, options?: { preserveExistingChat?: boolean }): void {
|
|
2191
2200
|
this.#uiHelpers.renderInitialMessages(prebuiltContext, options);
|
|
2201
|
+
this.#initialTranscriptPainted = true;
|
|
2192
2202
|
}
|
|
2193
2203
|
|
|
2194
2204
|
getUserMessageText(message: Message): string {
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import type { AgentMessage } from "@gajae-code/agent-core";
|
|
2
2
|
import type { CompactionSettings } from "@gajae-code/agent-core/compaction";
|
|
3
|
-
import {
|
|
4
|
-
effectiveReserveTokens,
|
|
5
|
-
estimateMessageTokensHeuristic,
|
|
6
|
-
resolveThresholdTokens,
|
|
7
|
-
} from "@gajae-code/agent-core/compaction";
|
|
3
|
+
import { effectiveReserveTokens, estimateMessageTokensHeuristic } from "@gajae-code/agent-core/compaction";
|
|
8
4
|
import type { Model } from "@gajae-code/ai/core";
|
|
9
5
|
import { formatNumber } from "@gajae-code/utils";
|
|
10
6
|
import type { AgentSession } from "../../session/agent-session";
|
|
@@ -135,7 +131,7 @@ export function computeContextBreakdown(
|
|
|
135
131
|
if (contextWindow > 0) {
|
|
136
132
|
const compactionSettings = session.settings.getGroup("compaction") as CompactionSettings;
|
|
137
133
|
if (compactionSettings.enabled && compactionSettings.strategy !== "off") {
|
|
138
|
-
const threshold =
|
|
134
|
+
const threshold = session.getAutoCompactionThresholdTokens(tokensForFreeSpace);
|
|
139
135
|
autoCompactBufferTokens = Math.max(0, contextWindow - threshold);
|
|
140
136
|
} else {
|
|
141
137
|
autoCompactBufferTokens = 0;
|
|
@@ -2,18 +2,12 @@ Launches subagents to parallelize workflows.
|
|
|
2
2
|
|
|
3
3
|
- Results are delivered automatically when complete.
|
|
4
4
|
- The tool result lists the assigned task ids (e.g. `0-AuthLoader`) — those are the live agent ids.
|
|
5
|
-
|
|
6
|
-
- Coordinate with running tasks via `irc` using those ids. `subagent` cancel terminates a task and **cannot carry a message**.
|
|
7
|
-
{{else}}
|
|
5
|
+
- Coordinate with running tasks via `irc` when available, using those ids. `subagent` cancel terminates a task and **cannot carry a message**.
|
|
8
6
|
- Use `subagent` action `inspect` or `list` to snapshot manager state.
|
|
9
|
-
{{/if}}
|
|
10
7
|
- To wait or cancel, use the `subagent` tool; its await/cancel doctrine is authoritative.
|
|
11
8
|
|
|
12
|
-
{{#if ircEnabled}}
|
|
13
|
-
Subagents have no conversation history, but they can reach you and their siblings live via the `irc` tool. Front-load every fact, file path, and direction they need in {{#if contextEnabled}}`context` or `assignment`{{else}}each `assignment`{{/if}}.
|
|
14
|
-
{{else}}
|
|
15
9
|
Subagents have no conversation history. Every fact, file path, and direction they need MUST be explicit in {{#if contextEnabled}}`context` or `assignment`{{else}}each `assignment`{{/if}}.
|
|
16
|
-
|
|
10
|
+
Use `irc` for live coordination when it is available.
|
|
17
11
|
|
|
18
12
|
<parameters>
|
|
19
13
|
- `agent`: agent type for all tasks
|
|
@@ -36,27 +30,16 @@ Subagents have no conversation history. Every fact, file path, and direction the
|
|
|
36
30
|
- HARD runtime gate: calls with more than 4 tasks are rejected before any child launches unless `spawnPlan` is complete.
|
|
37
31
|
- NEVER assign tasks to run project-wide build/test/lint. Caller verifies after the batch.
|
|
38
32
|
- **Subagents do not verify, lint, or format.** Every assignment MUST instruct the subagent to skip all gates and formatters. You run them once at the end across the union of changed files — avoids redundant runs and racing formatter passes.
|
|
39
|
-
|
|
40
|
-
- Each task: ≤3–5 explicit files. Overlapping file sets are tolerable when peers can coordinate via `irc`, but still fan out to a cluster when the scopes are cleanly separable.
|
|
41
|
-
- No globs, no "update all", no package-wide scope.
|
|
42
|
-
{{else}}
|
|
43
|
-
- Each task: ≤3–5 explicit files. No globs, no "update all", no package-wide scope. Fan out to a cluster instead.
|
|
44
|
-
{{/if}}
|
|
33
|
+
- Each task: ≤3–5 explicit files. No globs, no "update all", no package-wide scope. Use `irc` to coordinate overlapping work when it is available; otherwise keep file sets independent.
|
|
45
34
|
- Pass large payloads via `local://<path>` URIs, not inline.
|
|
46
35
|
{{#if contextEnabled}}- Put shared constraints in `context` once; do not duplicate across assignments.{{/if}}
|
|
47
36
|
- Prefer agents that investigate **and** edit in one pass; only spin a read-only discovery step when affected files are genuinely unknown.
|
|
48
37
|
</rules>
|
|
49
38
|
|
|
50
39
|
<parallelization>
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
Parallel when tasks touch disjoint files, are independent refactors/tests, or only need occasional clarification that can be resolved peer-to-peer.
|
|
55
|
-
{{else}}
|
|
56
|
-
Test: can task B run correctly without seeing A's output? If no, sequence A → B.
|
|
57
|
-
Sequential when one task produces a contract (types, API, schema, core module) the other consumes.
|
|
58
|
-
Parallel when tasks touch disjoint files or are independent refactors/tests.
|
|
59
|
-
{{/if}}
|
|
40
|
+
Test: can task B run correctly without seeing A's output? If no, sequence A → B, unless `irc` is available and A can answer a small clarification.
|
|
41
|
+
Still sequence when one task produces a large, evolving contract (generated types, schema migration, core module API); live coordination is not a substitute for a finished contract.
|
|
42
|
+
Parallel when tasks touch disjoint files, are independent refactors/tests, or need only occasional peer clarification.
|
|
60
43
|
</parallelization>
|
|
61
44
|
|
|
62
45
|
{{#if contextEnabled}}
|
|
@@ -77,10 +60,8 @@ Parallel when tasks touch disjoint files or are independent refactors/tests.
|
|
|
77
60
|
{{#if spawningDisabled}}
|
|
78
61
|
Agent spawning is disabled for this context.
|
|
79
62
|
{{else}}
|
|
80
|
-
{{#list agents join="
|
|
81
|
-
|
|
82
|
-
{{description}}
|
|
83
|
-
{{/list}}
|
|
63
|
+
Bundled role names: {{#list agents join=", "}}{{name}}{{/list}}. A configured agent may override a bundled role name and takes precedence.
|
|
64
|
+
Other configured agents (project, user, plugin) may also be available; calling with an unknown `agent` lists the agents callable in this session.
|
|
84
65
|
{{/if}}
|
|
85
66
|
</agents>
|
|
86
67
|
|
package/src/sdk/acp/adapter.ts
CHANGED
|
@@ -112,6 +112,16 @@ function credentialFreeLifecycleResult(value: unknown): unknown {
|
|
|
112
112
|
return output;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
const ACP_MCP_FAILURE_MESSAGE_MAX_LENGTH = 240;
|
|
116
|
+
|
|
117
|
+
function credentialFreeLifecycleMessage(error: unknown): string {
|
|
118
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
119
|
+
return message
|
|
120
|
+
.replace(/\b(?:endpoint|token|url)\s*[:=]\s*(?:"[^"]*"|'[^']*'|\S+)/gi, "[redacted]")
|
|
121
|
+
.replace(/https?:\/\/[^\s)]+/gi, "[redacted-url]")
|
|
122
|
+
.slice(0, ACP_MCP_FAILURE_MESSAGE_MAX_LENGTH);
|
|
123
|
+
}
|
|
124
|
+
|
|
115
125
|
/**
|
|
116
126
|
* Lifecycle failures the ACP MCP launch wrapper must report verbatim. Everything else
|
|
117
127
|
* is re-attributed to the configured MCP servers, which is the useful answer for a
|
|
@@ -135,22 +145,47 @@ const ACP_MCP_PRESERVED_LAUNCH_CODES = new Set([
|
|
|
135
145
|
"worktree_in_use",
|
|
136
146
|
]);
|
|
137
147
|
|
|
148
|
+
/**
|
|
149
|
+
* SDK client transport failures happen before the lifecycle request reaches any MCP server and
|
|
150
|
+
* must retain their original retry semantics. The `transport` origin marker is required because
|
|
151
|
+
* broker ERROR frames are also represented as SdkClientError instances with these same codes.
|
|
152
|
+
*/
|
|
153
|
+
const ACP_MCP_PRESERVED_TRANSPORT_CODES = new Set(["connection_closed", "unavailable", "timeout"]);
|
|
154
|
+
|
|
138
155
|
/**
|
|
139
156
|
* The error an ACP session launch must throw once a lifecycle request that carried MCP
|
|
140
|
-
* servers has failed.
|
|
157
|
+
* servers has failed. Broker transport failures from the SDK client are preserved verbatim;
|
|
158
|
+
* other failures keep the adapter's `unavailable` code while carrying a bounded, credential-free
|
|
159
|
+
* copy of the original code and message as their cause diagnostic.
|
|
141
160
|
*/
|
|
142
161
|
export function acpMcpLaunchFailure(error: unknown, mcpServers: SessionLifecycleMcpServer[]): unknown {
|
|
143
162
|
const code =
|
|
144
163
|
typeof error === "object" && error !== null && "code" in error && typeof error.code === "string"
|
|
145
164
|
? error.code
|
|
146
165
|
: undefined;
|
|
147
|
-
if (
|
|
166
|
+
if (
|
|
167
|
+
mcpServers.length === 0 ||
|
|
168
|
+
(code !== undefined && ACP_MCP_PRESERVED_LAUNCH_CODES.has(code)) ||
|
|
169
|
+
(error instanceof SdkClientError &&
|
|
170
|
+
error.transport === true &&
|
|
171
|
+
code !== undefined &&
|
|
172
|
+
ACP_MCP_PRESERVED_TRANSPORT_CODES.has(code))
|
|
173
|
+
)
|
|
174
|
+
return error;
|
|
148
175
|
const names = mcpServers
|
|
149
176
|
.slice(0, 8)
|
|
150
177
|
.map(server => server.name)
|
|
151
178
|
.join(", ");
|
|
152
179
|
const suffix = mcpServers.length > 8 ? `, and ${mcpServers.length - 8} more` : "";
|
|
153
|
-
|
|
180
|
+
const originalCode = code ?? "unknown";
|
|
181
|
+
const originalMessage = credentialFreeLifecycleMessage(error);
|
|
182
|
+
const diagnostic = originalMessage ? ` [${originalCode}: ${originalMessage}]` : ` [${originalCode}]`;
|
|
183
|
+
const wrapped = new AcpSdkAdapterError(
|
|
184
|
+
"unavailable",
|
|
185
|
+
`MCP server request failed to start (${names}${suffix}): broker lifecycle failed${diagnostic}`,
|
|
186
|
+
);
|
|
187
|
+
Object.assign(wrapped, { cause: error });
|
|
188
|
+
return wrapped;
|
|
154
189
|
}
|
|
155
190
|
|
|
156
191
|
export type AcpReconnectFailedHandler = (error: SdkClientError) => void;
|
|
@@ -63,6 +63,41 @@ function typedResult(response: unknown): BrokerRestartResult {
|
|
|
63
63
|
return { ok: false, error: { code: "protocol_error", message: "broker returned a malformed restart response" } };
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Sends one restart verb. SdkClient rejects an `{ok:false}` response as an
|
|
68
|
+
* SdkClientError, but that refusal is the broker's own typed decision, so it is
|
|
69
|
+
* returned as the result. Only a request that got no response for its id (its
|
|
70
|
+
* outcome is unknown) propagates as an error.
|
|
71
|
+
*/
|
|
72
|
+
async function restartRequest(
|
|
73
|
+
client: SdkClient,
|
|
74
|
+
operation: string,
|
|
75
|
+
input: Record<string, unknown>,
|
|
76
|
+
): Promise<BrokerRestartResult> {
|
|
77
|
+
let requestFrameId: unknown;
|
|
78
|
+
let refused = false;
|
|
79
|
+
const unsubscribe = client.onFrame(frame => {
|
|
80
|
+
const id = typeof frame.id === "string" ? frame.id : frame.requestId;
|
|
81
|
+
if (requestFrameId !== undefined && id === requestFrameId && (frame.ok === false || frame.status === "error"))
|
|
82
|
+
refused = true;
|
|
83
|
+
});
|
|
84
|
+
try {
|
|
85
|
+
return typedResult(
|
|
86
|
+
await client.global(operation, input, {
|
|
87
|
+
onDispatch: ({ frame }) => {
|
|
88
|
+
requestFrameId = frame.id;
|
|
89
|
+
},
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (refused && error instanceof SdkClientError)
|
|
94
|
+
return { ok: false, error: { code: error.code, message: error.message } };
|
|
95
|
+
throw error;
|
|
96
|
+
} finally {
|
|
97
|
+
unsubscribe();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
66
101
|
export async function prepareDoctorBrokerRestart(
|
|
67
102
|
client: SdkClient,
|
|
68
103
|
discovery: BrokerDiscovery,
|
|
@@ -74,7 +109,7 @@ export async function prepareDoctorBrokerRestart(
|
|
|
74
109
|
deadlineAt: Date.now() + (options.deadlineMs ?? 30_000),
|
|
75
110
|
drain: options.drain,
|
|
76
111
|
};
|
|
77
|
-
return
|
|
112
|
+
return await restartRequest(client, "broker.prepare_restart", { ...input });
|
|
78
113
|
}
|
|
79
114
|
|
|
80
115
|
export async function commitDoctorBrokerRestart(
|
|
@@ -90,11 +125,11 @@ export async function commitDoctorBrokerRestart(
|
|
|
90
125
|
lease: prepared.lease,
|
|
91
126
|
occupancyEpoch: prepared.occupancyEpoch,
|
|
92
127
|
};
|
|
93
|
-
return
|
|
128
|
+
return await restartRequest(client, "broker.commit_restart", { ...input });
|
|
94
129
|
}
|
|
95
130
|
|
|
96
131
|
export async function cancelDoctorBrokerRestart(client: SdkClient, requestId: string): Promise<BrokerRestartResult> {
|
|
97
|
-
return
|
|
132
|
+
return await restartRequest(client, "broker.cancel_restart", { requestId });
|
|
98
133
|
}
|
|
99
134
|
|
|
100
135
|
const OLD_OWNER_EXIT_POLL_MS = 50;
|
package/src/sdk/bus/index.ts
CHANGED
|
@@ -9636,10 +9636,10 @@ export function createNotificationsExtension(
|
|
|
9636
9636
|
else if (finalAssistant?.errorKind === "provider_safety_stop")
|
|
9637
9637
|
outcome = { kind: "stopped", reason: "refusal", provenance: "agent" };
|
|
9638
9638
|
// A missing/normal final assistant with a non-failing loop stop reason is a
|
|
9639
|
-
// normal turn end; only explicit error/aborted assistants or
|
|
9640
|
-
// loop stop reasons are failures. Text is never parsed.
|
|
9639
|
+
// normal turn end; only explicit error/aborted assistants or other
|
|
9640
|
+
// non-completed loop stop reasons are failures. Text is never parsed.
|
|
9641
9641
|
else if (
|
|
9642
|
-
(event.stopReason === undefined || event.stopReason === "completed") &&
|
|
9642
|
+
(event.stopReason === undefined || event.stopReason === "completed" || event.stopReason === "paused") &&
|
|
9643
9643
|
finalAssistant?.stopReason !== "error" &&
|
|
9644
9644
|
finalAssistant?.stopReason !== "aborted"
|
|
9645
9645
|
)
|
|
@@ -297,7 +297,7 @@ export const SDK_LIFECYCLE_ROUTER_PROTOCOL_VERSION = 1;
|
|
|
297
297
|
* Generation 193 gates streamed content on negotiated observer capabilities,
|
|
298
298
|
* so existing owners are replaced before exposing the new observer path.
|
|
299
299
|
*/
|
|
300
|
-
export const DAEMON_GENERATION =
|
|
300
|
+
export const DAEMON_GENERATION = 197;
|
|
301
301
|
|
|
302
302
|
/**
|
|
303
303
|
* Serving-compatibility boundary for daemon lifecycle requests. Epoch 7
|