thinkpool-pair 0.7.280 → 0.7.282
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 +29 -18
- package/flow-models.mjs +32 -0
- package/package.json +1 -1
- package/providers.mjs +21 -0
- package/thinkpool-capabilities.json +4 -2
- package/thinkpool-room-prompt.mjs +4 -5
package/bridge.mjs
CHANGED
|
@@ -46,7 +46,7 @@ import { createPermNotifier, shouldNotifyTurnDone, permissionSummary, clipSummar
|
|
|
46
46
|
// resolveProviderEnv(id) → {ANTHROPIC_BASE_URL,ANTHROPIC_AUTH_TOKEN,ANTHROPIC_MODEL} for a
|
|
47
47
|
// registered custom provider, or null for the built-in/unknown (leave the default env intact).
|
|
48
48
|
// Multi-provider BYOK slice 1: a lane spawned with a `provider` id runs on that endpoint.
|
|
49
|
-
import { resolveProviderEnv, providerNameMap, publicKeyB64, announceProviders, listProviders, addProvider, removeProvider, unseal, effectiveLaneModel, sameProviderEnv, providerModel } from './providers.mjs'
|
|
49
|
+
import { resolveProviderEnv, resolveProviderRef, providerNameMap, publicKeyB64, announceProviders, listProviders, addProvider, removeProvider, unseal, effectiveLaneModel, sameProviderEnv, providerModel } from './providers.mjs'
|
|
50
50
|
import { validateProviderSwitch, providerSwitchPlan, BUILTIN_PROVIDER } from './switch-provider.mjs'
|
|
51
51
|
import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk'
|
|
52
52
|
import { z } from 'zod'
|
|
@@ -68,7 +68,7 @@ import { hostMemoryAdmission } from './host-memory.mjs'
|
|
|
68
68
|
const STRUCTURED_MODES = new Set(['default', 'acceptEdits', 'plan', 'review', 'bypassPermissions'])
|
|
69
69
|
import { FLOW_CONDUCTOR_PROMPT, FLOW_LANE_PROMPT, FLOW_CODEX_CONDUCTOR_PROMPT, FLOW_CODEX_LANE_PROMPT, buildConductorEnv, assembleCrossWaveContext, buildLanePrompt } from './flow-conductor.mjs'
|
|
70
70
|
import { legacyBuilderCompletionAllowed, normalizePlanOutput, validatePlanForRuntime, validReviewTargetShape } from './flow-task-graph.mjs'
|
|
71
|
-
import { normalizeFlowRuntime, flowLaneModelFor, flowConductorModelFor, spawnedLaneModelFor, modelCatalogValues, resolveCodexModel, assertRuntimeModelCompatible } from './flow-models.mjs'
|
|
71
|
+
import { normalizeFlowRuntime, flowLaneModelFor, flowConductorModelFor, spawnedLaneModelFor, modelCatalogValues, resolveCodexModel, resolveHermesOpenModel, assertRuntimeModelCompatible } from './flow-models.mjs'
|
|
72
72
|
// S1 (context-offload) — durable digest store. mark_flow_done digests a closed slice in;
|
|
73
73
|
// the dispatch loop reads the bounded cross-wave context back out (via assembleCrossWaveContext,
|
|
74
74
|
// which enforces the CEILING). Consume the store — the internals live in flow-context-store.mjs.
|
|
@@ -2071,32 +2071,43 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2071
2071
|
const isIndependentMain = !entry.spawnedBy && !entry.sideParent && !entry.flowRole && (entry.spawnDepth || 0) === 0
|
|
2072
2072
|
const canOpenMainConductor = isIndependentMain && entry.cascadeRole !== 'conductor'
|
|
2073
2073
|
const canSpawnWorkers = isIndependentMain
|
|
2074
|
+
const registeredProviderChoices = () => announceProviders()
|
|
2075
|
+
.filter((provider) => provider.id !== 'anthropic')
|
|
2076
|
+
.map((provider) => `${provider.name}${provider.model ? ` (${provider.model})` : ''}`)
|
|
2077
|
+
.join(', ')
|
|
2074
2078
|
const resolveAgentOpen = (args, { worker = false } = {}) => {
|
|
2075
2079
|
const runtime = args?.runtime || entry.runtime || 'claude'
|
|
2076
2080
|
if (runtime === 'codex' && args?.provider) return { error: 'A Codex terminal uses its Codex/OpenAI login in v1; custom bridge providers are not wired to Codex yet. Omit provider or open a Claude terminal for that provider.' }
|
|
2077
2081
|
if (runtime === 'hermes' && args?.provider) return { error: 'A Hermes terminal uses its isolated Hermes profile and ACP model catalog; bridge provider IDs do not apply. Omit provider and choose an exact Hermes model ID.' }
|
|
2078
|
-
|
|
2079
|
-
|
|
2082
|
+
const hermesCatalog = entry.runtime === 'hermes' && entry.models?.length
|
|
2083
|
+
? entry.models
|
|
2084
|
+
: [...sessions.values()].flatMap((session) => session.runtime === 'hermes' ? (session.models || []) : [])
|
|
2085
|
+
const catalog = runtime === 'codex' ? readCodexModels() : runtime === 'hermes' ? hermesCatalog : []
|
|
2080
2086
|
if (runtime === 'codex' && args?.model && !modelCatalogValues(catalog).has(args.model)) {
|
|
2081
2087
|
return { error: `Could not open a Codex terminal on ${JSON.stringify(args.model)} — that model is not in this host's visible Codex catalog.` }
|
|
2082
2088
|
}
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2089
|
+
const hermesModel = runtime === 'hermes'
|
|
2090
|
+
? resolveHermesOpenModel(args?.model, catalog, { strictCatalog: entry.runtime === 'hermes' })
|
|
2091
|
+
: null
|
|
2092
|
+
if (hermesModel && !hermesModel.ok) return { error: hermesModel.error }
|
|
2093
|
+
const provider = runtime === 'claude' && args?.provider ? resolveProviderRef(args.provider) : args?.provider
|
|
2094
|
+
if (runtime === 'claude' && args?.provider && !provider) {
|
|
2095
|
+
const choices = registeredProviderChoices()
|
|
2096
|
+
return { error: `Unknown registered provider ${JSON.stringify(args.provider)}. No lane was opened and the built-in Claude login was not used.${choices ? ` Available provider names: ${choices}.` : ' No custom providers are registered on this host.'}` }
|
|
2086
2097
|
}
|
|
2087
2098
|
if (runtime === 'claude' && !args?.provider && args?.model && /^gpt-/i.test(args.model)) {
|
|
2088
2099
|
return { error: `Could not open a Claude terminal on Codex model ${JSON.stringify(args.model)}. Choose runtime="codex" or a Claude model.` }
|
|
2089
2100
|
}
|
|
2090
2101
|
return {
|
|
2091
2102
|
runtime,
|
|
2092
|
-
model: args?.model || (worker && args?.sliceType
|
|
2103
|
+
model: hermesModel?.model || args?.model || (worker && args?.sliceType
|
|
2093
2104
|
? spawnedLaneModelFor({ sliceType: args.sliceType, runtime, catalog })
|
|
2094
2105
|
: undefined),
|
|
2095
2106
|
// Worker lanes are the autonomous execution unit. They never inherit a
|
|
2096
2107
|
// permission-card mode from the conductor and never honor a lower explicit
|
|
2097
2108
|
// override: spawn_terminal always opens them in bypassPermissions.
|
|
2098
2109
|
mode: worker ? 'bypassPermissions' : (args?.mode || (entry.mode === 'plan' ? 'default' : entry.mode)),
|
|
2099
|
-
provider
|
|
2110
|
+
provider,
|
|
2100
2111
|
}
|
|
2101
2112
|
}
|
|
2102
2113
|
const dispatchContext = (now = Date.now()) => ({
|
|
@@ -2350,13 +2361,13 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2350
2361
|
// conductor mains, spawned workers, Side lanes, and Flow lanes cannot use it.
|
|
2351
2362
|
...(canOpenMainConductor ? [tool(
|
|
2352
2363
|
'open_main_terminal',
|
|
2353
|
-
'Open a separate independent MAIN terminal to conduct a Cascade. Use this whenever a person explicitly asks to open, launch, start, or spawn a new Cascade/conductor terminal. This is the top-level room lifecycle, NOT Ensemble dispatch: the new terminal has no spawnedBy owner and appears as a main terminal. Never substitute spawn_terminal, which creates worker sub-terminals only. Give the conductor its full initial task; it will use spawn_terminal for its workers.',
|
|
2364
|
+
'Open a separate independent MAIN terminal to conduct a Cascade on any supported runtime: Claude (including a connected Anthropic-compatible BYOK provider), Codex, or Hermes. Use this whenever a person explicitly asks to open, launch, start, or spawn a new Cascade/conductor terminal. This is the top-level room lifecycle, NOT Ensemble dispatch: the new terminal has no spawnedBy owner and appears as a main terminal. Never substitute spawn_terminal, which creates worker sub-terminals only. Give the conductor its full initial task; it will use spawn_terminal for its workers.',
|
|
2354
2365
|
{
|
|
2355
2366
|
name: z.string().max(80).optional().describe('short label for the main conductor, e.g. "Cascade · Multi-device chaos"'),
|
|
2356
2367
|
task: z.string().min(1).describe('complete initial Cascade brief for the conductor'),
|
|
2357
|
-
model: z.string().optional().describe('optional conductor model, e.g. opus / gpt-5.6-sol'),
|
|
2358
|
-
runtime: z.enum(['claude', 'codex']).optional().describe('agent runtime;
|
|
2359
|
-
provider: z.string().optional().describe(
|
|
2368
|
+
model: z.string().optional().describe('optional conductor model, e.g. opus / gpt-5.6-sol / nous:z-ai/glm-5.2'),
|
|
2369
|
+
runtime: z.enum(['claude', 'codex', 'hermes']).optional().describe('agent runtime; every top-level terminal can open every supported runtime, defaulting to its own'),
|
|
2370
|
+
provider: z.string().optional().describe(`optional registered Claude-compatible provider name or id; omit for built-in Anthropic${registeredProviderChoices() ? `. Available now: ${registeredProviderChoices()}` : ''}`),
|
|
2360
2371
|
mode: z.enum(['default', 'acceptEdits', 'bypassPermissions', 'plan']).optional().describe('permission mode; defaults to inheriting this main terminal, except plan falls back to default'),
|
|
2361
2372
|
},
|
|
2362
2373
|
async (args) => {
|
|
@@ -2407,14 +2418,14 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2407
2418
|
// not receive this tool, so the hierarchy is capability-enforced.
|
|
2408
2419
|
...(canSpawnWorkers ? [tool(
|
|
2409
2420
|
'spawn_terminal',
|
|
2410
|
-
'Open a visible WORKER SUB-TERMINAL (
|
|
2421
|
+
'Open a visible WORKER SUB-TERMINAL on any supported runtime—Claude (including a connected Anthropic-compatible BYOK provider), Codex, or Hermes—from any top-level terminal. spawn_terminal is worker-only: it can never create a Cascade conductor or main terminal. For ordinary workers use an appropriate non-Sol tier: gpt-5.6-luna or gpt-5.4-mini for scaffold/search, gpt-5.6-terra for feature/fix, and a balanced tier for adversarial review; never select Sol for routine workers. A Hermes sliceType=review lane is structurally reviewer-scoped before ACP startup: it can inspect files and use the bridge review-check tool, but has no terminal, write, patch, delegate, browser, skill, memory, or session-history schema. Pass sliceType=scaffold for mechanical work, feature/fix for builders, and review for adversarial verification. Give the worker a bounded initial task, collect its result with read_terminal, then ALWAYS close_terminal it.',
|
|
2411
2422
|
{
|
|
2412
2423
|
name: z.string().max(80).optional().describe('a short label for the new lane so the room (and you) can find it, e.g. "Research: topologies"'),
|
|
2413
2424
|
task: z.string().optional().describe('an initial task to hand the new lane immediately; omit to open it idle'),
|
|
2414
|
-
model: z.string().optional().describe('model for the chosen runtime; Hermes
|
|
2425
|
+
model: z.string().optional().describe('model for the chosen runtime; Hermes accepts its profile default or an ACP ID such as nous:z-ai/glm-5.2'),
|
|
2415
2426
|
sliceType: z.enum(['scaffold', 'feature', 'fix', 'review']).optional().describe('optional cascade slice tier; omitted preserves normal inheritance/default, explicit model wins'),
|
|
2416
|
-
runtime: z.enum(['claude', 'codex', 'hermes']).optional().describe('agent runtime for the new lane;
|
|
2417
|
-
provider: z.string().optional().describe(
|
|
2427
|
+
runtime: z.enum(['claude', 'codex', 'hermes']).optional().describe('agent runtime for the new lane; every top-level terminal can open every supported runtime'),
|
|
2428
|
+
provider: z.string().optional().describe(`optional registered Claude-compatible provider name or id; omit for built-in Anthropic${registeredProviderChoices() ? `. Available now: ${registeredProviderChoices()}` : ''}`),
|
|
2418
2429
|
mode: z.enum(['default', 'acceptEdits', 'bypassPermissions', 'plan']).optional().describe('accepted for compatibility; spawned workers always run autonomously in bypassPermissions'),
|
|
2419
2430
|
},
|
|
2420
2431
|
async (args) => {
|
|
@@ -2666,7 +2677,7 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2666
2677
|
cascadeRole: entry.cascadeRole,
|
|
2667
2678
|
flowRole: entry.flowRole,
|
|
2668
2679
|
sideParent: entry.sideParent,
|
|
2669
|
-
}),
|
|
2680
|
+
}), canSpawnWorkers ? HERMES_VISIBLE_WORKER_FALLBACK_RULE : ''].filter(Boolean).join('\n\n')
|
|
2670
2681
|
entry.session = startStructuredSession(runtime, {
|
|
2671
2682
|
// laneModel, NOT the raw `model` param: the SDK's `model` option OVERRIDES the
|
|
2672
2683
|
// ANTHROPIC_MODEL supplied by resolveProviderEnv() in `env` below, so an inherited
|
package/flow-models.mjs
CHANGED
|
@@ -24,6 +24,38 @@ export function modelCatalogValues(catalog = []) {
|
|
|
24
24
|
)).filter(Boolean))
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
const exactHermesModelId = (value) => /^[A-Za-z0-9._-]+:[A-Za-z0-9._-]+\/[A-Za-z0-9._:/-]+$/.test(value)
|
|
28
|
+
|
|
29
|
+
const expandHermesModelAlias = (value) => {
|
|
30
|
+
if (/^glm[-_.]/i.test(value)) return `nous:z-ai/${value.toLowerCase()}`
|
|
31
|
+
const shorthand = value.match(/^([^:/]+):([^/]+)$/)
|
|
32
|
+
if (!shorthand) return value
|
|
33
|
+
const provider = shorthand[1].toLowerCase() === 'zai' ? 'z-ai' : shorthand[1].toLowerCase()
|
|
34
|
+
return `nous:${provider}/${shorthand[2]}`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve a Hermes model for any bridge opener. A Hermes parent has a live ACP
|
|
39
|
+
* catalog and remains strict. Other runtimes may use a full id or unambiguous
|
|
40
|
+
* shorthand; the child session acknowledges session/set_model before inference.
|
|
41
|
+
* Omitting a model intentionally selects the isolated Hermes profile default.
|
|
42
|
+
*/
|
|
43
|
+
export function resolveHermesOpenModel(model, catalog = [], { strictCatalog = false } = {}) {
|
|
44
|
+
const requested = String(model || '').trim()
|
|
45
|
+
if (!requested) return { ok: true, model: undefined, validatedBy: 'default' }
|
|
46
|
+
const values = [...modelCatalogValues(catalog)]
|
|
47
|
+
if (values.includes(requested)) return { ok: true, model: requested, validatedBy: 'catalog' }
|
|
48
|
+
|
|
49
|
+
const expanded = expandHermesModelAlias(requested)
|
|
50
|
+
const normalized = (value) => String(value).toLowerCase().replace(/[^a-z0-9]+/g, '')
|
|
51
|
+
const requestedKey = normalized(expanded).replace(/^nous/, '')
|
|
52
|
+
const aliases = values.filter((value) => normalized(value).replace(/^nous/, '') === requestedKey)
|
|
53
|
+
if (aliases.length === 1) return { ok: true, model: aliases[0], validatedBy: 'catalog' }
|
|
54
|
+
if (strictCatalog && values.length) return { ok: false, error: `Could not open a Hermes terminal on ${JSON.stringify(requested)} — that model is not in this session's ACP catalog.` }
|
|
55
|
+
if (!exactHermesModelId(expanded)) return { ok: false, error: `Could not resolve Hermes model ${JSON.stringify(requested)}. Use a full ACP model ID such as "nous:z-ai/glm-5.2".` }
|
|
56
|
+
return { ok: true, model: expanded, validatedBy: 'child-runtime' }
|
|
57
|
+
}
|
|
58
|
+
|
|
27
59
|
function firstVisible(candidates, catalog) {
|
|
28
60
|
const visible = modelCatalogValues(catalog)
|
|
29
61
|
for (const candidate of candidates) if (visible.has(candidate)) return candidate
|
package/package.json
CHANGED
package/providers.mjs
CHANGED
|
@@ -333,6 +333,27 @@ export function announceProviders() {
|
|
|
333
333
|
return [{ id: BUILTIN_ID, name: 'Anthropic (Claude)', model: null, group: BUILTIN_ID }, ...custom]
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
+
/**
|
|
337
|
+
* Resolve an agent-facing provider reference without ever falling through to the
|
|
338
|
+
* built-in Claude login. Accept an exact id, a unique case-insensitive display
|
|
339
|
+
* name, or a unique configured model. Unknown/ambiguous references return null
|
|
340
|
+
* so callers can fail closed and show the current safe choices.
|
|
341
|
+
*/
|
|
342
|
+
export function resolveProviderRef(ref) {
|
|
343
|
+
if (!ref || ref === BUILTIN_ID) return BUILTIN_ID
|
|
344
|
+
const raw = String(ref).trim()
|
|
345
|
+
if (!raw) return BUILTIN_ID
|
|
346
|
+
const providers = loadProviders()
|
|
347
|
+
const exact = providers.find((provider) => provider.id === raw)
|
|
348
|
+
if (exact) return exact.id
|
|
349
|
+
const needle = raw.toLocaleLowerCase()
|
|
350
|
+
const matches = providers.filter((provider) => (
|
|
351
|
+
String(provider.name || '').trim().toLocaleLowerCase() === needle
|
|
352
|
+
|| String(provider.model || '').trim().toLocaleLowerCase() === needle
|
|
353
|
+
))
|
|
354
|
+
return matches.length === 1 ? matches[0].id : null
|
|
355
|
+
}
|
|
356
|
+
|
|
336
357
|
/**
|
|
337
358
|
* id → name map for the ROOM announce's per-lane provider projection (name-only,
|
|
338
359
|
* NEVER key or baseUrl). Built once per announce so a cross-device viewer resolves
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"bundleVersion":
|
|
3
|
+
"bundleVersion": 8,
|
|
4
4
|
"contracts": [
|
|
5
5
|
{
|
|
6
6
|
"id": "room-coordination",
|
|
@@ -41,7 +41,9 @@
|
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
"id": "work-routing",
|
|
44
|
-
"version":
|
|
44
|
+
"version": 4,
|
|
45
|
+
"providerRoutingContract": "A Claude lane may select a connected Anthropic-compatible provider by durable id, unique display name, or unique configured model. Unknown or ambiguous references fail closed and must never fall through to built-in Claude.",
|
|
46
|
+
"openerParityContract": "Every top-level terminal may use spawn_terminal or open_main_terminal to open every supported structured runtime: Claude, Codex, or Hermes. Runtime-specific provider/model validation remains authoritative and occurs before inference.",
|
|
45
47
|
"routes": [
|
|
46
48
|
{
|
|
47
49
|
"id": "work-routing",
|
|
@@ -152,8 +152,7 @@ export const CODEX_THINKPOOL_FIRST_TURN_PREAMBLE = [
|
|
|
152
152
|
...THINKPOOL_REMOTE_DELIVERY_RULES,
|
|
153
153
|
].join(' ')
|
|
154
154
|
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
export const HERMES_VISIBLE_WORKER_FALLBACK_RULE = 'HERMES VISIBLE-WORKER FALLBACK: spawn_terminal may open a visible Hermes worker only from a top-level Hermes terminal and only with an explicit exact model ID advertised in this session\'s ACP catalog. If a native Claude Code worker reports an account/provider authentication or subscription-access failure, treat the entire native Claude provider as unavailable for the rest of the task—do not retry Sonnet, Opus, or Haiku aliases on it. For a manual Claude-family review fallback, use runtime="hermes", the exact Nous Claude model suggested by read_terminal, mode="default", and sliceType="review". This fallback is permission-gated, not structurally read-only: instruct it not to edit, stop/deny any write request, and never call it Flow/reviewer safety. Never use hidden Hermes delegate_task.'
|
|
155
|
+
// Every top-level main can open every supported runtime. Hermes model selection
|
|
156
|
+
// is acknowledged by the child ACP session before inference, so it is safe for a
|
|
157
|
+
// Claude or Codex parent to request a visible Hermes lane.
|
|
158
|
+
export const HERMES_VISIBLE_WORKER_FALLBACK_RULE = 'UNIVERSAL OPENER ROUTING: a top-level terminal of any runtime may use spawn_terminal or open_main_terminal to open Claude (including a connected Anthropic-compatible BYOK provider), Codex, or Hermes. For Hermes, omit model to use the isolated profile default or pass an ACP ID such as model="nous:z-ai/glm-5.2"; common GLM shorthand is normalized and the child acknowledges the selected model before inference. If a native Claude Code worker reports an account/provider authentication or subscription-access failure, treat the entire native Claude provider as unavailable for the rest of the task—do not retry Sonnet, Opus, or Haiku aliases on it. For a manual Claude-family review fallback, use runtime="hermes", the exact Nous Claude model suggested by read_terminal, mode="default", and sliceType="review". This fallback is permission-gated, not structurally read-only: instruct it not to edit, stop/deny any write request, and never call it Flow/reviewer safety. Never use hidden Hermes delegate_task.'
|