thinkpool-pair 0.7.235 → 0.7.237
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/bridge.mjs +28 -2
- package/event-id.mjs +1 -1
- package/hermes-event-mapper.mjs +12 -0
- package/hermes-probe.mjs +4 -1
- package/hermes-session.mjs +77 -15
- package/hermes-setup.mjs +13 -1
- package/package.json +1 -1
- package/runtime-registry.mjs +3 -3
package/bridge.mjs
CHANGED
|
@@ -2616,12 +2616,29 @@ function openStructured({ id, runtime = 'claude', model, effort, resume, log, co
|
|
|
2616
2616
|
// run on different registered providers. (bridge-truth slice.)
|
|
2617
2617
|
{ const onBuiltin = entry.runtime !== 'hermes' && (!entry.provider || entry.provider === 'anthropic')
|
|
2618
2618
|
const m = (entry.runtime === 'hermes' || onBuiltin) ? (evt.model || evt.ctx?.model) : null
|
|
2619
|
-
if (m && m !== entry.model) { entry.model = m; announce() }
|
|
2619
|
+
if (m && m !== entry.model) { entry.model = m; announce(); if (evt.kind === 'model-switch') persist() }
|
|
2620
2620
|
// A custom Anthropic-compatible endpoint may expose its own catalog through the
|
|
2621
2621
|
// same SDK method. Never publish that as the ROOM's Claude catalog: its models
|
|
2622
2622
|
// already render in the provider group and otherwise leak under ANTHROPIC.
|
|
2623
2623
|
if (entry.runtime === 'hermes' && evt.kind === 'models' && Array.isArray(evt.models) && evt.models.length) { entry.models = evt.models; announce() }
|
|
2624
2624
|
else if (onBuiltin && evt.kind === 'models' && Array.isArray(evt.models) && evt.models.length) { roomModels = evt.models; announce() } }
|
|
2625
|
+
// Hermes model changes are asynchronous ACP requests. Keep the previous model
|
|
2626
|
+
// authoritative until Hermes acknowledges session/set_model; then settle the
|
|
2627
|
+
// originating control line exactly once. A rejected model never poisons the
|
|
2628
|
+
// saved session or leaves a cross-device chip claiming the optimistic pick.
|
|
2629
|
+
if (evt.kind === 'model-switch') {
|
|
2630
|
+
const request = entry.pendingModelSwitch
|
|
2631
|
+
entry.pendingModelSwitch = null
|
|
2632
|
+
const requested = evt.requested || request?.model || 'requested model'
|
|
2633
|
+
const text = evt.status === 'success'
|
|
2634
|
+
? `switched model → ${evt.model || requested}`
|
|
2635
|
+
: `couldn’t switch to ${requested} — still using ${entry.model || 'the previous model'}`
|
|
2636
|
+
const control = { kind: 'control', text, by: request?.by || null, cid: request?.cid || undefined }
|
|
2637
|
+
pushLog(entry, control)
|
|
2638
|
+
bcast('code-event', { term: id, evt: control })
|
|
2639
|
+
entry.flush?.()
|
|
2640
|
+
announce()
|
|
2641
|
+
}
|
|
2625
2642
|
// Cache the last usage/ctx meter on the entry (persisted + replayed — see
|
|
2626
2643
|
// openStructured). Chrome events bypass pushLog/persist in emitTail below, so
|
|
2627
2644
|
// usage needs an explicit persist() here to survive a bridge restart.
|
|
@@ -2700,7 +2717,7 @@ function openStructured({ id, runtime = 'claude', model, effort, resume, log, co
|
|
|
2700
2717
|
// card) and is intentionally NOT chrome, so it persists + replays.
|
|
2701
2718
|
// 'suggestion' is Claude Code's predicted-next-prompt ghost text — ephemeral
|
|
2702
2719
|
// composer UI, broadcast to both clients but never logged/persisted/replayed.
|
|
2703
|
-
const chrome = evt.kind === 'mode' || evt.kind === 'usage' || evt.kind === 'clear' || evt.kind === 'compact' || evt.kind === 'stalled' || evt.kind === 'suggestion' || evt.kind === 'assistant_stream'
|
|
2720
|
+
const chrome = evt.kind === 'mode' || evt.kind === 'usage' || evt.kind === 'clear' || evt.kind === 'compact' || evt.kind === 'stalled' || evt.kind === 'suggestion' || evt.kind === 'assistant_stream' || evt.kind === 'model-switch'
|
|
2704
2721
|
// The compact turn settled with `entry.compacting` STILL TRUE — which, because the
|
|
2705
2722
|
// `compaction` branch above clears the flag the moment the milestone lands, means
|
|
2706
2723
|
// exactly one thing: NO compaction happened. Three ways to get here, all of which
|
|
@@ -3428,10 +3445,19 @@ channel
|
|
|
3428
3445
|
const mm = text.match(/^\/model\b\s*(\S+)?/)
|
|
3429
3446
|
if (mm) {
|
|
3430
3447
|
if (mm[1]) {
|
|
3448
|
+
if (s.runtime === 'hermes' && s.pendingModelSwitch) {
|
|
3449
|
+
ctlLine(`finish the current Hermes model switch before choosing another model`)
|
|
3450
|
+
return
|
|
3451
|
+
}
|
|
3431
3452
|
if (s.session.setModel(mm[1]) === false) {
|
|
3432
3453
|
ctlLine(`finish or stop the current ${structuredRuntimeMetadata(s.runtime)?.label || 'agent'} turn before switching models`)
|
|
3433
3454
|
return
|
|
3434
3455
|
}
|
|
3456
|
+
if (s.runtime === 'hermes') {
|
|
3457
|
+
s.pendingModelSwitch = { model: mm[1], by: payload.by, cid: payload.cid }
|
|
3458
|
+
bcast('code-event', { term: payload.term, evt: { kind: 'model-switch', status: 'pending', requested: mm[1], ts: Date.now() } })
|
|
3459
|
+
return
|
|
3460
|
+
}
|
|
3435
3461
|
// Reflect the switch in the announce IMMEDIATELY (before any turn). The SDK
|
|
3436
3462
|
// only echoes the resolved full id on the next turn, and meta/ctx model go
|
|
3437
3463
|
// stale until then, so without this the chip snapped back to the old model
|
package/event-id.mjs
CHANGED
|
@@ -61,7 +61,7 @@ export function makeSeqCounter(startAfter = 0) {
|
|
|
61
61
|
// consumed a seq + set the durable archive floor while the client dropped them, leaving
|
|
62
62
|
// the client's oldest RENDERED seq above the floor → a phantom "load earlier history"
|
|
63
63
|
// pill on a fresh lane (models fires on every session init) that fetched nothing.
|
|
64
|
-
export const NO_SEQ_KINDS = new Set(['system', 'thinking_tokens', 'effort', 'models', 'queue-add', 'queue-cancel', 'propose-done', 'update-available'])
|
|
64
|
+
export const NO_SEQ_KINDS = new Set(['system', 'thinking_tokens', 'effort', 'models', 'model-switch', 'queue-add', 'queue-cancel', 'propose-done', 'update-available'])
|
|
65
65
|
export const seqable = (evt) => !!evt && typeof evt === 'object' && !NO_SEQ_KINDS.has(evt.kind)
|
|
66
66
|
|
|
67
67
|
// Highest numeric `seq` across a log of events (0 when none) — the floor a restored
|
package/hermes-event-mapper.mjs
CHANGED
|
@@ -109,6 +109,18 @@ export class HermesEventMapper {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
finishTurn({ stopReason = 'end_turn', usage = null } = {}) {
|
|
112
|
+
if (stopReason === 'cancelled') {
|
|
113
|
+
for (const [toolCallId, tool] of this.tools) {
|
|
114
|
+
this._emit({
|
|
115
|
+
kind: 'tool_result', toolUseId: toolCallId,
|
|
116
|
+
content: [{ type: 'text', text: '[cancelled]' }],
|
|
117
|
+
isError: false,
|
|
118
|
+
durationMs: tool.startedAt ? Date.now() - tool.startedAt : undefined,
|
|
119
|
+
parentToolUseId: null,
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
this.tools.clear()
|
|
112
124
|
const blocks = []
|
|
113
125
|
if (this.thought) blocks.push({ type: 'thinking', text: this.thought })
|
|
114
126
|
if (this.text) blocks.push({ type: 'text', text: this.text })
|
package/hermes-probe.mjs
CHANGED
|
@@ -8,11 +8,14 @@ export function probeHermesRuntime({ command = 'thinkpool', prefixArgs = [], exe
|
|
|
8
8
|
const version = clean(versionOutput).match(/Hermes Agent v([^\s]+)/i)?.[1] || null
|
|
9
9
|
execFile(command, [...prefixArgs, 'acp', '--check'], { encoding: 'utf8', env, timeout: 15_000, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
10
10
|
const hooksOutput = execFile(command, [...prefixArgs, 'hooks', 'list'], { encoding: 'utf8', env, timeout: 10_000, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
11
|
+
const doctorOutput = execFile(command, [...prefixArgs, 'hooks', 'doctor'], { encoding: 'utf8', env, timeout: 10_000, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
11
12
|
const guardOutput = execFile(command, [...prefixArgs, 'hooks', 'test', 'pre_tool_call', '--for-tool', 'delegate_task'], { encoding: 'utf8', env, timeout: 10_000, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
13
|
+
const doctorHealthy = /All shell hooks look healthy/i.test(doctorOutput)
|
|
14
|
+
&& !/(?:issue\(s\) found|modified since approval|⚠)/i.test(doctorOutput)
|
|
12
15
|
const delegationBlocked = /(?:action|decision)["']?\s*[:=]\s*["']block/i.test(guardOutput)
|
|
13
16
|
&& /ThinkPool blocks hidden Hermes delegation/i.test(guardOutput)
|
|
14
17
|
&& /pre_tool_call[\s\S]*delegate_task[\s\S]*(?:✓ allowed|allowed)/i.test(hooksOutput)
|
|
15
|
-
if (!delegationBlocked) return { available: false, version, reason: 'delegate_task is not structurally
|
|
18
|
+
if (!doctorHealthy || !delegationBlocked) return { available: false, version, reason: 'delegate_task guard is not healthy, unchanged, allowlisted, and structurally blocking in the dedicated Hermes profile' }
|
|
16
19
|
return { available: true, version, acpProtocol: 1, delegationBlocked: true }
|
|
17
20
|
} catch (error) {
|
|
18
21
|
return { available: false, version: null, reason: clean(error?.stderr || error?.message || error) }
|
package/hermes-session.mjs
CHANGED
|
@@ -14,6 +14,7 @@ export const HERMES_ACP_PROTOCOL_VERSION = 1
|
|
|
14
14
|
export const HERMES_SUPPORTED_MODES = new Set(['default', 'acceptEdits'])
|
|
15
15
|
|
|
16
16
|
const HERMES_SECRET_ENV_KEY = /(?:TOKEN|SECRET|PASSWORD|PRIVATE_KEY|API_KEY|ACCESS_KEY|CREDENTIAL)/i
|
|
17
|
+
const HERMES_REPLAY_UPDATES = new Set(['agent_message_chunk', 'agent_thought_chunk', 'tool_call', 'tool_call_update', 'plan'])
|
|
17
18
|
export const hermesChildEnv = (env = {}) => Object.fromEntries(
|
|
18
19
|
Object.entries(env).filter(([key]) => !HERMES_SECRET_ENV_KEY.test(key)),
|
|
19
20
|
)
|
|
@@ -61,6 +62,11 @@ export function startHermesSession({
|
|
|
61
62
|
let firstTurn = true
|
|
62
63
|
let stderrTail = ''
|
|
63
64
|
let promptChain = Promise.resolve()
|
|
65
|
+
let activeTurnId = 0
|
|
66
|
+
const abortedTurns = new Set()
|
|
67
|
+
let retireClient = () => {}
|
|
68
|
+
let resuming = false
|
|
69
|
+
let modelSwitchPending = false
|
|
64
70
|
|
|
65
71
|
const emit = (event) => { try { onEvent?.(event) } catch { /* consumer isolation */ } }
|
|
66
72
|
|
|
@@ -79,7 +85,10 @@ export function startHermesSession({
|
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
function onNotification(method, params) {
|
|
82
|
-
if (method
|
|
88
|
+
if (method !== 'session/update') return
|
|
89
|
+
const update = params?.update || params
|
|
90
|
+
if (resuming && HERMES_REPLAY_UPDATES.has(update?.sessionUpdate)) return
|
|
91
|
+
mapper?.push(params)
|
|
83
92
|
}
|
|
84
93
|
|
|
85
94
|
async function boot() {
|
|
@@ -101,13 +110,15 @@ export function startHermesSession({
|
|
|
101
110
|
const profile = probeHermesRuntime({ command, env: childEnv })
|
|
102
111
|
if (!profile.available) throw new Error(`Hermes profile safety check failed: ${profile.reason || 'runtime unavailable'}`)
|
|
103
112
|
}
|
|
113
|
+
let retired = false
|
|
114
|
+
retireClient = () => { retired = true }
|
|
104
115
|
client = clientFactory({
|
|
105
116
|
command, args, cwd: activeCwd, env: childEnv,
|
|
106
117
|
onNotification,
|
|
107
118
|
onRequest,
|
|
108
119
|
onStderr: (text) => { stderrTail = (stderrTail + text).slice(-2000) },
|
|
109
120
|
onClose: (error) => {
|
|
110
|
-
if (ended || crashed) return
|
|
121
|
+
if (ended || crashed || retired) return
|
|
111
122
|
crashed = true
|
|
112
123
|
turnActive = false
|
|
113
124
|
emit({ kind: 'error', message: `Hermes ACP closed unexpectedly: ${error?.message || error}`, recoverable: true })
|
|
@@ -129,10 +140,14 @@ export function startHermesSession({
|
|
|
129
140
|
if (!initialized?.agentCapabilities?.sessionCapabilities?.resume) {
|
|
130
141
|
throw new Error(`Hermes session ${sessionId} cannot be resumed by this ACP release; no fresh session was substituted`)
|
|
131
142
|
}
|
|
132
|
-
try {
|
|
143
|
+
try {
|
|
144
|
+
resuming = true
|
|
145
|
+
state = await client.request('session/resume', { ...params, sessionId }, 30_000)
|
|
146
|
+
}
|
|
133
147
|
catch (error) {
|
|
134
148
|
throw new Error(`Hermes session ${sessionId} is not available; no fresh session was substituted (${error?.message || error})`)
|
|
135
149
|
}
|
|
150
|
+
finally { resuming = false }
|
|
136
151
|
} else state = await client.request('session/new', params, 30_000)
|
|
137
152
|
if (!sessionId) sessionId = state?.sessionId || null
|
|
138
153
|
if (!sessionId) throw new Error('Hermes ACP did not return a session id')
|
|
@@ -158,6 +173,15 @@ export function startHermesSession({
|
|
|
158
173
|
return starting
|
|
159
174
|
}
|
|
160
175
|
|
|
176
|
+
async function restartAfterCancelledResponseFailure() {
|
|
177
|
+
const oldClient = client
|
|
178
|
+
retireClient()
|
|
179
|
+
client = null
|
|
180
|
+
mapper = null
|
|
181
|
+
oldClient?.end()
|
|
182
|
+
await boot()
|
|
183
|
+
}
|
|
184
|
+
|
|
161
185
|
function promptBlocks(text, options = {}) {
|
|
162
186
|
const rawText = String(text)
|
|
163
187
|
// Hermes handles /steer, /queue, /compact and /reset only when the slash is
|
|
@@ -175,14 +199,33 @@ export function startHermesSession({
|
|
|
175
199
|
async function runPrompt(text, options = {}) {
|
|
176
200
|
await boot()
|
|
177
201
|
const steering = turnActive
|
|
202
|
+
const turnId = steering ? activeTurnId : ++activeTurnId
|
|
178
203
|
const body = steering ? `/steer ${String(text)}` : String(text)
|
|
179
204
|
if (!steering) turnActive = true
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
205
|
+
let result
|
|
206
|
+
try {
|
|
207
|
+
result = await client.request('session/prompt', {
|
|
208
|
+
sessionId,
|
|
209
|
+
prompt: promptBlocks(body, options),
|
|
210
|
+
messageId: randomUUID(),
|
|
211
|
+
}, 0)
|
|
212
|
+
} catch (error) {
|
|
213
|
+
// Hermes 0.18.2 successfully interrupts the model/tool, then its ACP
|
|
214
|
+
// prompt handler can reject with Internal error while constructing the
|
|
215
|
+
// cancelled response. The bridge already owns the canonical aborted
|
|
216
|
+
// boundary below, so a late response failure for that exact turn is not a
|
|
217
|
+
// second turn failure. Process-close errors still surface through onClose.
|
|
218
|
+
if (abortedTurns.has(turnId)) {
|
|
219
|
+
// Hermes 0.18.2 throws before clearing its transient is_running flag.
|
|
220
|
+
// Resume the same persisted session in a fresh per-lane ACP process so
|
|
221
|
+
// the next queued prompt cannot disappear into that stale runtime queue.
|
|
222
|
+
if (!ended) await restartAfterCancelledResponseFailure()
|
|
223
|
+
return { stopReason: 'cancelled' }
|
|
224
|
+
}
|
|
225
|
+
throw error
|
|
226
|
+
}
|
|
185
227
|
if (!steering) {
|
|
228
|
+
if (abortedTurns.has(turnId)) return result
|
|
186
229
|
turnActive = false
|
|
187
230
|
firstTurn = false
|
|
188
231
|
mapper.finishTurn({ stopReason: result?.stopReason, usage: result?.usage })
|
|
@@ -209,8 +252,23 @@ export function startHermesSession({
|
|
|
209
252
|
return true
|
|
210
253
|
},
|
|
211
254
|
abort() {
|
|
212
|
-
if (!sessionId || !client?.alive) return
|
|
213
|
-
|
|
255
|
+
if (!sessionId || !client?.alive || !turnActive) return
|
|
256
|
+
const turnId = activeTurnId
|
|
257
|
+
abortedTurns.add(turnId)
|
|
258
|
+
// Bound retained turn ids while preserving any concurrent /steer request
|
|
259
|
+
// that may settle after the primary prompt response.
|
|
260
|
+
if (abortedTurns.size > 32) abortedTurns.delete(abortedTurns.values().next().value)
|
|
261
|
+
turnActive = false
|
|
262
|
+
firstTurn = false
|
|
263
|
+
mapper?.finishTurn({ stopReason: 'cancelled' })
|
|
264
|
+
void client.notify('session/cancel', { sessionId }).catch((error) => {
|
|
265
|
+
// Never display a stopped lane while the un-cancelled Hermes process
|
|
266
|
+
// might still be executing. A transport failure tears down the ACP
|
|
267
|
+
// process and makes the lane honestly non-resumable until restart.
|
|
268
|
+
crashed = true
|
|
269
|
+
client?.end()
|
|
270
|
+
emit({ kind: 'error', message: `Hermes cancellation delivery failed; ACP process stopped: ${error?.message || error}`, recoverable: true })
|
|
271
|
+
})
|
|
214
272
|
},
|
|
215
273
|
end() {
|
|
216
274
|
ended = true
|
|
@@ -220,12 +278,16 @@ export function startHermesSession({
|
|
|
220
278
|
void mcpHttp?.close().catch(() => {})
|
|
221
279
|
},
|
|
222
280
|
setModel(nextModel) {
|
|
223
|
-
if (!nextModel || turnActive) return false
|
|
224
|
-
|
|
225
|
-
|
|
281
|
+
if (!nextModel || turnActive || modelSwitchPending) return false
|
|
282
|
+
const requested = String(nextModel)
|
|
283
|
+
modelSwitchPending = true
|
|
284
|
+
void boot().then(() => client.request('session/set_model', { sessionId, modelId: requested })).then(() => {
|
|
285
|
+
if (ended) return
|
|
286
|
+
activeModel = requested
|
|
226
287
|
if (mapper) mapper.model = activeModel
|
|
227
|
-
emit({ kind: '
|
|
228
|
-
}).catch((error) => emit({ kind: '
|
|
288
|
+
emit({ kind: 'model-switch', status: 'success', model: activeModel, requested })
|
|
289
|
+
}).catch((error) => { if (!ended) emit({ kind: 'model-switch', status: 'failed', model: activeModel, requested, message: `Hermes model switch failed: ${error?.message || error}` }) })
|
|
290
|
+
.finally(() => { modelSwitchPending = false })
|
|
229
291
|
return true
|
|
230
292
|
},
|
|
231
293
|
setMode(nextMode) {
|
package/hermes-setup.mjs
CHANGED
|
@@ -39,11 +39,16 @@ function snapshotFile(filePath, fsImpl) {
|
|
|
39
39
|
return {
|
|
40
40
|
content: fsImpl.readFileSync(filePath),
|
|
41
41
|
mode: fsImpl.statSync(filePath).mode & 0o777,
|
|
42
|
+
atimeMs: fsImpl.statSync(filePath).atimeMs,
|
|
43
|
+
mtimeMs: fsImpl.statSync(filePath).mtimeMs,
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
function restoreFile(filePath, snapshot, fsImpl) {
|
|
46
|
-
if (snapshot)
|
|
48
|
+
if (snapshot) {
|
|
49
|
+
atomicWrite(filePath, snapshot.content, { fsImpl, mode: snapshot.mode })
|
|
50
|
+
fsImpl.utimesSync?.(filePath, snapshot.atimeMs / 1000, snapshot.mtimeMs / 1000)
|
|
51
|
+
}
|
|
47
52
|
else fsImpl.rmSync(filePath, { force: true })
|
|
48
53
|
}
|
|
49
54
|
|
|
@@ -116,13 +121,19 @@ export function setupHermesRuntime({
|
|
|
116
121
|
]
|
|
117
122
|
|
|
118
123
|
const guardPath = hermesGuardPath({ homeDir, env, platform })
|
|
124
|
+
const allowlistPath = path.join(profilePath, 'shell-hooks-allowlist.json')
|
|
119
125
|
const guardSnapshot = snapshotFile(guardPath, fsImpl)
|
|
120
126
|
const configSnapshot = snapshotFile(configPath, fsImpl)
|
|
127
|
+
const allowlistSnapshot = snapshotFile(allowlistPath, fsImpl)
|
|
121
128
|
try {
|
|
122
129
|
atomicWrite(guardPath, guardSource, { fsImpl, mode: 0o700 })
|
|
123
130
|
atomicWrite(configPath, YAML.stringify(config, { lineWidth: 0 }), { fsImpl, mode: 0o600 })
|
|
124
131
|
|
|
125
132
|
const consentEnv = { ...env, HERMES_ACCEPT_HOOKS: '1' }
|
|
133
|
+
// Hermes does not refresh an existing approval when the same command's
|
|
134
|
+
// script mtime changes. Revoke only ThinkPool's exact guard command, then
|
|
135
|
+
// let the synthetic test consent to that exact command again.
|
|
136
|
+
run([...profileArgs, 'hooks', 'revoke', commandText(guardPath)], { env: consentEnv })
|
|
126
137
|
const testOutput = run([...profileArgs, 'hooks', 'test', 'pre_tool_call', '--for-tool', 'delegate_task'], { env: consentEnv })
|
|
127
138
|
if (!/(?:action|decision)["']?\s*[:=]\s*["']block/i.test(testOutput)
|
|
128
139
|
|| !/ThinkPool blocks hidden Hermes delegation/i.test(testOutput)) {
|
|
@@ -137,6 +148,7 @@ export function setupHermesRuntime({
|
|
|
137
148
|
try {
|
|
138
149
|
restoreFile(configPath, configSnapshot, fsImpl)
|
|
139
150
|
restoreFile(guardPath, guardSnapshot, fsImpl)
|
|
151
|
+
restoreFile(allowlistPath, allowlistSnapshot, fsImpl)
|
|
140
152
|
} catch (rollbackError) {
|
|
141
153
|
error.message = `${error.message}; rollback also failed: ${clean(rollbackError.message)}`
|
|
142
154
|
}
|
package/package.json
CHANGED
package/runtime-registry.mjs
CHANGED
|
@@ -3,17 +3,17 @@ import path from 'node:path'
|
|
|
3
3
|
const RUNTIMES = Object.freeze({
|
|
4
4
|
claude: Object.freeze({
|
|
5
5
|
id: 'claude', command: 'claude', label: 'Claude Code', protocol: 'agent-sdk',
|
|
6
|
-
structured: true, flow: true, canSteer: false, images: true, nativeModelCatalog: false, defaultMode: 'default',
|
|
6
|
+
structured: true, flow: true, canSteer: false, images: true, nativeModelCatalog: false, effortControl: true, defaultMode: 'default',
|
|
7
7
|
modes: Object.freeze(['default', 'acceptEdits', 'plan', 'bypassPermissions']),
|
|
8
8
|
}),
|
|
9
9
|
codex: Object.freeze({
|
|
10
10
|
id: 'codex', command: 'codex', label: 'Codex', protocol: 'codex-app-server',
|
|
11
|
-
structured: true, flow: true, canSteer: true, images: true, nativeModelCatalog: true, defaultMode: 'bypassPermissions',
|
|
11
|
+
structured: true, flow: true, canSteer: true, images: true, nativeModelCatalog: true, effortControl: true, defaultMode: 'bypassPermissions',
|
|
12
12
|
modes: Object.freeze(['default', 'acceptEdits', 'plan', 'bypassPermissions', 'review']),
|
|
13
13
|
}),
|
|
14
14
|
hermes: Object.freeze({
|
|
15
15
|
id: 'hermes', command: 'thinkpool', label: 'Hermes Agent', protocol: 'acp',
|
|
16
|
-
structured: true, flow: false, canSteer: true, images: true, nativeModelCatalog: true, defaultMode: 'default',
|
|
16
|
+
structured: true, flow: false, canSteer: true, images: true, nativeModelCatalog: true, effortControl: false, defaultMode: 'default',
|
|
17
17
|
modes: Object.freeze(['default', 'acceptEdits']),
|
|
18
18
|
beta: true,
|
|
19
19
|
}),
|