thinkpool-pair 0.7.148 → 0.7.150
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 +25 -6
- package/flow-task-graph.mjs +31 -0
- package/package.json +1 -1
package/bridge.mjs
CHANGED
|
@@ -41,7 +41,7 @@ import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk'
|
|
|
41
41
|
import { z } from 'zod'
|
|
42
42
|
import { startClaudeSession, oneShotSummary } from './claude-session.mjs'
|
|
43
43
|
import { FLOW_CONDUCTOR_PROMPT, FLOW_LANE_PROMPT, buildConductorEnv, assembleCrossWaveContext, buildLanePrompt } from './flow-conductor.mjs'
|
|
44
|
-
import { normalizePlanOutput } from './flow-task-graph.mjs' // FL-B1 — validate the conductor's submit_flow_plan task-graph
|
|
44
|
+
import { normalizePlanOutput, laneModelFor } from './flow-task-graph.mjs' // FL-B1 — validate the conductor's submit_flow_plan task-graph; laneModelFor — per-slice model tier (2026-07-03-flow-lane-model-tiers)
|
|
45
45
|
// S1 (context-offload) — durable digest store. mark_flow_done digests a closed slice in;
|
|
46
46
|
// the dispatch loop reads the bounded cross-wave context back out (via assembleCrossWaveContext,
|
|
47
47
|
// which enforces the CEILING). Consume the store — the internals live in flow-context-store.mjs.
|
|
@@ -1565,7 +1565,7 @@ function openStructured({ id, model, resume, log, commands, mode, spawnedBy, rol
|
|
|
1565
1565
|
// recursively spawn/post onward — the fork-bomb breaker (mirrors Tier C).
|
|
1566
1566
|
tool(
|
|
1567
1567
|
'spawn_terminal',
|
|
1568
|
-
'Open a NEW agent terminal (a fresh Claude lane) in this ThinkPool Code room — YOUR lane to use, not a sibling that is already working. Use it to fan work out: give it an initial `task` and it runs in parallel in its own lane; collect its result later with read_terminal, and close it with close_terminal when done. By default the new lane INHERITS your current permission mode — so if you are in bypass, it runs autonomously with no per-action clicking; pass `mode` to override. ALWAYS prefer spawning a fresh lane over post_to_terminal into one that is already busy. Bounded by the room terminal cap and a per-turn limit.',
|
|
1568
|
+
'Open a NEW agent terminal (a fresh Claude lane) in this ThinkPool Code room — YOUR lane to use, not a sibling that is already working. Use it to fan work out: give it an initial `task` and it runs in parallel in its own lane; collect its result later with read_terminal, and close it with close_terminal when done. By default the new lane INHERITS your current permission mode — so if you are in bypass, it runs autonomously with no per-action clicking; pass `mode` to override. ALWAYS prefer spawning a fresh lane over post_to_terminal into one that is already busy. Tier guidance: default worker lanes to opus; use sonnet for mechanical work (renames, boilerplate, doc sweeps); reserve your own model for planning/review. Bounded by the room terminal cap and a per-turn limit.',
|
|
1569
1569
|
{
|
|
1570
1570
|
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"'),
|
|
1571
1571
|
task: z.string().optional().describe('an initial task to hand the new lane immediately; omit to open it idle'),
|
|
@@ -1840,8 +1840,21 @@ function openStructured({ id, model, resume, log, commands, mode, spawnedBy, rol
|
|
|
1840
1840
|
// mid-turn) flash a brief "Compacting…" beat so it isn't silent — we only learn of it
|
|
1841
1841
|
// post-hoc, so there's no real duration to animate (Max, 2026-07-02).
|
|
1842
1842
|
if (evt.kind === 'compaction') {
|
|
1843
|
-
if (entry.compacting)
|
|
1844
|
-
|
|
1843
|
+
if (entry.compacting) {
|
|
1844
|
+
// MANUAL /compact: attribute the recap card, then CLEAR the "Compacting…"
|
|
1845
|
+
// indicator right here — the compact_boundary milestone IS the "done" signal.
|
|
1846
|
+
// We used to wait for the turn's `result` (below), but a real /compact re-inits
|
|
1847
|
+
// the SDK session, so the result can land after entry.compacting was lost →
|
|
1848
|
+
// the indicator hung forever and kept counting (Max, 2026-07-03). The milestone
|
|
1849
|
+
// is emitted with compacting still true (that's how we attribute `by`), so it's
|
|
1850
|
+
// the reliable place to stop it. The result handler below stays as the backstop
|
|
1851
|
+
// for a NO-OP /compact ("nothing to compact"), which emits a result but no
|
|
1852
|
+
// compaction milestone.
|
|
1853
|
+
evt.by = entry.compactBy || null
|
|
1854
|
+
entry.compacting = false
|
|
1855
|
+
entry.compactBy = null
|
|
1856
|
+
bcast('code-event', { term: id, evt: { kind: 'compact', status: 'done', ts: Date.now() } })
|
|
1857
|
+
} else if (evt.trigger === 'auto') {
|
|
1845
1858
|
bcast('code-event', { term: id, evt: { kind: 'compact', status: 'start', ts: Date.now() } })
|
|
1846
1859
|
setTimeout(() => { bcast('code-event', { term: id, evt: { kind: 'compact', status: 'done', ts: Date.now() } }) }, 2500)
|
|
1847
1860
|
}
|
|
@@ -2558,7 +2571,10 @@ flowChannel
|
|
|
2558
2571
|
// lanes form ONE Ensemble group (same ensembleRoot) — the drilled flow shows a row of
|
|
2559
2572
|
// all its terminals to hop between, with a working Back. (No flowTaskKey → it's still
|
|
2560
2573
|
// the conductor, excluded from the lane cap below.)
|
|
2561
|
-
|
|
2574
|
+
// Model tiers (2026-07-03-flow-lane-model-tiers): the conductor keeps whatever brain it was
|
|
2575
|
+
// given by default (TP_FLOW_CONDUCTOR_MODEL unset → undefined → today's behavior); set the env
|
|
2576
|
+
// to pin a cheaper/smarter conductor. Lanes get tiered below via laneModelFor.
|
|
2577
|
+
openStructured({ id: cid, model: process.env.TP_FLOW_CONDUCTOR_MODEL || undefined, mode: 'default', rolePrompt: FLOW_CONDUCTOR_PROMPT, flowSessionId: payload.flowId, spawnedBy: `flow:${payload.flowId}` })
|
|
2562
2578
|
const ce = sessions.get(cid)
|
|
2563
2579
|
if (ce?.session) { try { ce.session.sendTurn(payload.prompt || '') } catch { /* session still starting */ } }
|
|
2564
2580
|
process.stderr.write(`\n ${A.mag}◆ flow conductor launched — flow ${String(payload.flowId).slice(0, 8)} (plan mode).${A.rst}\n`)
|
|
@@ -2635,7 +2651,10 @@ flowChannel
|
|
|
2635
2651
|
// no dangling tool_use → no 400) instead of a cold start; undefined for a fresh lane.
|
|
2636
2652
|
const laneBase = isReview ? FLOW_REVIEWER_PROMPT : FLOW_LANE_PROMPT
|
|
2637
2653
|
const laneRolePrompt = buildLanePrompt({ base: laneBase })
|
|
2638
|
-
|
|
2654
|
+
// Model tiers (2026-07-03-flow-lane-model-tiers): pick the lane's brain by slice_type
|
|
2655
|
+
// (scaffold→sonnet, feature/fix/review→opus; env can blanket-override or `inherit` to
|
|
2656
|
+
// restore today's exact behavior). undefined → no model key passed (openStructured default).
|
|
2657
|
+
openStructured({ id: laneId, cwd: dir, model: laneModelFor(t.slice_type), mode: 'bypassPermissions', rolePrompt: laneRolePrompt, flowSessionId: payload.flowId, flowTaskKey: t.task_key, spawnedBy: `flow:${payload.flowId}`, resume: redispatch?.resumeSessionId || undefined, reviewSliceRoots })
|
|
2639
2658
|
const le = sessions.get(laneId)
|
|
2640
2659
|
if (le) {
|
|
2641
2660
|
// S4 — stamp the surviving revert target on the resumed lane so a later reviewer still
|
package/flow-task-graph.mjs
CHANGED
|
@@ -172,3 +172,34 @@ export function normalizePlanOutput (raw) {
|
|
|
172
172
|
validateDag(tasks)
|
|
173
173
|
return { summary, tasks }
|
|
174
174
|
}
|
|
175
|
+
|
|
176
|
+
// ── Flow lane model tiers ───────────────────────────────────────────────────
|
|
177
|
+
// Spec: docs/specs/2026-07-03-flow-lane-model-tiers.md
|
|
178
|
+
// The conductor thinks; the worker lanes type. Running every lane on the same
|
|
179
|
+
// expensive brain is wasteful — the tier map picks a model per slice_type so
|
|
180
|
+
// intelligence matches the job:
|
|
181
|
+
// scaffold → sonnet (boilerplate / project bootstrap — mechanical, cheap)
|
|
182
|
+
// feature → opus (real building — needs the smart brain)
|
|
183
|
+
// fix → opus (root-cause debugging — needs the smart brain)
|
|
184
|
+
// review → opus (adversarial verification is intelligence-SENSITIVE; never cheapen it)
|
|
185
|
+
// Unknown/missing slice types fall to opus (safe default — never silently cheapen).
|
|
186
|
+
// Dev-only, no product gating. The conductor's own model is untouched by this map
|
|
187
|
+
// (bridge reads TP_FLOW_CONDUCTOR_MODEL at the conductor spawn site instead).
|
|
188
|
+
//
|
|
189
|
+
// Env overrides (checked FIRST, in order):
|
|
190
|
+
// TP_FLOW_LANE_MODEL='inherit' → return undefined (no model key passed → today's exact behavior)
|
|
191
|
+
// TP_FLOW_LANE_MODEL=<model> → blanket-override every lane to that model
|
|
192
|
+
// (empty-string / unset) → fall through to the tier map
|
|
193
|
+
const LANE_MODEL_TIERS = {
|
|
194
|
+
scaffold: 'sonnet',
|
|
195
|
+
feature: 'opus',
|
|
196
|
+
fix: 'opus',
|
|
197
|
+
review: 'opus',
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function laneModelFor (sliceType, env = process.env) {
|
|
201
|
+
const override = env.TP_FLOW_LANE_MODEL
|
|
202
|
+
if (override === 'inherit') return undefined // sentinel — restore today's behavior exactly
|
|
203
|
+
if (override) return override // blanket override for all lanes (non-empty string)
|
|
204
|
+
return LANE_MODEL_TIERS[sliceType] || 'opus' // tier map; unknown/missing → opus (never cheapen by accident)
|
|
205
|
+
}
|