dsh-ntr 0.3.1 → 0.3.3
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 +3 -1
- package/README.md +3 -1
- package/cordis.patch.yml +17 -1
- package/lib/bridge.js +2436 -82
- package/lib/client/acp-ui.js +103 -3
- package/lib/client/acp-view.js +24 -4
- package/lib/client/activity-pill.js +156 -0
- package/lib/client/branch-action.js +149 -0
- package/lib/client/composer-mode.js +48 -13
- package/lib/client/context-meter.js +86 -0
- package/lib/client/index.js +279 -16
- package/lib/client/model-seat.js +8 -2
- package/lib/client/panel.js +96 -29
- package/lib/client/released-banner.js +66 -0
- package/lib/client/session-controls.js +38 -13
- package/lib/client/slash-isolation.js +2 -2
- package/lib/client/store.js +380 -13
- package/lib/client.js +1364 -70
- package/lib/client.js.map +1 -1
- package/lib/impl.js +1229 -8
- package/lib/index.js +34 -0
- package/lib/protocol.js +19 -0
- package/lib/tps.js +32 -0
- package/lib/types/bridge.d.ts +850 -18
- package/lib/types/client/acp-ui.d.ts +34 -2
- package/lib/types/client/acp-view.d.ts +13 -2
- package/lib/types/client/activity-pill.d.ts +81 -0
- package/lib/types/client/branch-action.d.ts +47 -0
- package/lib/types/client/composer-mode.d.ts +28 -0
- package/lib/types/client/context-meter.d.ts +51 -0
- package/lib/types/client/model-seat.d.ts +21 -0
- package/lib/types/client/released-banner.d.ts +34 -0
- package/lib/types/client/slash-isolation.d.ts +1 -1
- package/lib/types/client/store.d.ts +108 -1
- package/lib/types/impl.d.ts +94 -7
- package/lib/types/protocol.d.ts +145 -0
- package/lib/types/tps.d.ts +17 -0
- package/package.json +2 -1
- package/src/bridge.ts +2754 -86
- package/src/client/acp-ui.tsx +123 -2
- package/src/client/acp-view.tsx +30 -5
- package/src/client/activity-pill.tsx +201 -0
- package/src/client/branch-action.tsx +178 -0
- package/src/client/composer-mode.tsx +59 -13
- package/src/client/context-meter.tsx +128 -0
- package/src/client/index.tsx +298 -15
- package/src/client/model-seat.tsx +28 -3
- package/src/client/panel.tsx +157 -40
- package/src/client/primitives.d.ts +27 -0
- package/src/client/released-banner.tsx +97 -0
- package/src/client/session-controls.tsx +34 -11
- package/src/client/slash-isolation.ts +2 -1
- package/src/client/store.ts +412 -14
- package/src/impl.ts +1251 -13
- package/src/index.ts +33 -0
- package/src/protocol.ts +145 -0
- package/src/tps.ts +40 -0
package/src/index.ts
CHANGED
|
@@ -91,6 +91,7 @@ export function apply(ctx: Context, config?: unknown): void {
|
|
|
91
91
|
turnStallMs: resolved.turnStallMs,
|
|
92
92
|
turnStallToolMs: resolved.turnStallToolMs,
|
|
93
93
|
turnStallTickMs: resolved.turnStallTickMs,
|
|
94
|
+
idleTimeoutMs: resolved.idleTimeoutMs,
|
|
94
95
|
},
|
|
95
96
|
)
|
|
96
97
|
const adapter = new AcpLlmAdapter(manager)
|
|
@@ -126,6 +127,13 @@ export function apply(ctx: Context, config?: unknown): void {
|
|
|
126
127
|
? rawConfig.permissionTimeoutMode
|
|
127
128
|
: undefined
|
|
128
129
|
manager.setConfiguredApprovalDeadlines(configuredDeadlines, configuredMode)
|
|
130
|
+
|
|
131
|
+
// The idle-release baseline the settings page's pick outranks. Only the
|
|
132
|
+
// raw value counts: `validateConfig` fills a default in, so the
|
|
133
|
+
// validated number cannot tell "the file says 10 minutes" from
|
|
134
|
+
// "the file says nothing, so 30 minutes applies".
|
|
135
|
+
const configuredIdleTimeoutMs = typeof rawConfig.idleTimeoutMs === 'number' ? rawConfig.idleTimeoutMs : undefined
|
|
136
|
+
manager.setConfiguredIdleRelease(configuredIdleTimeoutMs)
|
|
129
137
|
// Boot hook kept only so this entry point's shape does not change:
|
|
130
138
|
// `bootstrapCatalogs` is intentionally empty — boot never spawns an agent,
|
|
131
139
|
// in any environment and for any `probeModelsOnBoot` value. Catalogs fill
|
|
@@ -269,6 +277,31 @@ export function apply(ctx: Context, config?: unknown): void {
|
|
|
269
277
|
})
|
|
270
278
|
}
|
|
271
279
|
|
|
280
|
+
const wireAttachments = (injectedCtx?: Context): void => {
|
|
281
|
+
const c = injectedCtx ?? ctx
|
|
282
|
+
let attachments: unknown
|
|
283
|
+
try {
|
|
284
|
+
if (typeof c.get === 'function') {
|
|
285
|
+
attachments = c.get('attachments') ?? c.get('attachmentService')
|
|
286
|
+
}
|
|
287
|
+
} catch {}
|
|
288
|
+
try {
|
|
289
|
+
if (attachments === undefined) {
|
|
290
|
+
attachments = (c as unknown as Record<string, unknown>).attachments ?? (c as unknown as Record<string, unknown>).attachmentService
|
|
291
|
+
}
|
|
292
|
+
} catch {}
|
|
293
|
+
if (attachments !== undefined) {
|
|
294
|
+
manager.setAttachmentResolver(attachments)
|
|
295
|
+
} else {
|
|
296
|
+
manager.setAttachmentResolver(c)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
wireAttachments()
|
|
300
|
+
if (typeof ctx.inject === 'function') {
|
|
301
|
+
ctx.inject(['attachments'], injectedCtx => { wireAttachments(injectedCtx) })
|
|
302
|
+
ctx.inject(['attachmentService'], injectedCtx => { wireAttachments(injectedCtx) })
|
|
303
|
+
}
|
|
304
|
+
|
|
272
305
|
/** Registration handle: callable disposer, plus `replace` to re-announce routes. */
|
|
273
306
|
interface LlmAdapterHandle {
|
|
274
307
|
(): void
|
package/src/protocol.ts
CHANGED
|
@@ -187,6 +187,18 @@ export interface AcpPluginConfig {
|
|
|
187
187
|
* deployment baseline that override falls back to. See {@link AcpApprovalDeadlines}.
|
|
188
188
|
*/
|
|
189
189
|
permissionTimeoutMs?: number
|
|
190
|
+
/**
|
|
191
|
+
* How long a chat process may sit idle before it is released (parked, then
|
|
192
|
+
* resumed by the next message). In MILLISECONDS; default 1800000 (30 min).
|
|
193
|
+
*
|
|
194
|
+
* This is the HOST DEFAULT: an agent that names its own
|
|
195
|
+
* {@link AcpAgentDef.idleTimeoutMs} keeps its own. Editable from the
|
|
196
|
+
* settings page (「进程管理」 → `ui/idle-release-set`), which persists an
|
|
197
|
+
* override in the plugin's own state file and applies it to the running
|
|
198
|
+
* bridges without restarting anything; the value here is the deployment
|
|
199
|
+
* baseline that override falls back to. See {@link AcpIdleRelease}.
|
|
200
|
+
*/
|
|
201
|
+
idleTimeoutMs?: number
|
|
190
202
|
/**
|
|
191
203
|
* LEGACY: the old conversation-route window, kept so an existing
|
|
192
204
|
* `cordis.patch.yml` still says what it meant. Folded into
|
|
@@ -446,6 +458,18 @@ export type AcpEvent =
|
|
|
446
458
|
*/
|
|
447
459
|
| { type: 'permission/synced'; requestIds: string[] }
|
|
448
460
|
| { type: 'turn/start'; sessionId: string; turnId: string; text: string }
|
|
461
|
+
| {
|
|
462
|
+
type: 'turn/metrics'
|
|
463
|
+
/** ACP session id (the store keys live burst state by it). */
|
|
464
|
+
sessionId: string
|
|
465
|
+
/** DSH chat session id (routing/context only). */
|
|
466
|
+
dshSessionId: string
|
|
467
|
+
turnId: string
|
|
468
|
+
/** Session aggregate INCLUDING the current turn when measurable. */
|
|
469
|
+
outputTokens: number
|
|
470
|
+
burstMs: number
|
|
471
|
+
final: boolean
|
|
472
|
+
}
|
|
449
473
|
| { type: 'turn/end'; sessionId: string; turnId: string; stopReason?: string; error?: string }
|
|
450
474
|
/**
|
|
451
475
|
* A DSH chat conversation was locked to its agent at the start of its first
|
|
@@ -469,6 +493,14 @@ export interface PermissionOptionView {
|
|
|
469
493
|
|
|
470
494
|
export type PermissionOutcome = { outcome: 'selected'; optionId: string } | { outcome: 'cancelled' }
|
|
471
495
|
|
|
496
|
+
/** Plugin-side measurable burst aggregate for one ACP session (not host turn timings). */
|
|
497
|
+
export interface SessionBurst {
|
|
498
|
+
/** Sum of estimated output tokens over measurable turns. */
|
|
499
|
+
outputTokens: number
|
|
500
|
+
/** Sum of within-burst chunk gaps over those turns; tool/approval/long pauses excluded. */
|
|
501
|
+
burstMs: number
|
|
502
|
+
}
|
|
503
|
+
|
|
472
504
|
/** Derived session snapshot for replay/reconnect (source of truth stays in the transcript). */
|
|
473
505
|
export interface SessionView {
|
|
474
506
|
sessionId: string
|
|
@@ -482,6 +514,8 @@ export interface SessionView {
|
|
|
482
514
|
availableCommands?: Array<{ name: string; description?: string; [key: string]: unknown }>
|
|
483
515
|
plan?: unknown[]
|
|
484
516
|
usage?: { used: number; size: number; cost?: { amount: number; currency: string } }
|
|
517
|
+
/** Measurable burst aggregate over this session's turns; absent until measured. */
|
|
518
|
+
burst?: SessionBurst
|
|
485
519
|
running: boolean
|
|
486
520
|
closed: boolean
|
|
487
521
|
/**
|
|
@@ -490,6 +524,14 @@ export interface SessionView {
|
|
|
490
524
|
* recover") when the agent declares the `loadSession` capability.
|
|
491
525
|
*/
|
|
492
526
|
released?: boolean
|
|
527
|
+
/**
|
|
528
|
+
* The agent-side session has history while the local buffer is empty: the
|
|
529
|
+
* record was restored from disk after a host restart (the `session/load`
|
|
530
|
+
* replay has not landed), or a resume cleared the buffer ahead of the
|
|
531
|
+
* replay. The ACP view then says history replays on restore instead of
|
|
532
|
+
* claiming "nothing yet".
|
|
533
|
+
*/
|
|
534
|
+
hasHistory?: boolean
|
|
493
535
|
/** Highest transcript seq included in this snapshot (for gap detection). */
|
|
494
536
|
seq: number
|
|
495
537
|
}
|
|
@@ -543,8 +585,38 @@ export interface ChatSessionView {
|
|
|
543
585
|
* agent can never see.
|
|
544
586
|
*/
|
|
545
587
|
availableCommands?: SessionView['availableCommands']
|
|
588
|
+
/**
|
|
589
|
+
* Context occupancy the agent last reported (`usage_update`): prompt-side
|
|
590
|
+
* used/size, not request tokens. Absent until the agent reports one; the
|
|
591
|
+
* browser live-patches this row from the update frames (see AGENTS/ui.md).
|
|
592
|
+
*/
|
|
593
|
+
usage?: SessionView['usage']
|
|
594
|
+
/** Measurable burst aggregate over this session's turns; absent until measured. */
|
|
595
|
+
burst?: SessionView['burst']
|
|
546
596
|
/** The agent process died; the session awaits a `session/load` resume. */
|
|
547
597
|
released?: boolean
|
|
598
|
+
/** The agent is running a turn on this session right now. */
|
|
599
|
+
running?: boolean
|
|
600
|
+
/**
|
|
601
|
+
* Whether this session's ACP agent can be forked (`session/fork`) into a
|
|
602
|
+
* child conversation, as the host computed it. The browser does not derive
|
|
603
|
+
* this itself — the capability is host knowledge (the agent's
|
|
604
|
+
* `initialize`), and the row is the one place it is delivered.
|
|
605
|
+
* `supported: false` carries the reason; no reason on `false` means the
|
|
606
|
+
* host half predates the field (treat as unsupported).
|
|
607
|
+
*/
|
|
608
|
+
fork?: ChatSessionForkInfo
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* The host's per-row verdict on forking one chat session's ACP session
|
|
612
|
+
* (`ChatSessionView.fork`). Computed from the agent's LAST KNOWN
|
|
613
|
+
* `sessionCapabilities.fork` (persisted with the model catalog) plus the
|
|
614
|
+
* session's own state — the browser never reads capability bits itself.
|
|
615
|
+
*/
|
|
616
|
+
export interface ChatSessionForkInfo {
|
|
617
|
+
supported: boolean
|
|
618
|
+
/** Why {@link supported} is false: the agent cannot fork, or the session cannot be forked right now. */
|
|
619
|
+
reason?: 'unsupported' | 'released' | 'busy'
|
|
548
620
|
}
|
|
549
621
|
export type ChatSessionsListResult = { sessions: ChatSessionView[] }
|
|
550
622
|
|
|
@@ -668,6 +740,63 @@ export interface TagHaloSetPayload { halo: AcpTagHalo }
|
|
|
668
740
|
/** The approval deadline the settings page can edit, by field name. ONE window. */
|
|
669
741
|
export type ApprovalDeadlineKey = 'permissionTimeoutMs'
|
|
670
742
|
|
|
743
|
+
/**
|
|
744
|
+
* The idle-release windows the settings page offers, in MILLISECONDS.
|
|
745
|
+
*
|
|
746
|
+
* ONE ladder, shared by both halves: the browser snaps its slider to these
|
|
747
|
+
* values and the host accepts only these values, so a window the slider can
|
|
748
|
+
* produce is always a window the host runs. The first rung is 1 minute (an
|
|
749
|
+
* aggressive release), the rest are 5-minute steps up to an hour.
|
|
750
|
+
*/
|
|
751
|
+
export const IDLE_RELEASE_MS: readonly number[] = [
|
|
752
|
+
60_000, 300_000, 600_000, 900_000, 1_200_000, 1_500_000, 1_800_000, 2_100_000, 2_400_000, 2_700_000, 3_000_000, 3_300_000, 3_600_000,
|
|
753
|
+
]
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* The built-in idle-release window for chat processes: 30 minutes.
|
|
757
|
+
*
|
|
758
|
+
* Lives beside the ladder it is a rung of, so the two cannot drift: a
|
|
759
|
+
* deployment that names no window of its own runs this, and the settings
|
|
760
|
+
* page's reset returns to it.
|
|
761
|
+
*/
|
|
762
|
+
export const DEFAULT_IDLE_TIMEOUT_MS = 1_800_000
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* The idle-release window for chat processes, as the settings page edits it.
|
|
766
|
+
*
|
|
767
|
+
* The window is the HOST default: an agent that names its own
|
|
768
|
+
* {@link AcpAgentDef.idleTimeoutMs} keeps it, and a probe-only process (one
|
|
769
|
+
* that never hosted a session) still honors the short probe window — see
|
|
770
|
+
* `AGENTS/protocol.md` §5.
|
|
771
|
+
*/
|
|
772
|
+
export interface AcpIdleRelease {
|
|
773
|
+
/** The window in force, in ms. */
|
|
774
|
+
idleTimeoutMs: number
|
|
775
|
+
/**
|
|
776
|
+
* The window a RESET returns to: the deployment baseline when the config
|
|
777
|
+
* file names one, else the built-in default (30 minutes).
|
|
778
|
+
*/
|
|
779
|
+
defaultIdleTimeoutMs: number
|
|
780
|
+
/**
|
|
781
|
+
* The window `cordis.patch.yml` names, when it names one. Reported only
|
|
782
|
+
* when the file actually names one: the settings card must not say "the
|
|
783
|
+
* file asks for 30 minutes" about a file that says nothing.
|
|
784
|
+
*/
|
|
785
|
+
configuredIdleTimeoutMs?: number
|
|
786
|
+
}
|
|
787
|
+
/** `ui/idle-release`: read the idle-release window in force. */
|
|
788
|
+
export type IdleReleaseResult = { idleRelease?: AcpIdleRelease }
|
|
789
|
+
/**
|
|
790
|
+
* `ui/idle-release-set`: write the idle-release window, or drop the persisted
|
|
791
|
+
* override with `reset` so the built-in default applies again.
|
|
792
|
+
*/
|
|
793
|
+
export interface IdleReleaseSetPayload {
|
|
794
|
+
/** The window to run, in ms; omitted when `reset` is true. */
|
|
795
|
+
idleTimeoutMs?: number
|
|
796
|
+
/** Drop the persisted override so the built-in default applies again. */
|
|
797
|
+
reset?: boolean
|
|
798
|
+
}
|
|
799
|
+
|
|
671
800
|
/**
|
|
672
801
|
* What an EXPIRED approval deadline does. Three modes, and only three — an
|
|
673
802
|
* unanswered ask either gets answered for you, keeps waiting, or is refused.
|
|
@@ -752,6 +881,22 @@ export interface ChatEffortSetPayload { dshSessionId: string; effort?: string; a
|
|
|
752
881
|
/** `session/chat-config-set`: set one of the agent's extra config options. */
|
|
753
882
|
export interface ChatConfigSetPayload { dshSessionId: string; configId: string; value: string | boolean; agentId?: string }
|
|
754
883
|
|
|
884
|
+
/**
|
|
885
|
+
* `session/chat-fork-adopt`: bind the DSH chat session a host `sessions.fork`
|
|
886
|
+
* just created to the SAME agent process, through the agent's own
|
|
887
|
+
* `session/fork` of the parent conversation's ACP session — the second half of
|
|
888
|
+
* the two-stage branch (host builds the DSH tree first, the plugin then adopts
|
|
889
|
+
* the ACP side; see the NTR message-actions plan, §D5/D7).
|
|
890
|
+
*
|
|
891
|
+
* On success the child conversation's next message reaches an agent that
|
|
892
|
+
* actually remembers the parent context. On failure NOTHING is bound: the
|
|
893
|
+
* child stays an ordinary copied-history conversation and its first message
|
|
894
|
+
* starts a fresh `session/new` (today's behavior).
|
|
895
|
+
*/
|
|
896
|
+
export interface ChatForkAdoptPayload { dshSessionId: string; parentDshSessionId: string }
|
|
897
|
+
/** `session/chat-fork-adopt` success: the adopted ACP session's ids. */
|
|
898
|
+
export type ChatForkAdoptResult = { sessionId: string; acpSessionId: string }
|
|
899
|
+
|
|
755
900
|
/** Host-detected agent presence and tested defaults. */
|
|
756
901
|
export interface DetectedAgentInfo {
|
|
757
902
|
id: string
|
package/src/tps.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** ACP `usage_update` is context occupancy, not request tokens; this is the plugin's 4-chars/token OUTPUT estimate (tool args excluded). */
|
|
2
|
+
export const OUTPUT_CHARS_PER_TOKEN = 4
|
|
3
|
+
export const BURST_GAP_MS = 1000
|
|
4
|
+
|
|
5
|
+
export interface TurnTpsSnapshot {
|
|
6
|
+
outputTokens: number
|
|
7
|
+
burstMs: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class TurnTpsTracker {
|
|
11
|
+
private readonly gapMs: number
|
|
12
|
+
private lastProseAt: number | undefined
|
|
13
|
+
private outputChars = 0
|
|
14
|
+
private burstMs = 0
|
|
15
|
+
|
|
16
|
+
constructor(gapMs = BURST_GAP_MS) {
|
|
17
|
+
this.gapMs = gapMs
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
onProse(text: string, at: number): void {
|
|
21
|
+
if (text === '') return
|
|
22
|
+
if (this.lastProseAt !== undefined) {
|
|
23
|
+
const gap = at - this.lastProseAt
|
|
24
|
+
if (gap <= this.gapMs) this.burstMs += gap
|
|
25
|
+
}
|
|
26
|
+
this.lastProseAt = at
|
|
27
|
+
this.outputChars += text.length
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
onBreak(): void {
|
|
31
|
+
this.lastProseAt = undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
snapshot(): TurnTpsSnapshot {
|
|
35
|
+
return {
|
|
36
|
+
outputTokens: Math.round(this.outputChars / OUTPUT_CHARS_PER_TOKEN),
|
|
37
|
+
burstMs: this.burstMs,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|