@cocorograph/hub-agent 0.6.25 → 0.6.27
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/package.json +2 -2
- package/src/claude-stream-bridge.mjs +109 -21
- package/src/main.mjs +8 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cocorograph/hub-agent",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.27",
|
|
4
4
|
"description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"LICENSE"
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@anthropic-ai/claude-agent-sdk": "^0.3.
|
|
35
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.158",
|
|
36
36
|
"commander": "^12.1.0",
|
|
37
37
|
"node-pty": "^1.0.0",
|
|
38
38
|
"pino": "^9.0.0",
|
|
@@ -137,6 +137,7 @@ class ClaudeStreamSession {
|
|
|
137
137
|
permissionMode,
|
|
138
138
|
maxTurns,
|
|
139
139
|
maxThinkingTokens,
|
|
140
|
+
effort,
|
|
140
141
|
resumeSessionId,
|
|
141
142
|
resident,
|
|
142
143
|
sdk,
|
|
@@ -158,6 +159,10 @@ class ClaudeStreamSession {
|
|
|
158
159
|
this.maxTurns = typeof maxTurns === "number" ? maxTurns : null
|
|
159
160
|
this.maxThinkingTokens =
|
|
160
161
|
typeof maxThinkingTokens === "number" ? maxThinkingTokens : null
|
|
162
|
+
// Opus 4.6+ の effort パラメータ (low/medium/high/xhigh/max)。adaptive thinking と
|
|
163
|
+
// 併用して思考の深さを制御する。Opus では budget 方式 (maxThinkingTokens) は廃止
|
|
164
|
+
// 扱いのため、effort モデルでは effort + thinking:{type:'adaptive'} を使う。
|
|
165
|
+
this.effort = effort || null
|
|
161
166
|
this.sdk = sdk
|
|
162
167
|
this.logger = logger
|
|
163
168
|
this.onEvent = onEvent
|
|
@@ -195,10 +200,15 @@ class ClaudeStreamSession {
|
|
|
195
200
|
|
|
196
201
|
/** 改修3: per-message セッションで busy 中に届いた送信を退避する pending キュー。
|
|
197
202
|
* 常駐 query 化 (改修2) とは別レイヤー。resume チェーンは維持したまま、現ターン
|
|
198
|
-
* 完了時 (finally) に先頭から drain して次ターンを自動発火する。
|
|
203
|
+
* 完了時 (finally) に先頭から drain して次ターンを自動発火する。
|
|
204
|
+
* キャンセル機能 (0.6.26): 各項目に安定 id を振り、browser から id 指定で
|
|
205
|
+
* 個別削除できるようにする。要素は { id: string, text: string }。 */
|
|
199
206
|
this._pendingMessages = []
|
|
200
|
-
/**
|
|
201
|
-
this.
|
|
207
|
+
/** キャンセル機能: pending 項目へ安定 id を振るための連番カウンタ。 */
|
|
208
|
+
this._queueSeq = 0
|
|
209
|
+
/** 改修3: 直近 browser へ通知した queue 署名 (件数 + id 列)。変化時のみ emit する。
|
|
210
|
+
* 空キューの署名 ("0:") で初期化し、空→空の冗長 emit を抑止する。 */
|
|
211
|
+
this._lastEmittedQueueSig = "0:"
|
|
202
212
|
|
|
203
213
|
/** @type {Map<string, {resolve: (decision: object) => void}>} permission 応答待ち */
|
|
204
214
|
this._permissionResolvers = new Map()
|
|
@@ -321,7 +331,7 @@ class ClaudeStreamSession {
|
|
|
321
331
|
* 値が undefined のキーは「変更なし」として無視する (バッジ未送出時に既存値を消さない)。
|
|
322
332
|
* model に空文字/null が来たら setModel(undefined) でデフォルトへ戻す。
|
|
323
333
|
* maxThinkingTokens に 0/null が来たら setMaxThinkingTokens(null) でオフにする。 */
|
|
324
|
-
applyRuntimeOptions({ model, permissionMode, maxThinkingTokens } = {}) {
|
|
334
|
+
applyRuntimeOptions({ model, permissionMode, maxThinkingTokens, effort } = {}) {
|
|
325
335
|
const q = this._residentQuery
|
|
326
336
|
// モデル
|
|
327
337
|
if (model !== undefined) {
|
|
@@ -373,6 +383,34 @@ class ClaudeStreamSession {
|
|
|
373
383
|
}
|
|
374
384
|
}
|
|
375
385
|
}
|
|
386
|
+
// effort (Opus 4.6+)。SDK には setEffort 相当のランタイム制御メソッドが無いため、
|
|
387
|
+
// 保持値の更新のみ行う。per-message セッションは次ターンの options 構築時に
|
|
388
|
+
// this.effort を読むため即反映される。常駐 query は次 spawn (resume 再起動) 時に
|
|
389
|
+
// 反映される (= 次回セッションから切替。ユーザー要件と一致)。
|
|
390
|
+
if (effort !== undefined) {
|
|
391
|
+
this.effort = effort || null
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** モデルが Opus 4.6+ (effort / adaptive thinking 対応) かどうか。
|
|
396
|
+
* budget 方式 (maxThinkingTokens) は Opus 4.7+ で廃止扱いのため、effort モデルでは
|
|
397
|
+
* effort + thinking:{type:'adaptive'} に切り替える。 */
|
|
398
|
+
_isEffortModel() {
|
|
399
|
+
return typeof this.model === "string" && /claude-opus-4-[678]/.test(this.model)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** 思考関連オプション (effort / adaptive thinking / 旧 budget) を options へ適用する。
|
|
403
|
+
* per-message / 常駐 query の両方から呼ぶ共通ロジック (分岐の二重定義を避ける)。 */
|
|
404
|
+
_applyThinkingOptions(options) {
|
|
405
|
+
if (this._isEffortModel()) {
|
|
406
|
+
// Opus: adaptive thinking を明示 ON にし、effort で深さを指定する。
|
|
407
|
+
// budget 方式 (maxThinkingTokens) は使わない (Opus 4.7+ で非対応)。
|
|
408
|
+
options.thinking = { type: "adaptive" }
|
|
409
|
+
if (this.effort) options.effort = this.effort
|
|
410
|
+
} else if (this.maxThinkingTokens != null) {
|
|
411
|
+
// 非 effort モデル (Sonnet / Haiku 等) は従来の budget 方式を維持。
|
|
412
|
+
options.maxThinkingTokens = this.maxThinkingTokens
|
|
413
|
+
}
|
|
376
414
|
}
|
|
377
415
|
|
|
378
416
|
/** soft detach: browser 切断時にターンを中断せずセッションを生かしたまま detached に
|
|
@@ -420,7 +458,7 @@ class ClaudeStreamSession {
|
|
|
420
458
|
// 次を push すると SDK streaming-input で割り込み扱いになり得るため (公式 interrupt 警告)。
|
|
421
459
|
// pending は queue_state で UI (送信待ちチップ) に出す (per-message と同じ体験)。
|
|
422
460
|
if (this._busy) {
|
|
423
|
-
this.
|
|
461
|
+
this._enqueuePending(prompt)
|
|
424
462
|
this.logger?.info(
|
|
425
463
|
{ stream_id: this.stream_id, queued: this._pendingMessages.length },
|
|
426
464
|
"resident busy, message queued",
|
|
@@ -444,7 +482,7 @@ class ClaudeStreamSession {
|
|
|
444
482
|
// 改修3: busy 中の送信は破棄せず pending キューへ退避し、現ターン完了時に drain する
|
|
445
483
|
// (ターミナル流の「積む→待機→順次実行」)。常駐 query 化はしないので暴走リスクは増えない。
|
|
446
484
|
if (this._busy) {
|
|
447
|
-
this.
|
|
485
|
+
this._enqueuePending(prompt)
|
|
448
486
|
this.logger?.info(
|
|
449
487
|
{ stream_id: this.stream_id, queued: this._pendingMessages.length },
|
|
450
488
|
"claude busy, message queued",
|
|
@@ -470,9 +508,10 @@ class ClaudeStreamSession {
|
|
|
470
508
|
}
|
|
471
509
|
if (this.model) options.model = this.model
|
|
472
510
|
if (this.permissionMode) options.permissionMode = this.permissionMode
|
|
473
|
-
// Phase B: チャット SDK に効くオプション (
|
|
511
|
+
// Phase B: チャット SDK に効くオプション (ツール往復上限)。
|
|
474
512
|
if (this.maxTurns != null) options.maxTurns = this.maxTurns
|
|
475
|
-
|
|
513
|
+
// 思考オプション (effort / adaptive thinking / 旧 budget) はモデルに応じて切替。
|
|
514
|
+
this._applyThinkingOptions(options)
|
|
476
515
|
// 直前ターンまでの session_id があれば resume チェーン
|
|
477
516
|
if (this.sessionId) options.resume = this.sessionId
|
|
478
517
|
|
|
@@ -573,8 +612,8 @@ class ClaudeStreamSession {
|
|
|
573
612
|
return
|
|
574
613
|
}
|
|
575
614
|
const next = this._pendingMessages.shift()
|
|
576
|
-
this._emitQueueState()
|
|
577
|
-
this._runPerMessage(next).catch((err) => {
|
|
615
|
+
this._emitQueueState([next.text])
|
|
616
|
+
this._runPerMessage(next.text).catch((err) => {
|
|
578
617
|
this.logger?.error(
|
|
579
618
|
{ stream_id: this.stream_id, err: err?.message },
|
|
580
619
|
"drain runPerMessage threw",
|
|
@@ -582,20 +621,53 @@ class ClaudeStreamSession {
|
|
|
582
621
|
})
|
|
583
622
|
}
|
|
584
623
|
|
|
624
|
+
/** キャンセル機能 (0.6.26): pending キューへ安定 id 付きで 1 件積む。 */
|
|
625
|
+
_enqueuePending(prompt) {
|
|
626
|
+
const id = `q${++this._queueSeq}`
|
|
627
|
+
this._pendingMessages.push({ id, text: prompt })
|
|
628
|
+
return id
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/** キャンセル機能 (0.6.26): browser から id 指定で送信待ちメッセージを 1 件取り消す。
|
|
632
|
+
* 実行開始済み (既に drain された) メッセージは pending に残っていないので no-op になる。
|
|
633
|
+
* 削除に成功したら queue_state を再 emit して送信待ちチップを更新する。 */
|
|
634
|
+
cancelQueued(id) {
|
|
635
|
+
const before = this._pendingMessages.length
|
|
636
|
+
this._pendingMessages = this._pendingMessages.filter((m) => m.id !== id)
|
|
637
|
+
const removed = before !== this._pendingMessages.length
|
|
638
|
+
if (removed) {
|
|
639
|
+
this.logger?.info(
|
|
640
|
+
{ stream_id: this.stream_id, id, queued: this._pendingMessages.length },
|
|
641
|
+
"queued message canceled",
|
|
642
|
+
)
|
|
643
|
+
this._emitQueueState()
|
|
644
|
+
}
|
|
645
|
+
return removed
|
|
646
|
+
}
|
|
647
|
+
|
|
585
648
|
/** 改修3: pending キューの現状を browser へ通知する (送信待ちチップ表示用)。
|
|
586
|
-
* onEvent 経由で claude.event(event.type="queue_state") として届く。
|
|
587
|
-
|
|
649
|
+
* onEvent 経由で claude.event(event.type="queue_state") として届く。
|
|
650
|
+
* @param {string[]} [started] このタイミングで pending から取り出して実行開始した
|
|
651
|
+
* メッセージ本文。drain 由来の emit でのみ渡す。frontend はこれを user バブルへ
|
|
652
|
+
* 昇格させる。キャンセル / 追加由来の emit では空 (昇格させない)。これにより
|
|
653
|
+
* 「先頭の pending をキャンセルした」のを「実行開始した」と誤認しなくなる (0.6.26)。 */
|
|
654
|
+
_emitQueueState(started = []) {
|
|
588
655
|
const count = this._pendingMessages.length
|
|
589
|
-
//
|
|
590
|
-
|
|
591
|
-
|
|
656
|
+
// 署名 = 件数 + id 列。件数が同じでもキャンセルで中身が変われば通知する。
|
|
657
|
+
const sig = `${count}:${this._pendingMessages.map((m) => m.id).join(",")}`
|
|
658
|
+
// started があるときは drain なので、sig 変化が無くても (理論上起きないが) 通知する。
|
|
659
|
+
if (started.length === 0 && sig === this._lastEmittedQueueSig) return
|
|
660
|
+
this._lastEmittedQueueSig = sig
|
|
592
661
|
try {
|
|
593
662
|
// messages は全文を載せる。frontend は実行開始 (drain) 時にこれを user バブルへ
|
|
594
663
|
// 昇格させるため、ここで切り詰めると本文が欠ける。チップの省略表示は CSS 側で行う。
|
|
664
|
+
// items は id 付きでキャンセルボタンの対象特定に使う (0.6.26)。
|
|
595
665
|
this.onEvent?.({
|
|
596
666
|
type: "queue_state",
|
|
597
667
|
pending: count,
|
|
598
|
-
messages:
|
|
668
|
+
messages: this._pendingMessages.map((m) => m.text),
|
|
669
|
+
items: this._pendingMessages.map((m) => ({ id: m.id, text: m.text })),
|
|
670
|
+
started,
|
|
599
671
|
})
|
|
600
672
|
} catch {
|
|
601
673
|
/* ignore */
|
|
@@ -623,8 +695,8 @@ class ClaudeStreamSession {
|
|
|
623
695
|
}
|
|
624
696
|
const next = this._pendingMessages.shift()
|
|
625
697
|
this._busy = true
|
|
626
|
-
this._inputQueue.push(toSDKUserMessage(next))
|
|
627
|
-
this._emitQueueState()
|
|
698
|
+
this._inputQueue.push(toSDKUserMessage(next.text))
|
|
699
|
+
this._emitQueueState([next.text])
|
|
628
700
|
}
|
|
629
701
|
|
|
630
702
|
/** 改修2+4: 常駐 query を 1 回だけ起動し、streaming input キューから複数ターンを処理する。
|
|
@@ -645,7 +717,7 @@ class ClaudeStreamSession {
|
|
|
645
717
|
if (this.model) options.model = this.model
|
|
646
718
|
if (this.permissionMode) options.permissionMode = this.permissionMode
|
|
647
719
|
if (this.maxTurns != null) options.maxTurns = this.maxTurns
|
|
648
|
-
|
|
720
|
+
this._applyThinkingOptions(options)
|
|
649
721
|
// 改修4: 起動時に sessionId (= resumeSessionId) があれば resume チェーンで文脈を引き継ぐ。
|
|
650
722
|
// query 起動時点の値のみ有効 (起動後に確定/変化する session_id は同一 query 内で継続される)。
|
|
651
723
|
if (this.sessionId) options.resume = this.sessionId
|
|
@@ -712,8 +784,8 @@ class ClaudeStreamSession {
|
|
|
712
784
|
const next = this._pendingMessages.shift()
|
|
713
785
|
this._busy = true
|
|
714
786
|
this._residentStarted = true
|
|
715
|
-
this._inputQueue.push(toSDKUserMessage(next))
|
|
716
|
-
this._emitQueueState()
|
|
787
|
+
this._inputQueue.push(toSDKUserMessage(next.text))
|
|
788
|
+
this._emitQueueState([next.text])
|
|
717
789
|
this._startResidentQuery()
|
|
718
790
|
}
|
|
719
791
|
}
|
|
@@ -833,6 +905,7 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
833
905
|
* permissionMode?: string|null,
|
|
834
906
|
* maxTurns?: number|null,
|
|
835
907
|
* maxThinkingTokens?: number|null,
|
|
908
|
+
* effort?: string|null,
|
|
836
909
|
* resumeSessionId?: string|null,
|
|
837
910
|
* }} args
|
|
838
911
|
* @returns {{ stream_id: string, resuming: boolean }}
|
|
@@ -844,6 +917,7 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
844
917
|
permissionMode,
|
|
845
918
|
maxTurns,
|
|
846
919
|
maxThinkingTokens,
|
|
920
|
+
effort,
|
|
847
921
|
resumeSessionId,
|
|
848
922
|
resident,
|
|
849
923
|
}) {
|
|
@@ -872,6 +946,7 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
872
946
|
permissionMode,
|
|
873
947
|
maxThinkingTokens:
|
|
874
948
|
typeof maxThinkingTokens === "number" ? maxThinkingTokens : null,
|
|
949
|
+
effort,
|
|
875
950
|
})
|
|
876
951
|
this.sessions.set(stream_id, live)
|
|
877
952
|
this.logger?.info(
|
|
@@ -882,6 +957,7 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
882
957
|
model: live.model,
|
|
883
958
|
permissionMode: live.permissionMode,
|
|
884
959
|
maxThinkingTokens: live.maxThinkingTokens,
|
|
960
|
+
effort: live.effort,
|
|
885
961
|
},
|
|
886
962
|
"claude stream reattached to live session",
|
|
887
963
|
)
|
|
@@ -896,6 +972,7 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
896
972
|
maxTurns: typeof maxTurns === "number" ? maxTurns : null,
|
|
897
973
|
maxThinkingTokens:
|
|
898
974
|
typeof maxThinkingTokens === "number" ? maxThinkingTokens : null,
|
|
975
|
+
effort: effort || null,
|
|
899
976
|
resumeSessionId: resumeSessionId || null,
|
|
900
977
|
resident,
|
|
901
978
|
sdk: this.sdk,
|
|
@@ -990,6 +1067,17 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
990
1067
|
return true
|
|
991
1068
|
}
|
|
992
1069
|
|
|
1070
|
+
/** キャンセル機能 (0.6.26): browser → 送信待ち (pending) メッセージを id 指定で取り消す。
|
|
1071
|
+
* 実行中ターンには影響しない (中断は interrupt を使う)。 */
|
|
1072
|
+
cancelQueued({ stream_id, id }) {
|
|
1073
|
+
const s = this.sessions.get(stream_id)
|
|
1074
|
+
if (!s) {
|
|
1075
|
+
this.logger?.warn({ stream_id, id }, "claude.queue.cancel but stream missing")
|
|
1076
|
+
return false
|
|
1077
|
+
}
|
|
1078
|
+
return s.cancelQueued(id)
|
|
1079
|
+
}
|
|
1080
|
+
|
|
993
1081
|
/**
|
|
994
1082
|
* セッション停止 (graceful)。実行中ターンは中断せず完走させ、完走後に
|
|
995
1083
|
* onReap で Map から撤去する。アイドルなら即時撤去。
|
package/src/main.mjs
CHANGED
|
@@ -756,6 +756,9 @@ async function dispatch(msg, ctx) {
|
|
|
756
756
|
typeof msg.max_thinking_tokens === "number"
|
|
757
757
|
? msg.max_thinking_tokens
|
|
758
758
|
: null,
|
|
759
|
+
// effort (Opus 4.6+): browser がチャット既定 (agent 設定由来) を送る。
|
|
760
|
+
// 空文字/未指定なら null (SDK / Claude Code 既定 = Opus は high)。
|
|
761
|
+
effort: msg.effort || ctx.config?.claude_effort || null,
|
|
759
762
|
resumeSessionId: msg.resume_session_id || null,
|
|
760
763
|
})
|
|
761
764
|
ctx.client.send({
|
|
@@ -844,6 +847,11 @@ async function dispatch(msg, ctx) {
|
|
|
844
847
|
if (!ctx.claudeBridge) return
|
|
845
848
|
ctx.claudeBridge.interrupt({ stream_id: msg.stream_id })
|
|
846
849
|
return
|
|
850
|
+
case "claude.queue.cancel":
|
|
851
|
+
// 送信待ち (pending) メッセージを id 指定で取り消す (0.6.26)。
|
|
852
|
+
if (!ctx.claudeBridge) return
|
|
853
|
+
ctx.claudeBridge.cancelQueued({ stream_id: msg.stream_id, id: msg.id })
|
|
854
|
+
return
|
|
847
855
|
case "claude.detach":
|
|
848
856
|
if (!ctx.claudeBridge) return
|
|
849
857
|
ctx.claudeBridge.detach({ stream_id: msg.stream_id })
|