dsh-ntr 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +9 -6
- package/README.md +9 -6
- package/cordis.patch.yml +12 -0
- package/lib/bridge.js +654 -85
- package/lib/client/panel.js +224 -78
- package/lib/client/store.js +133 -1
- package/lib/client.js +702 -304
- package/lib/client.js.map +1 -1
- package/lib/impl.js +47 -0
- package/lib/index.js +15 -1
- package/lib/types/bridge.d.ts +208 -6
- package/lib/types/client/panel.d.ts +0 -1
- package/lib/types/client/store.d.ts +54 -1
- package/lib/types/impl.d.ts +26 -1
- package/lib/types/protocol.d.ts +73 -0
- package/lib/types/update.d.ts +183 -0
- package/lib/update.js +681 -0
- package/package.json +5 -1
- package/src/bridge.ts +700 -83
- package/src/client/panel.tsx +406 -205
- package/src/client/store.ts +146 -2
- package/src/impl.ts +66 -1
- package/src/index.ts +16 -1
- package/src/protocol.ts +76 -0
- package/src/update.ts +699 -0
package/lib/impl.js
CHANGED
|
@@ -514,6 +514,22 @@ export function validateConfig(raw) {
|
|
|
514
514
|
if (typeof staleCancelMaxRetries !== 'number' || !Number.isInteger(staleCancelMaxRetries) || staleCancelMaxRetries < 0 || staleCancelMaxRetries > 5) {
|
|
515
515
|
throw new AcpError('bad_config', 'acp: staleCancelMaxRetries must be an integer in [0, 5]');
|
|
516
516
|
}
|
|
517
|
+
// Stall-recovery windows (inactivity watchdog). Two lengths on purpose: a
|
|
518
|
+
// silent turn with the floor is a stuck agent, but a turn waiting on a TOOL
|
|
519
|
+
// call is a working agent, and the tool half may take as long as the guard's
|
|
520
|
+
// own tool-output wait. `0` means "off" / "fall back", never "expire now".
|
|
521
|
+
const turnStallMs = value.turnStallMs ?? 600_000;
|
|
522
|
+
if (typeof turnStallMs !== 'number' || !Number.isInteger(turnStallMs) || turnStallMs < 0 || turnStallMs > 86_400_000) {
|
|
523
|
+
throw new AcpError('bad_config', 'acp: turnStallMs must be an integer in [0, 86400000] (0 = disable the watchdog)');
|
|
524
|
+
}
|
|
525
|
+
const turnStallToolMs = value.turnStallToolMs ?? 1_800_000;
|
|
526
|
+
if (typeof turnStallToolMs !== 'number' || !Number.isInteger(turnStallToolMs) || turnStallToolMs < 0 || turnStallToolMs > 86_400_000) {
|
|
527
|
+
throw new AcpError('bad_config', 'acp: turnStallToolMs must be an integer in [0, 86400000] (0 = fall back to turnStallMs)');
|
|
528
|
+
}
|
|
529
|
+
const turnStallTickMs = value.turnStallTickMs ?? 30_000;
|
|
530
|
+
if (typeof turnStallTickMs !== 'number' || !Number.isInteger(turnStallTickMs) || turnStallTickMs < 1 || turnStallTickMs > 60_000) {
|
|
531
|
+
throw new AcpError('bad_config', 'acp: turnStallTickMs must be an integer in [1, 60000]');
|
|
532
|
+
}
|
|
517
533
|
return {
|
|
518
534
|
agents,
|
|
519
535
|
transcriptMaxEntries: max,
|
|
@@ -524,6 +540,9 @@ export function validateConfig(raw) {
|
|
|
524
540
|
cancelFallbackMs,
|
|
525
541
|
staleCancelRetryDelayMs,
|
|
526
542
|
staleCancelMaxRetries,
|
|
543
|
+
turnStallMs,
|
|
544
|
+
turnStallToolMs,
|
|
545
|
+
turnStallTickMs,
|
|
527
546
|
};
|
|
528
547
|
}
|
|
529
548
|
// ---- Workspace guard ----
|
|
@@ -1499,6 +1518,19 @@ export class PermissionLedger {
|
|
|
1499
1518
|
sessionIds() {
|
|
1500
1519
|
return [...this.pending.values()].map(e => ({ requestId: e.requestId, sessionId: e.sessionId }));
|
|
1501
1520
|
}
|
|
1521
|
+
/**
|
|
1522
|
+
* Whether ANY outstanding ask belongs to `sessionId`, a HOST session id
|
|
1523
|
+
* (`LiveSession.sessionId`) — NOT the ACP protocol session id. Used by the
|
|
1524
|
+
* stall watchdog: a turn blocked on a human is not a silent turn, so the
|
|
1525
|
+
* session with a pending permission must not be finalized as stalled.
|
|
1526
|
+
*/
|
|
1527
|
+
hasPendingFor(sessionId) {
|
|
1528
|
+
for (const entry of this.pending.values()) {
|
|
1529
|
+
if (entry.sessionId === sessionId)
|
|
1530
|
+
return true;
|
|
1531
|
+
}
|
|
1532
|
+
return false;
|
|
1533
|
+
}
|
|
1502
1534
|
/**
|
|
1503
1535
|
* Outstanding asks WITH the view the browser was shown, for the reconnect
|
|
1504
1536
|
* replay. Entries created without a view are skipped: replaying a frame the
|
|
@@ -1734,6 +1766,21 @@ export async function dispatchRpc(manager, endpoint, payload) {
|
|
|
1734
1766
|
}
|
|
1735
1767
|
case 'preset/status':
|
|
1736
1768
|
return ok({ preset: await manager.presetStatus?.() ?? null });
|
|
1769
|
+
case 'plugin/version':
|
|
1770
|
+
// Absent on a host half from before the badge existed: the browser
|
|
1771
|
+
// hides it instead of naming a version it cannot know.
|
|
1772
|
+
return ok({ version: manager.pluginVersion?.() });
|
|
1773
|
+
case 'plugin/update-check':
|
|
1774
|
+
// An older host half answers `unsupported` rather than failing: the
|
|
1775
|
+
// settings card then hides the update section.
|
|
1776
|
+
return ok(await (manager.pluginUpdateCheck?.() ?? { status: 'unsupported', current: '' }));
|
|
1777
|
+
case 'plugin/update-apply': {
|
|
1778
|
+
const version = str(body, 'version', 64);
|
|
1779
|
+
if (!/^\d+\.\d+\.\d+$/.test(version)) {
|
|
1780
|
+
throw new AcpError('bad_payload', 'acp: version must be a stable semver (x.y.z)');
|
|
1781
|
+
}
|
|
1782
|
+
return ok(await (manager.pluginUpdateApply?.(version) ?? { status: 'unsupported', current: '' }));
|
|
1783
|
+
}
|
|
1737
1784
|
case 'agent/stop':
|
|
1738
1785
|
await manager.stopAgent(str(body, 'agentId', 64));
|
|
1739
1786
|
return ok({ done: true });
|
package/lib/index.js
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* them down with the plugin fiber. Named exports only (a default export would
|
|
5
5
|
* hide the loader metadata — see harness postmortem 0001).
|
|
6
6
|
*/
|
|
7
|
-
import { ACP_PRESET_ID, AcpLlmAdapter, BridgeManager } from "./bridge.js";
|
|
7
|
+
import { ACP_PRESET_ID, AcpLlmAdapter, BridgeManager, PLUGIN_VERSION } from "./bridge.js";
|
|
8
8
|
import { EventHub, dispatchRpc, permissionReplayFrames, sessionAgentPreset, sessionAgentPresetFromLog, validateConfig } from "./impl.js";
|
|
9
|
+
import { UpdateService } from "./update.js";
|
|
9
10
|
export const name = 'dsh-ntr';
|
|
10
11
|
// Deliberately minimal: every other host service is probed structurally
|
|
11
12
|
// (`ctx.get` + a late `ctx.inject`) so a deployment without one still loads
|
|
@@ -28,8 +29,21 @@ export function apply(ctx, config) {
|
|
|
28
29
|
cancelFallbackMs: resolved.cancelFallbackMs,
|
|
29
30
|
staleCancelRetryDelayMs: resolved.staleCancelRetryDelayMs,
|
|
30
31
|
staleCancelMaxRetries: resolved.staleCancelMaxRetries,
|
|
32
|
+
turnStallMs: resolved.turnStallMs,
|
|
33
|
+
turnStallToolMs: resolved.turnStallToolMs,
|
|
34
|
+
turnStallTickMs: resolved.turnStallTickMs,
|
|
31
35
|
});
|
|
32
36
|
const adapter = new AcpLlmAdapter(manager);
|
|
37
|
+
// Built-in update: the plugin never downloads or swaps files itself. The
|
|
38
|
+
// harness plugin manager owns the install; this only relays its facts.
|
|
39
|
+
// `resolveManager` re-probes on every call so a late-loading service is
|
|
40
|
+
// picked up without an `inject` dependency (a host without it must still
|
|
41
|
+
// load this plugin, and then reports `unsupported`).
|
|
42
|
+
manager.setUpdateService(new UpdateService({
|
|
43
|
+
resolveManager: () => (typeof ctx.get === 'function' ? ctx.get('pluginManager') : undefined),
|
|
44
|
+
currentVersion: PLUGIN_VERSION,
|
|
45
|
+
isBusy: () => manager.hasActiveTurns(),
|
|
46
|
+
}));
|
|
33
47
|
// The RAW patch-file deadlines, before validation filled the defaults in.
|
|
34
48
|
// `validateConfig` returns a number for every knob, so the validated object
|
|
35
49
|
// cannot tell "the deployment asked for 10 minutes" from "the deployment said
|
package/lib/types/bridge.d.ts
CHANGED
|
@@ -9,7 +9,15 @@
|
|
|
9
9
|
import { type InitializeResponse } from '@agentclientprotocol/sdk';
|
|
10
10
|
import { EventHub, PermissionLedger, Transcript } from './impl.ts';
|
|
11
11
|
import type { AcpToolCallSnapshot, PendingPermissionView, RosterPresetRow } from './impl.ts';
|
|
12
|
-
import type { AcpAgentCatalog, AcpAgentDef, AcpApprovalDeadlines, AcpConversationChoice, AcpPresetStatus, AcpTagHalo, AgentCatalogResult, AgentProbeModelsResult, AgentTestResult, AgentView, ChatSessionView, ChatTranscriptView, DetectedAgentInfo, GenerateOptions, LlmAdapterLike, LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, ApprovalDeadlineKey, ApprovalDeadlinePatch, PermissionTimeoutMode, PreparedAdapterCall, SessionView, StreamChunk } from './protocol.ts';
|
|
12
|
+
import type { AcpAgentCatalog, AcpAgentDef, AcpApprovalDeadlines, AcpConversationChoice, AcpPresetStatus, AcpTagHalo, AgentCatalogResult, AgentProbeModelsResult, AgentTestResult, AgentView, ChatSessionView, ChatTranscriptView, DetectedAgentInfo, GenerateOptions, LlmAdapterLike, LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, ApprovalDeadlineKey, ApprovalDeadlinePatch, PermissionTimeoutMode, PluginUpdateState, PreparedAdapterCall, SessionView, StreamChunk } from './protocol.ts';
|
|
13
|
+
import { UpdateService } from './update.ts';
|
|
14
|
+
/**
|
|
15
|
+
* Version of this installed package, read from the shipped `package.json` —
|
|
16
|
+
* one level above both `src/` at test time and `lib/` at runtime. The settings
|
|
17
|
+
* badge and the ACP handshake's `clientInfo` both report it; a copied literal
|
|
18
|
+
* would go stale on every release.
|
|
19
|
+
*/
|
|
20
|
+
export declare const PLUGIN_VERSION: string;
|
|
13
21
|
interface LiveSession {
|
|
14
22
|
/** Host-minted id (stable across an agent restart; the ACP id is per-connection). */
|
|
15
23
|
sessionId: string;
|
|
@@ -37,6 +45,20 @@ interface LiveSession {
|
|
|
37
45
|
closed: boolean;
|
|
38
46
|
/** The agent process died; the session awaits a `session/load` resume. */
|
|
39
47
|
released: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* When the agent last showed ANY sign of life on this session: an update, a
|
|
50
|
+
* permission ask (arrival or answer), or a turn start. The stall watchdog
|
|
51
|
+
* measures silence from here rather than from the prompt, which may
|
|
52
|
+
* legitimately be followed by a long tool run.
|
|
53
|
+
*/
|
|
54
|
+
lastAgentActivityAt: number;
|
|
55
|
+
/**
|
|
56
|
+
* Tool calls the agent announced and has not finished, by ACP `toolCallId`.
|
|
57
|
+
* A SET, not a flag: parallel calls finish independently, and one sibling's
|
|
58
|
+
* `completed` must not clear the long tool window while another is still
|
|
59
|
+
* running. Non-empty = the agent is working on its own clock.
|
|
60
|
+
*/
|
|
61
|
+
activeToolCalls: Set<string>;
|
|
40
62
|
/**
|
|
41
63
|
* Owning DSH chat session id, when the session was opened through the LLM
|
|
42
64
|
* adapter (`AcpLlmAdapter`). It is what the conversation's permission card
|
|
@@ -90,6 +112,36 @@ interface LiveSession {
|
|
|
90
112
|
* that answer means the message was silently dropped.
|
|
91
113
|
*/
|
|
92
114
|
cancelRequestedAtTurn?: number;
|
|
115
|
+
/**
|
|
116
|
+
* The {@link turnSeq} of a turn the stall watchdog force-finalized while the
|
|
117
|
+
* agent still held its `session/prompt`. The agent is presumed wedged, so the
|
|
118
|
+
* NEXT prompt reclaims the process (`BridgeManager.prepareStalledSession`)
|
|
119
|
+
* instead of queueing behind a turn that may never answer. Cleared when the
|
|
120
|
+
* late response lands or when the reclaim replaces the process.
|
|
121
|
+
*/
|
|
122
|
+
orphanPromptTurnSeq?: number;
|
|
123
|
+
/**
|
|
124
|
+
* The {@link turnSeq} of a turn a reclaim (`prepareStalledSession`) ended by
|
|
125
|
+
* closing the connection while the agent still held its `session/prompt`.
|
|
126
|
+
* The close rejects that request, and the caller's catch would broadcast a
|
|
127
|
+
* second `turn/end` for a turn already finalized: this marker tells the
|
|
128
|
+
* late `then`/`catch` to drop the answer instead. Set by the reclaim, cleared
|
|
129
|
+
* when that answer lands, at every new turn start, and by a resume.
|
|
130
|
+
*/
|
|
131
|
+
reclaimedTurnSeq?: number;
|
|
132
|
+
/**
|
|
133
|
+
* The controller of the turn currently streaming through this session.
|
|
134
|
+
*
|
|
135
|
+
* Chat turns are ALSO indexed by chat id ({@link BridgeManager}'s
|
|
136
|
+
* `turnStreams`) because their continuation steps arrive as fresh
|
|
137
|
+
* `streamPrompt` calls that look the turn up by chat id. An EPHEMERAL turn
|
|
138
|
+
* (`options.sessionId === undefined`) has no chat id to index under, so this
|
|
139
|
+
* attachment is the only way the stall watchdog can find its controller —
|
|
140
|
+
* without it a silent ephemeral turn falls into the `/acp` branch, which
|
|
141
|
+
* leaves the driver's segment queue open instead of finishing it. Cleared by
|
|
142
|
+
* the controller's own `retire` when it still points at it.
|
|
143
|
+
*/
|
|
144
|
+
turnController?: TurnStreamController;
|
|
93
145
|
transcript: Transcript;
|
|
94
146
|
}
|
|
95
147
|
/** Reliability tuning, threaded from `AcpPluginConfig` through the manager. */
|
|
@@ -155,6 +207,23 @@ export interface BridgeTuning {
|
|
|
155
207
|
* racing a concurrent `session/prompt` into the agent.
|
|
156
208
|
*/
|
|
157
209
|
turnSettleMs?: number;
|
|
210
|
+
/**
|
|
211
|
+
* How long a turn may stay SILENT — no update, no ask, no turn start — before
|
|
212
|
+
* the watchdog force-finalizes it as stalled. The turn is only watched while
|
|
213
|
+
* the agent is the one holding the floor (a segment on air, no pending ask),
|
|
214
|
+
* and the process survives: the next message reclaims it.
|
|
215
|
+
*
|
|
216
|
+
* `0` disables the watchdog.
|
|
217
|
+
*/
|
|
218
|
+
turnStallMs?: number;
|
|
219
|
+
/**
|
|
220
|
+
* The longer silence window used while an ACP tool call is in progress: the
|
|
221
|
+
* agent runs the tool on its own clock, so the plain turn window would call
|
|
222
|
+
* a healthy long-running tool stalled. `0` falls back to `turnStallMs`.
|
|
223
|
+
*/
|
|
224
|
+
turnStallToolMs?: number;
|
|
225
|
+
/** How often the watchdog scans for silent turns. */
|
|
226
|
+
turnStallTickMs?: number;
|
|
158
227
|
/**
|
|
159
228
|
* Default idle-release window for agents without their own
|
|
160
229
|
* `AcpAgentDef.idleTimeoutMs`: a READY process that holds no live session
|
|
@@ -248,8 +317,19 @@ export declare class AgentBridge {
|
|
|
248
317
|
private lastActivityAt;
|
|
249
318
|
/** Throwaway catalog probes in flight — they hold no live session of their own. */
|
|
250
319
|
private pendingProbes;
|
|
251
|
-
/**
|
|
252
|
-
|
|
320
|
+
/**
|
|
321
|
+
* Host→agent session commands in flight (mode / config); not turns. Counted
|
|
322
|
+
* PER SESSION: a command the wedged agent never answers must not pin the
|
|
323
|
+
* bridge against reclaiming THAT session (it is exactly the state the
|
|
324
|
+
* watchdog is recovering from), while another session's command still does.
|
|
325
|
+
*/
|
|
326
|
+
private readonly pendingCommands;
|
|
327
|
+
/**
|
|
328
|
+
* `session/new` requests in flight. The session does not exist in `sessions`
|
|
329
|
+
* yet, so nothing else would tell a teardown (`releaseAll` / idle release)
|
|
330
|
+
* that a caller is waiting on the child it is about to kill.
|
|
331
|
+
*/
|
|
332
|
+
private pendingCreates;
|
|
253
333
|
/**
|
|
254
334
|
* Whether a REAL session has ever been registered on this bridge. Probes
|
|
255
335
|
* bypass session bookkeeping, so a bridge that has never hosted anything was
|
|
@@ -383,6 +463,12 @@ export declare class AgentBridge {
|
|
|
383
463
|
* @returns the settler (idempotent; also used by connection loss).
|
|
384
464
|
*/
|
|
385
465
|
beginTurn(session: LiveSession): () => void;
|
|
466
|
+
/**
|
|
467
|
+
* Open a session's settle gate without owning its turn stream: the turn's
|
|
468
|
+
* stream is gone (a resume, a release) but the next prompt still waits on
|
|
469
|
+
* this promise, and nothing is left that could ever resolve it.
|
|
470
|
+
*/
|
|
471
|
+
private openSettleGate;
|
|
386
472
|
view(): AgentView;
|
|
387
473
|
/**
|
|
388
474
|
* Work a teardown would interrupt: a turn in flight (or one the agent is
|
|
@@ -390,6 +476,18 @@ export declare class AgentBridge {
|
|
|
390
476
|
* probe, a resume, or a start. A registered-but-idle session is NOT work.
|
|
391
477
|
*/
|
|
392
478
|
private hasActiveWork;
|
|
479
|
+
/**
|
|
480
|
+
* Active work on this bridge EXCEPT `session`'s own — what a restart-carrying
|
|
481
|
+
* reclaim (the stall watchdog's follow-up) would destroy. Killing this child
|
|
482
|
+
* restarts every ACP session on the connection, so a live conversation that
|
|
483
|
+
* is mid-turn, mid-resume, or waiting on a human pins the process: its
|
|
484
|
+
* context is the one thing a restart cannot give back.
|
|
485
|
+
*
|
|
486
|
+
* `session`'s OWN mode/config command is deliberately not counted: an agent
|
|
487
|
+
* that ignores `session/set_mode` would otherwise make its own stall
|
|
488
|
+
* unrecoverable — the command's counter is exactly the evidence of the wedge.
|
|
489
|
+
*/
|
|
490
|
+
hasOtherActiveWork(session: LiveSession): boolean;
|
|
393
491
|
/**
|
|
394
492
|
* Nothing depends on this child's identity right now: no live session and no
|
|
395
493
|
* work. This is the WORKSPACE MOVE's predicate. It stays stricter than the
|
|
@@ -398,6 +496,13 @@ export declare class AgentBridge {
|
|
|
398
496
|
* state for a restart on every cross-workspace turn.
|
|
399
497
|
*/
|
|
400
498
|
private isQuiet;
|
|
499
|
+
/**
|
|
500
|
+
* A turn is in flight (or the agent has not settled its response yet) on any
|
|
501
|
+
* of this bridge's sessions. `settled` only stays non-undefined while the
|
|
502
|
+
* agent's answer is outstanding — the settler clears it on completion — so
|
|
503
|
+
* this is real work, not an idle session.
|
|
504
|
+
*/
|
|
505
|
+
hasActiveTurn(): boolean;
|
|
401
506
|
/**
|
|
402
507
|
* An idle teardown is safe when nothing is WORKING. Idle live sessions do not
|
|
403
508
|
* block it: `releaseAll` parks them as `released` (never closed), and the next
|
|
@@ -437,6 +542,8 @@ export declare class AgentBridge {
|
|
|
437
542
|
* @param spawnCwd - where the agent PROCESS should run; see `ensureReady`.
|
|
438
543
|
*/
|
|
439
544
|
createSession(cwd: string | undefined, extraRoots?: readonly string[], spawnCwd?: string): Promise<LiveSession>;
|
|
545
|
+
/** The body of {@link createSession}; the caller keeps `pendingCreates` for it. */
|
|
546
|
+
private openSession;
|
|
440
547
|
/**
|
|
441
548
|
* Spawn a throwaway ACP session purely to read its `configOptions` (the
|
|
442
549
|
* model catalog probe). Deliberately bypasses the live-session bookkeeping
|
|
@@ -482,6 +589,17 @@ export declare class AgentBridge {
|
|
|
482
589
|
* successor — so a moved-on {@link LiveSession.turnSeq} disarms it.
|
|
483
590
|
*/
|
|
484
591
|
private armCancelFallback;
|
|
592
|
+
/**
|
|
593
|
+
* Force the local end of one turn whose agent never acknowledged it: clear
|
|
594
|
+
* `running`, open the settle gate, and broadcast the turn end with `turnId:
|
|
595
|
+
* ''` (there is no agent response to attribute it to). Shared by the cancel
|
|
596
|
+
* fallback and the stall watchdog.
|
|
597
|
+
*
|
|
598
|
+
* Generation-guarded: a turn that already ended — or that a later turn
|
|
599
|
+
* superseded — is not this call's to finalize. Returns whether it acted; a
|
|
600
|
+
* `false` means the caller was late and must leave the session alone.
|
|
601
|
+
*/
|
|
602
|
+
finalizeTurn(session: LiveSession, turnGen: number, message: string): boolean;
|
|
485
603
|
/**
|
|
486
604
|
* Bring a released session back on a fresh agent process via
|
|
487
605
|
* `session/load` ("send to recover"). Concurrent callers share one inflight
|
|
@@ -512,6 +630,18 @@ export declare class AgentBridge {
|
|
|
512
630
|
hasSession(sessionId: string): LiveSession | undefined;
|
|
513
631
|
/** Kill the child and drop every local session bound to this connection. */
|
|
514
632
|
stop(reason?: 'closed' | 'agent-stopped' | 'agent-died'): Promise<void>;
|
|
633
|
+
/**
|
|
634
|
+
* Drop every live session record and kill the child WITHOUT asking the agent
|
|
635
|
+
* to close them. `stop` sends `session/close` per session, and a wedged agent
|
|
636
|
+
* that ignores it would hang the caller (the stall reclaim) — the very agent
|
|
637
|
+
* this teardown exists to get rid of. The local teardown is what frees the
|
|
638
|
+
* sessions, so it runs unconditionally and immediately.
|
|
639
|
+
*
|
|
640
|
+
* Deliberately does NOT clear `released`: those records are constraint 10's
|
|
641
|
+
* — a released session survives a process death and is resumed by its next
|
|
642
|
+
* message. This is a stop, not a discard.
|
|
643
|
+
*/
|
|
644
|
+
forceReset(reason: 'agent-stopped'): void;
|
|
515
645
|
private killChild;
|
|
516
646
|
/**
|
|
517
647
|
* Answer one ACP permission ask.
|
|
@@ -805,6 +935,8 @@ interface SegmentState {
|
|
|
805
935
|
index: number;
|
|
806
936
|
activeType: 'reasoning' | 'text' | 'tool-call' | null;
|
|
807
937
|
activeText: string;
|
|
938
|
+
/** UTF-16 units of this segment's text and reasoning chunks (the usage estimate). */
|
|
939
|
+
outputChars: number;
|
|
808
940
|
/** The open tool-call block's snapshot, while `activeType === 'tool-call'`. */
|
|
809
941
|
activeToolCall: AcpToolCallSnapshot | undefined;
|
|
810
942
|
/** Whether this segment opened at least one tool-call block. */
|
|
@@ -836,6 +968,7 @@ type TurnSettle = {
|
|
|
836
968
|
} | {
|
|
837
969
|
kind: 'error';
|
|
838
970
|
message: string;
|
|
971
|
+
code?: string;
|
|
839
972
|
};
|
|
840
973
|
/**
|
|
841
974
|
* The state of ONE ACP prompt, shared across every driver step of the turn.
|
|
@@ -845,7 +978,7 @@ type TurnSettle = {
|
|
|
845
978
|
* `transcript.since(cursor)` — events that arrive while no segment is open are
|
|
846
979
|
* recorded, never lost), the seen-call registry (a call id gets exactly one
|
|
847
980
|
* card per turn, wherever its restatements land), and the turn-level facts
|
|
848
|
-
* (`sawVisibleBlock
|
|
981
|
+
* (`sawVisibleBlock`) that outlive any single segment.
|
|
849
982
|
*/
|
|
850
983
|
declare class TurnStreamController {
|
|
851
984
|
private readonly manager;
|
|
@@ -860,7 +993,6 @@ declare class TurnStreamController {
|
|
|
860
993
|
private disposed;
|
|
861
994
|
private readonly seenCalls;
|
|
862
995
|
private readonly announcedKinds;
|
|
863
|
-
private latestUsage;
|
|
864
996
|
/** Narrative lines (permission asks) that arrived while no segment was open. */
|
|
865
997
|
private readonly pendingProse;
|
|
866
998
|
sawVisibleBlock: boolean;
|
|
@@ -874,6 +1006,8 @@ declare class TurnStreamController {
|
|
|
874
1006
|
get replayCursor(): number;
|
|
875
1007
|
/** Whether the agent already settled its `session/prompt` for this turn. */
|
|
876
1008
|
get hasSettled(): boolean;
|
|
1009
|
+
/** Whether a segment is on air — the agent is expected to speak into a live driver step. */
|
|
1010
|
+
get hasOpenSegment(): boolean;
|
|
877
1011
|
/** Generation guard: a controller whose turn has been superseded must not touch the session. */
|
|
878
1012
|
get stale(): boolean;
|
|
879
1013
|
/**
|
|
@@ -893,6 +1027,13 @@ declare class TurnStreamController {
|
|
|
893
1027
|
abortActiveSegment(): void;
|
|
894
1028
|
/** Stop observing and release the manager's index. Idempotent. */
|
|
895
1029
|
dispose(): void;
|
|
1030
|
+
/**
|
|
1031
|
+
* Stop observing and release the manager's index WITHOUT touching the
|
|
1032
|
+
* process-wide tool-output waiters. A late settle of a superseded turn must
|
|
1033
|
+
* use this: those waiters now belong to the turn on air, and waking them
|
|
1034
|
+
* early would hand the guard a "no output" answer for a tool still running.
|
|
1035
|
+
*/
|
|
1036
|
+
retire(): void;
|
|
896
1037
|
private finishSegment;
|
|
897
1038
|
/** A segment opened on an already-settled turn: replay, then close it out. */
|
|
898
1039
|
private finishAfterSettle;
|
|
@@ -908,7 +1049,6 @@ declare class TurnStreamController {
|
|
|
908
1049
|
private pushProse;
|
|
909
1050
|
/** Events that arrive between segments: recorded, replayed into the next one. */
|
|
910
1051
|
private recordWithoutSegment;
|
|
911
|
-
private noteUsage;
|
|
912
1052
|
/** Record a tool call's output (idempotent) and wake any waiter. */
|
|
913
1053
|
private noteToolRecord;
|
|
914
1054
|
/**
|
|
@@ -1016,6 +1156,28 @@ export declare class BridgeManager {
|
|
|
1016
1156
|
forgetTurnStream(controller: TurnStreamController): void;
|
|
1017
1157
|
/** Tear down the turn streams of every session of one bridge (close/release/stop). */
|
|
1018
1158
|
private disposeTurnStreamsOf;
|
|
1159
|
+
/**
|
|
1160
|
+
* The stall watchdog's scan loop. Armed lazily at the first turn start and
|
|
1161
|
+
* DROPPED once nothing anywhere is running: a manager that never runs a turn
|
|
1162
|
+
* never holds a timer, and one that goes quiet does not keep the host awake.
|
|
1163
|
+
*/
|
|
1164
|
+
private stallTimer?;
|
|
1165
|
+
private armStallTimer;
|
|
1166
|
+
/**
|
|
1167
|
+
* Force-finalize turns the agent has gone silent in.
|
|
1168
|
+
*
|
|
1169
|
+
* "Silent" is scoped to the stretch where the AGENT owes the turn a word: a
|
|
1170
|
+
* running session with a segment on air and no pending ask. A turn waiting on
|
|
1171
|
+
* the host (tool cards executing between segments) or on a human (an
|
|
1172
|
+
* unanswered permission) is not stuck and is skipped.
|
|
1173
|
+
*
|
|
1174
|
+
* Firing does NOT kill the process (see `LiveSession.orphanPromptTurnSeq`):
|
|
1175
|
+
* the session, its transcript and its binding survive, and the next message
|
|
1176
|
+
* reclaims the process. The turn's streaming face ends through its own
|
|
1177
|
+
* controller when there is one, so the driver step on air closes with an
|
|
1178
|
+
* error instead of being torn down mid-stream.
|
|
1179
|
+
*/
|
|
1180
|
+
private reapStalledTurns;
|
|
1019
1181
|
constructor(defs: readonly AcpAgentDef[], hub: EventHub, transcriptMax: number, configStoragePath?: string | undefined, catalogStoragePath?: string | undefined,
|
|
1020
1182
|
/** Auto-probe agents for model catalogs (boot + lazy); manual probes always run. */
|
|
1021
1183
|
probeModelsOnBoot?: boolean, tuning?: BridgeTuning);
|
|
@@ -1252,6 +1414,23 @@ export declare class BridgeManager {
|
|
|
1252
1414
|
* the block rather than claiming the preset is gone.
|
|
1253
1415
|
*/
|
|
1254
1416
|
presetStatus(): Promise<AcpPresetStatus | undefined>;
|
|
1417
|
+
/** The installed package's version (the settings page's badge). */
|
|
1418
|
+
pluginVersion(): string;
|
|
1419
|
+
/** Wire the built-in updater (host entry only; absent in pure tests). */
|
|
1420
|
+
setUpdateService(service?: UpdateService): void;
|
|
1421
|
+
/** Registry check for the settings card; `unsupported` when no updater is wired. */
|
|
1422
|
+
pluginUpdateCheck(): Promise<PluginUpdateState>;
|
|
1423
|
+
/** Delegate one install to the host plugin manager; never touches files itself. */
|
|
1424
|
+
pluginUpdateApply(version: string): Promise<PluginUpdateState>;
|
|
1425
|
+
/**
|
|
1426
|
+
* Whether any ACP work is in flight. An update install may restart the host,
|
|
1427
|
+
* so check/apply describe the host as busy while a turn is running.
|
|
1428
|
+
*
|
|
1429
|
+
* Segmented turn streams count even between steps (the host is still
|
|
1430
|
+
* continuing that turn); `running` / `settled` on a session covers `/acp`
|
|
1431
|
+
* turns and the window between a prompt and the agent's acknowledgement.
|
|
1432
|
+
*/
|
|
1433
|
+
hasActiveTurns(): boolean;
|
|
1255
1434
|
private getStoragePath;
|
|
1256
1435
|
/**
|
|
1257
1436
|
* Read the agent registry file.
|
|
@@ -1522,6 +1701,8 @@ export declare class BridgeManager {
|
|
|
1522
1701
|
prompt(sessionId: string, text: string): Promise<{
|
|
1523
1702
|
turnId: string;
|
|
1524
1703
|
}>;
|
|
1704
|
+
/** The late answer landed (or failed): the turn is no longer an orphan. */
|
|
1705
|
+
private clearOrphanPrompt;
|
|
1525
1706
|
cancel(sessionId: string): Promise<void>;
|
|
1526
1707
|
closeSession(sessionId: string): Promise<void>;
|
|
1527
1708
|
setMode(sessionId: string, modeId: string): Promise<void>;
|
|
@@ -1539,6 +1720,11 @@ export declare class BridgeManager {
|
|
|
1539
1720
|
shutdownAll(): Promise<void>;
|
|
1540
1721
|
/** Host-side reader of the harness preset roster (`ctx.agentPresets`), for `presetStatus`. */
|
|
1541
1722
|
private presetRosterReader?;
|
|
1723
|
+
/**
|
|
1724
|
+
* The built-in updater, wired by the host entry. Absent in pure bridge tests —
|
|
1725
|
+
* the update RPCs then answer `unsupported` instead of inventing a source.
|
|
1726
|
+
*/
|
|
1727
|
+
private updateService?;
|
|
1542
1728
|
/**
|
|
1543
1729
|
* The plugin's own default ACP agent (the panel's ★ gesture): which agent an
|
|
1544
1730
|
* ACP-mode conversation starts on. Host-side state in the agent registry
|
|
@@ -1633,6 +1819,22 @@ export declare class BridgeManager {
|
|
|
1633
1819
|
*/
|
|
1634
1820
|
private seedChatChoiceFromCatalog;
|
|
1635
1821
|
getOrCreateDshSession(dshSessionId: string | undefined, agentId: string, cwd?: string): Promise<LiveSession>;
|
|
1822
|
+
/**
|
|
1823
|
+
* Take the agent process back from a turn the stall watchdog abandoned.
|
|
1824
|
+
*
|
|
1825
|
+
* The wedged agent still holds its `session/prompt`, so the next message
|
|
1826
|
+
* cannot simply be sent: it would queue behind a turn that may never answer.
|
|
1827
|
+
* The process is restarted instead — unless another conversation on the same
|
|
1828
|
+
* connection is doing real work, because killing the child destroys THEIR
|
|
1829
|
+
* context too, and "retry after they finish" is the honest answer.
|
|
1830
|
+
*
|
|
1831
|
+
* A restart that can `session/load` costs the conversation nothing: the
|
|
1832
|
+
* session is parked `released` and the normal acquisition path resumes it, so
|
|
1833
|
+
* chat keeps its lock / `applyChatChoice` semantics and `/acp` keeps
|
|
1834
|
+
* `locateOrResume`. An agent without resume support is reset instead, and the
|
|
1835
|
+
* user is told the context is gone.
|
|
1836
|
+
*/
|
|
1837
|
+
private prepareStalledSession;
|
|
1636
1838
|
streamPrompt(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
1637
1839
|
}
|
|
1638
1840
|
export declare class AcpLlmAdapter implements LlmAdapterLike {
|
|
@@ -86,7 +86,6 @@ export declare function approvalDeadlineFacts(deadlines: AcpApprovalDeadlines):
|
|
|
86
86
|
/** A reset is only honest when something is actually being outranked. */
|
|
87
87
|
overridden: boolean;
|
|
88
88
|
};
|
|
89
|
-
/** Settings configuration section for ACP agents. */
|
|
90
89
|
export declare function AcpSettingsSection(props: WithStore): import("react").JSX.Element;
|
|
91
90
|
/** Everything the connection form edits, in one object (kept in the row). */
|
|
92
91
|
interface AgentForm {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Deliberately framework-free: React binds through `useSyncExternalStore`.
|
|
7
7
|
*/
|
|
8
|
-
import type { AcpApprovalDeadlines, AcpConversationChoice, AcpPresetStatus, AcpTagHalo, AgentCatalogResult, AgentProbeModelsResult, AgentSavePayload, AgentTestResult, AgentView, ChatSessionView, DetectedAgentInfo, PermissionTimeoutMode, SessionView } from '../protocol.ts';
|
|
8
|
+
import type { AcpApprovalDeadlines, AcpConversationChoice, AcpPresetStatus, AcpTagHalo, AgentCatalogResult, AgentProbeModelsResult, AgentSavePayload, AgentTestResult, AgentView, ChatSessionView, DetectedAgentInfo, PermissionTimeoutMode, PluginUpdateState, SessionView } from '../protocol.ts';
|
|
9
9
|
/** One slash command the ACP agent declared, whole row (the menu draws name, description and `input.hint`). */
|
|
10
10
|
export type AgentCommand = NonNullable<ChatSessionView['availableCommands']>[number];
|
|
11
11
|
/** The unary RPC face (narrowed `ctx.connection.rpc.call`). */
|
|
@@ -304,6 +304,18 @@ export interface UiState {
|
|
|
304
304
|
* (`preset/status` answering `null`), in which case the panel hides the block.
|
|
305
305
|
*/
|
|
306
306
|
preset?: AcpPresetStatus;
|
|
307
|
+
/**
|
|
308
|
+
* The installed host package's version (`plugin/version`), rendered as the
|
|
309
|
+
* settings page badge. Absent on a host half without the endpoint, which
|
|
310
|
+
* hides the badge rather than naming a version this browser invented.
|
|
311
|
+
*/
|
|
312
|
+
pluginVersion?: string;
|
|
313
|
+
/**
|
|
314
|
+
* The running version and its update status (`plugin/update-check`). Absent
|
|
315
|
+
* on a host half without the endpoint, which hides the update entry instead
|
|
316
|
+
* of offering an install this browser cannot perform.
|
|
317
|
+
*/
|
|
318
|
+
pluginUpdate?: PluginUpdateState;
|
|
307
319
|
}
|
|
308
320
|
interface Listener {
|
|
309
321
|
(): void;
|
|
@@ -375,6 +387,22 @@ export declare class AcpStore {
|
|
|
375
387
|
private readonly cachedModelReads;
|
|
376
388
|
/** Counts behind the last successful explicit re-detect, so a locale switch can re-render its summary. */
|
|
377
389
|
private detectCounts;
|
|
390
|
+
/** When {@link UiState.pluginUpdate} was last read from the host (epoch ms). */
|
|
391
|
+
private pluginUpdateFetchedAt;
|
|
392
|
+
/**
|
|
393
|
+
* The shared in-flight `plugin/update-check`. The settings section mounts
|
|
394
|
+
* twice (both entries) and the update popover asks too; without this, two
|
|
395
|
+
* mounts in the same tick each fire their own registry read.
|
|
396
|
+
*/
|
|
397
|
+
private pluginUpdateInflight?;
|
|
398
|
+
/** Re-entry guard for `plugin/update-apply` (a double click is one install). */
|
|
399
|
+
private pluginUpdateApplying;
|
|
400
|
+
/**
|
|
401
|
+
* Update read generation. An apply bumps it so a check that was already in
|
|
402
|
+
* flight (started before the install) cannot land afterwards and overwrite
|
|
403
|
+
* the install's verdict with the pre-install `available`.
|
|
404
|
+
*/
|
|
405
|
+
private pluginUpdateGeneration;
|
|
378
406
|
constructor(rpc: RpcCall, locale: (zh: string, en: string) => string);
|
|
379
407
|
subscribe(listener: Listener): () => void;
|
|
380
408
|
getSnapshot(): UiState;
|
|
@@ -398,6 +426,31 @@ export declare class AcpStore {
|
|
|
398
426
|
private call;
|
|
399
427
|
/** Snapshot refresh used on panel open and after a stream outage. */
|
|
400
428
|
resync(): Promise<void>;
|
|
429
|
+
/**
|
|
430
|
+
* Read the plugin's update state (`plugin/update-check`). Throttled to ten
|
|
431
|
+
* minutes unless `force`: the settings panel asks on mount, and every
|
|
432
|
+
* remount must not hit the registry.
|
|
433
|
+
*
|
|
434
|
+
* An older host serves no such endpoint (`unknown endpoint`). That is a
|
|
435
|
+
* normal degradation, not an error worth a notice: the answer is `undefined`
|
|
436
|
+
* and the panel simply keeps its update entry hidden.
|
|
437
|
+
*/
|
|
438
|
+
checkPluginUpdate(force?: boolean): Promise<PluginUpdateState | undefined>;
|
|
439
|
+
private readPluginUpdate;
|
|
440
|
+
/**
|
|
441
|
+
* Install one exact version (`plugin/update-apply`). The status flips to
|
|
442
|
+
* `applying` first so the badge answers the click; the host's answer then
|
|
443
|
+
* becomes the state.
|
|
444
|
+
*
|
|
445
|
+
* A failed call does NOT prove the install did not happen: on an HMR host
|
|
446
|
+
* the update uninstalls the plugin before reinstalling it, which can cut the
|
|
447
|
+
* response. Whatever the error, the disk may already carry the new version,
|
|
448
|
+
* so the state becomes `restart-required` (the one neutral verdict for "the
|
|
449
|
+
* running code is not the disk's") with `installed` dropped (unknown, not a
|
|
450
|
+
* fact to display) and the notice asks for a reload rather than restoring
|
|
451
|
+
* `available` and pretending nothing changed.
|
|
452
|
+
*/
|
|
453
|
+
applyPluginUpdate(version: string): Promise<PluginUpdateState | undefined>;
|
|
401
454
|
detectAgents(): Promise<void>;
|
|
402
455
|
private detectSummaryOf;
|
|
403
456
|
saveAgent(payload: AgentSavePayload): Promise<void>;
|
package/lib/types/impl.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* fakes.
|
|
6
6
|
*/
|
|
7
7
|
import { randomUUID } from './ids.ts';
|
|
8
|
-
import type { AcpAgentCatalog, AcpAgentDef, AcpApprovalDeadlines, AcpConversationChoice, AcpEvent, AcpMcpServerDef, AcpPluginConfig, AcpPresetStatus, AcpTagHalo, AgentCatalogResult, AgentProbeModelsResult, AgentTestResult, AgentView, ApprovalDeadlineKey, ApprovalDeadlinePatch, ChatSessionView, ChatTranscriptView, DetectedAgentInfo, PermissionOptionView, PermissionOutcome, PermissionTimeoutMode, RpcResult } from './protocol.ts';
|
|
8
|
+
import type { AcpAgentCatalog, AcpAgentDef, AcpApprovalDeadlines, AcpConversationChoice, AcpEvent, AcpMcpServerDef, AcpPluginConfig, AcpPresetStatus, AcpTagHalo, AgentCatalogResult, AgentProbeModelsResult, AgentTestResult, AgentView, ApprovalDeadlineKey, ApprovalDeadlinePatch, ChatSessionView, ChatTranscriptView, DetectedAgentInfo, PermissionOptionView, PermissionOutcome, PermissionTimeoutMode, PluginUpdateState, RpcResult } from './protocol.ts';
|
|
9
9
|
export { randomUUID };
|
|
10
10
|
/** Structured failure the RPC layer converts to `{ok:false, error}` and `apply()` surfaces at boot. */
|
|
11
11
|
export declare class AcpError extends Error {
|
|
@@ -605,6 +605,13 @@ export declare class PermissionLedger {
|
|
|
605
605
|
requestId: string;
|
|
606
606
|
sessionId: string;
|
|
607
607
|
}>;
|
|
608
|
+
/**
|
|
609
|
+
* Whether ANY outstanding ask belongs to `sessionId`, a HOST session id
|
|
610
|
+
* (`LiveSession.sessionId`) — NOT the ACP protocol session id. Used by the
|
|
611
|
+
* stall watchdog: a turn blocked on a human is not a silent turn, so the
|
|
612
|
+
* session with a pending permission must not be finalized as stalled.
|
|
613
|
+
*/
|
|
614
|
+
hasPendingFor(sessionId: string): boolean;
|
|
608
615
|
/**
|
|
609
616
|
* Outstanding asks WITH the view the browser was shown, for the reconnect
|
|
610
617
|
* replay. Entries created without a view are skipped: replaying a frame the
|
|
@@ -808,6 +815,24 @@ export interface AcpManager {
|
|
|
808
815
|
* the registry service), which hides the panel block instead of guessing.
|
|
809
816
|
*/
|
|
810
817
|
presetStatus?(): Promise<AcpPresetStatus | undefined>;
|
|
818
|
+
/**
|
|
819
|
+
* The installed host package's version, reported for the settings page's
|
|
820
|
+
* badge. `undefined` on a host half that predates the endpoint, which hides
|
|
821
|
+
* the badge rather than showing a number the browser invented.
|
|
822
|
+
*/
|
|
823
|
+
pluginVersion?(): string | undefined;
|
|
824
|
+
/**
|
|
825
|
+
* Ask the host's plugin manager what is published for this package. Read-only
|
|
826
|
+
* toward the installation: never downloads, never swaps files, never restarts
|
|
827
|
+
* the host. Absent on a host half from before the updater existed.
|
|
828
|
+
*/
|
|
829
|
+
pluginUpdateCheck?(): Promise<PluginUpdateState>;
|
|
830
|
+
/**
|
|
831
|
+
* Install one exact stable version THROUGH the host's plugin manager. The
|
|
832
|
+
* manager owns download/restart semantics; this plugin only relays. Absent on
|
|
833
|
+
* an older host half.
|
|
834
|
+
*/
|
|
835
|
+
pluginUpdateApply?(version: string): Promise<PluginUpdateState>;
|
|
811
836
|
}
|
|
812
837
|
/** Dispatch one decoded `/acp` channel call. Never rejects; protocol errors come back as `ok:false`. */
|
|
813
838
|
export declare function dispatchRpc(manager: AcpManager, endpoint: string, payload: unknown): Promise<RpcResult<unknown>>;
|