opencode-swarm 7.136.2 → 7.136.4
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/.opencode/skills/brainstorm/SKILL.md +1 -1
- package/.opencode/skills/clarify/SKILL.md +3 -3
- package/.opencode/skills/clarify-spec/SKILL.md +2 -2
- package/.opencode/skills/consult/SKILL.md +1 -1
- package/.opencode/skills/council/SKILL.md +1 -1
- package/.opencode/skills/critic-gate/SKILL.md +8 -3
- package/.opencode/skills/discover/SKILL.md +1 -1
- package/.opencode/skills/execute/SKILL.md +1 -1
- package/.opencode/skills/gate-attribution/SKILL.md +1 -1
- package/.opencode/skills/issue-ingest/SKILL.md +3 -3
- package/.opencode/skills/phase-wrap/SKILL.md +1 -1
- package/.opencode/skills/plan/SKILL.md +3 -3
- package/.opencode/skills/pre-phase-briefing/SKILL.md +1 -1
- package/.opencode/skills/resume/SKILL.md +1 -1
- package/.opencode/skills/specify/SKILL.md +1 -1
- package/dist/cli/{config-doctor-b67nb84b.js → config-doctor-qwp5y9dk.js} +2 -2
- package/dist/cli/{core-4z1s2ak1.js → core-jpjk2qvt.js} +1 -1
- package/dist/cli/{curation-policy-kvzhfaj1.js → curation-policy-8tsbaa4q.js} +6 -6
- package/dist/cli/{curator-nrmj8bds.js → curator-763smew7.js} +25 -25
- package/dist/cli/{curator-llm-factory-x7kvry9x.js → curator-llm-factory-dg8mmzy7.js} +25 -25
- package/dist/cli/{evidence-summary-service-cf8sz2bq.js → evidence-summary-service-nse06dqr.js} +7 -7
- package/dist/cli/{gate-evidence-aenyz6vt.js → gate-evidence-kdygjr56.js} +4 -4
- package/dist/cli/{guardrail-explain-5kd44rcj.js → guardrail-explain-6jc012g3.js} +26 -26
- package/dist/cli/{guardrail-log-aksrzqdx.js → guardrail-log-86aw7fne.js} +3 -3
- package/dist/cli/{hive-promoter-cy21rhnd.js → hive-promoter-en786ja4.js} +25 -25
- package/dist/cli/{index-84nz7bhe.js → index-06htxvzq.js} +3 -3
- package/dist/cli/{index-s0vahtdm.js → index-1hef020t.js} +1 -1
- package/dist/cli/{index-kwwxevne.js → index-45y0y3xh.js} +5 -5
- package/dist/cli/{index-ad6j0m7k.js → index-4cv991ra.js} +3 -3
- package/dist/cli/{index-45t7w06b.js → index-7t3vjw5e.js} +1 -1
- package/dist/cli/{index-m5fw4mer.js → index-89fpahtg.js} +1 -1
- package/dist/cli/{index-rw3f73aq.js → index-94pvh7hc.js} +2 -2
- package/dist/cli/{index-ey29aap6.js → index-b3jfcptk.js} +1 -1
- package/dist/cli/{index-0755132s.js → index-bb3ks0v3.js} +6 -6
- package/dist/cli/{index-7a2hm51h.js → index-cze4bq1x.js} +41 -0
- package/dist/cli/{index-mrtms113.js → index-d2sf9an1.js} +2 -2
- package/dist/cli/{index-wxyxf0bd.js → index-d61h3f3h.js} +2 -2
- package/dist/cli/{index-ckybsn6p.js → index-et8b70c8.js} +27 -27
- package/dist/cli/{index-1kn24ja0.js → index-fhnbzz75.js} +2 -2
- package/dist/cli/{index-s0rbdp61.js → index-fkdxfdbx.js} +4 -4
- package/dist/cli/{index-37v2wxqe.js → index-g0sdhyqk.js} +1 -1
- package/dist/cli/{index-7ayaq27q.js → index-gjs5g97v.js} +1 -1
- package/dist/cli/{index-c9ddxv4k.js → index-hh34tyv7.js} +1 -1
- package/dist/cli/{index-tcn457d5.js → index-kym0cctr.js} +1 -1
- package/dist/cli/{index-z1tm47nj.js → index-ne28wyyc.js} +2 -2
- package/dist/cli/{index-wjm896ey.js → index-nfm9f10v.js} +1 -1
- package/dist/cli/{index-y0xaq4f3.js → index-pq8ge6fe.js} +8 -2
- package/dist/cli/{index-azghvnja.js → index-qv1xc6rd.js} +2 -2
- package/dist/cli/{index-wnz282j3.js → index-rkp6jvc3.js} +2 -2
- package/dist/cli/{index-sr2cynyw.js → index-t1s4b5ha.js} +108 -51
- package/dist/cli/{index-09b9zncg.js → index-tncy55bp.js} +1 -1
- package/dist/cli/{index-vg3yx648.js → index-w6j1n5az.js} +1 -1
- package/dist/cli/{index-21115szq.js → index-wy2f83j5.js} +5 -5
- package/dist/cli/{index-dzyjb33e.js → index-zzhyws9g.js} +1 -1
- package/dist/cli/index.js +25 -25
- package/dist/cli/{knowledge-escalator-ta3xjrdj.js → knowledge-escalator-e815bveb.js} +7 -7
- package/dist/cli/{knowledge-events-dk6banmz.js → knowledge-events-dz2tyhpw.js} +5 -5
- package/dist/cli/{knowledge-link-mm1w967j.js → knowledge-link-zr40rnwr.js} +4 -4
- package/dist/cli/{knowledge-store-h3jz10bv.js → knowledge-store-97qr7m9k.js} +5 -5
- package/dist/cli/{knowledge-validator-jsz1dxkr.js → knowledge-validator-xsetvy4v.js} +8 -8
- package/dist/cli/{pending-delegations-qajsxct0.js → pending-delegations-0h5b18p7.js} +3 -3
- package/dist/cli/{pr-subscriptions-qhr41epq.js → pr-subscriptions-jn0h047q.js} +3 -3
- package/dist/cli/{runner-2v413r74.js → runner-deeswadt.js} +5 -5
- package/dist/cli/{scan-cursor-48gwzh9c.js → scan-cursor-129fwf7e.js} +6 -6
- package/dist/cli/{schema-f937b9cs.js → schema-3xdza5gg.js} +1 -1
- package/dist/cli/{scope-persistence-h2fpgxww.js → scope-persistence-5xc9ntdh.js} +4 -4
- package/dist/cli/{skill-generator-4p10ktw1.js → skill-generator-8zhprasg.js} +9 -9
- package/dist/cli/{telemetry-859khp82.js → telemetry-6678gya0.js} +1 -1
- package/dist/cli/{worktree-collision-ownership-13btcj9g.js → worktree-collision-ownership-wt7cc850.js} +3 -3
- package/dist/config/schema.d.ts +10 -0
- package/dist/hooks/context-budget.d.ts +8 -0
- package/dist/hooks/delegation-gate.d.ts +12 -1
- package/dist/hooks/gate-denial-tracker.d.ts +175 -0
- package/dist/hooks/guardrails/execution-episode.d.ts +41 -0
- package/dist/hooks/guardrails/execution-stall.d.ts +285 -0
- package/dist/hooks/guardrails/internals-guard.d.ts +117 -0
- package/dist/hooks/guardrails/messages-transform.d.ts +85 -0
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/message-priority.d.ts +62 -4
- package/dist/hooks/trajectory-logger.d.ts +76 -0
- package/dist/index.js +477 -463
- package/dist/memory/schema.d.ts +3 -3
- package/dist/prm/index.d.ts +2 -0
- package/dist/state.d.ts +32 -0
- package/dist/telemetry.d.ts +66 -1
- package/dist/types/events.d.ts +14 -1
- package/package.json +2 -1
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARCHITECT EXECUTION-STALL DETECTOR (issue #2063, workstream B5)
|
|
3
|
+
*
|
|
4
|
+
* Shape (ii) of the reported loop is an architect that keeps making tool calls
|
|
5
|
+
* — reads, greps, bash probes — while nothing in the world changes: no
|
|
6
|
+
* delegation completes, no file is written, no task status moves. The no-op
|
|
7
|
+
* detector (B2) observes this but is advisory by design, because read-only
|
|
8
|
+
* modes legitimately make hundreds of non-write calls. This module is the hard
|
|
9
|
+
* lever, and the thing that makes it safe to be hard is the EPISODE.
|
|
10
|
+
*
|
|
11
|
+
* ── Episode ───────────────────────────────────────────────────────────────
|
|
12
|
+
* An episode ARMS when the session actually attempts execution work:
|
|
13
|
+
* (a) a `Task` dispatch to a mutating/verifying role is ATTEMPTED — counted
|
|
14
|
+
* on the ATTEMPT, in `tool.execute.before`, so a dispatch that a LATER
|
|
15
|
+
* gate denies (the motivating `ACCEPTANCE_FIELD_REQUIRED` loop) still
|
|
16
|
+
* arms the episode. Guardrails' toolBefore runs at `src/index.ts:2710`,
|
|
17
|
+
* the delegation gate at `:2719`, so ordering guarantees this; or
|
|
18
|
+
* (b) `update_task_status(..., in_progress)` succeeds.
|
|
19
|
+
*
|
|
20
|
+
* An episode DISARMS on either of two conditions:
|
|
21
|
+
*
|
|
22
|
+
* (1) IDLENESS, not elapsed time since arming: `execution_stall_episode_minutes`
|
|
23
|
+
* with NO TOOL CALLS AT ALL. This distinction is the whole point (critic
|
|
24
|
+
* round-3 fix 1). Keying the lapse to arming time would let an hour-long
|
|
25
|
+
* slow stall time out and reset before it ever reached the hard rung;
|
|
26
|
+
* keying it to idleness means continuous non-progress activity KEEPS the
|
|
27
|
+
* episode armed and the counter climbing, while a genuinely abandoned
|
|
28
|
+
* episode ages out.
|
|
29
|
+
*
|
|
30
|
+
* (2) NO OPEN TASK: the plan carries no task with status `in_progress`
|
|
31
|
+
* (reviewer round-4 fix, REQUIRED 1). Idleness alone was not enough: a
|
|
32
|
+
* CONTINUOUSLY-ACTIVE architect that finishes its execution phase with a
|
|
33
|
+
* final `update_task_status(..., completed)` and then flows into commit /
|
|
34
|
+
* CI / reporting work never goes idle, so it kept accumulating
|
|
35
|
+
* non-progress `bash`/`read` calls with no reachable progress event and
|
|
36
|
+
* was hard-denied at 60 while doing exactly the right thing. This
|
|
37
|
+
* condition is the symmetric counterpart of arming path (b): an episode
|
|
38
|
+
* that arms when a task opens must end when no task is open.
|
|
39
|
+
*
|
|
40
|
+
* Lapse (1) is evaluated LAZILY on the next observed tool call. There are no
|
|
41
|
+
* timers — invariant 1 forbids background work on the plugin, and a timer
|
|
42
|
+
* would keep the process alive. The observable consequence: an episode that
|
|
43
|
+
* went idle stays nominally armed until one more tool call arrives, at which
|
|
44
|
+
* point it disarms BEFORE that call is counted.
|
|
45
|
+
*
|
|
46
|
+
* Disarm (2) is evaluated at two places, both in `tool.execute.after`:
|
|
47
|
+
* - immediately after a successful `update_task_status` to a NON-`in_progress`
|
|
48
|
+
* status (the in-band signal — `plan.json` is current there, because
|
|
49
|
+
* `plan/manager.savePlan` writes the projection as its LAST step
|
|
50
|
+
* (`src/plan/manager.ts:215` documents this) and `updateTaskStatus` awaits
|
|
51
|
+
* it (`:2144`) before `executeUpdateTaskStatus` builds the `success: true`
|
|
52
|
+
* payload (`src/tools/update-task-status.ts:1329`) this hook reads); and
|
|
53
|
+
* - inside the periodic workspace probe, so an OUT-OF-BAND plan change (the
|
|
54
|
+
* plan file edited directly, a sub-agent path that settles the last task)
|
|
55
|
+
* eventually disarms too. The probe already does I/O once per
|
|
56
|
+
* WORKSPACE_PROBE_EVERY_CALLS calls, so the extra read is bounded.
|
|
57
|
+
*
|
|
58
|
+
* DEVIATION (deliberate): a MISSING, unreadable, or malformed `plan.json`
|
|
59
|
+
* answers `'unknown'`, and `'unknown'` does NOT disarm. Read literally, "no
|
|
60
|
+
* task with status in_progress" is also true of a plan that does not exist —
|
|
61
|
+
* but a session with no plan cannot be "finishing its execution phase", and
|
|
62
|
+
* treating an unreadable plan as a disarm would silently delete the lever for
|
|
63
|
+
* every plan-less architect loop. Only POSITIVE evidence (a parsed plan whose
|
|
64
|
+
* tasks are all non-`in_progress`) disarms.
|
|
65
|
+
*
|
|
66
|
+
* ── Progress ──────────────────────────────────────────────────────────────
|
|
67
|
+
* A progress event resets the counter and clears any active denial rung, but
|
|
68
|
+
* KEEPS the episode armed:
|
|
69
|
+
* - successful completion of a `Task` dispatch to a mutating/verifying role.
|
|
70
|
+
* Read-only roles (`explorer`, `sme`) deliberately do NOT count: otherwise
|
|
71
|
+
* "delegate the spelunking" is a trivial escape from the whole lever.
|
|
72
|
+
* - any file-write tool success (same `WRITE_TOOL_NAMES` set as B2).
|
|
73
|
+
* - `update_task_status` success, ANY status.
|
|
74
|
+
* - a periodic workspace-diff probe showing new changes.
|
|
75
|
+
*
|
|
76
|
+
* ── Ladder ────────────────────────────────────────────────────────────────
|
|
77
|
+
* `execution_stall_warn_calls` (default 30) → strong advisory, once/streak.
|
|
78
|
+
* `execution_stall_stop_calls` (default 60) → HARD DENY of non-productive
|
|
79
|
+
* tools ONLY (read/glob/grep/bash/shell). `Task`, `update_task_status`,
|
|
80
|
+
* and every plan/status/query/swarm tool stay open BY CONSTRUCTION: the
|
|
81
|
+
* deny set is a closed allowlist of denied names, not an exclusion list.
|
|
82
|
+
*
|
|
83
|
+
* ── Scope ─────────────────────────────────────────────────────────────────
|
|
84
|
+
* Architect sessions only. A subagent has a real budget window and the
|
|
85
|
+
* existing circuit breaker; this lever exists for the session that is exempt
|
|
86
|
+
* from both. Determined the same way every other guardrail does it
|
|
87
|
+
* (`swarmState.activeAgent` → `stripKnownSwarmPrefix` → `ORCHESTRATOR_NAME`),
|
|
88
|
+
* falling back to the session's own `agentName`.
|
|
89
|
+
*
|
|
90
|
+
* Denials are thrown from the fail-closed chain, so they flow through the B1
|
|
91
|
+
* gate-denial tracker: an architect that keeps retrying the denied read
|
|
92
|
+
* escalates to the STOP directive.
|
|
93
|
+
*/
|
|
94
|
+
import type { BackgroundWorkspaceSnapshot } from '../../background/pending-delegations.js';
|
|
95
|
+
import { captureWorkspaceSnapshot, changedFilesSinceSnapshot } from '../../background/workspace-snapshot.js';
|
|
96
|
+
/** Default advisory rung (mirrors `guardrails.execution_stall_warn_calls`). */
|
|
97
|
+
export declare const DEFAULT_EXECUTION_STALL_WARN_CALLS = 30;
|
|
98
|
+
/** Default hard rung (mirrors `guardrails.execution_stall_stop_calls`). */
|
|
99
|
+
export declare const DEFAULT_EXECUTION_STALL_STOP_CALLS = 60;
|
|
100
|
+
/** Default idleness lapse (mirrors `guardrails.execution_stall_episode_minutes`). */
|
|
101
|
+
export declare const DEFAULT_EXECUTION_STALL_EPISODE_MINUTES = 30;
|
|
102
|
+
/**
|
|
103
|
+
* Canonical roles whose dispatch arms an episode and whose COMPLETION counts as
|
|
104
|
+
* progress.
|
|
105
|
+
*
|
|
106
|
+
* `security_reviewer` is the `qa-gate-pipeline.ts` id for the security review
|
|
107
|
+
* lane. It is not itself a member of `ALL_AGENT_NAMES`, so the canonicalizer
|
|
108
|
+
* resolves `security_reviewer` / `security-reviewer` to `reviewer` via its
|
|
109
|
+
* longest-suffix scan — which is already in this set. The explicit entry is
|
|
110
|
+
* kept so the set reads as the spec states it and so a future promotion of
|
|
111
|
+
* `security_reviewer` to a canonical role needs no change here.
|
|
112
|
+
*/
|
|
113
|
+
export declare const MUTATING_DELEGATION_ROLES: ReadonlySet<string>;
|
|
114
|
+
/**
|
|
115
|
+
* The ONLY tools the hard rung denies.
|
|
116
|
+
*
|
|
117
|
+
* A closed allowlist of DENIED names — never an exclusion list — so a tool that
|
|
118
|
+
* did not exist when this was written can never accidentally become deniable.
|
|
119
|
+
* `shell` accompanies `bash` for the same reason as in `internals-guard.ts`:
|
|
120
|
+
* `tool-before.ts` treats the pair as one tool everywhere else.
|
|
121
|
+
*/
|
|
122
|
+
export declare const EXECUTION_STALL_DENIED_TOOLS: ReadonlySet<string>;
|
|
123
|
+
/**
|
|
124
|
+
* Bound on tracked sessions (invariant 8). Same order as the no-op detector's
|
|
125
|
+
* `MAX_TRACKED_NO_OP_SESSIONS`.
|
|
126
|
+
*/
|
|
127
|
+
export declare const MAX_TRACKED_STALL_SESSIONS = 200;
|
|
128
|
+
interface ExecutionStallState {
|
|
129
|
+
armed: boolean;
|
|
130
|
+
/** Non-progress tool calls since the last progress event. Armed only. */
|
|
131
|
+
nonProgressCalls: number;
|
|
132
|
+
/** Wall clock of the last observed tool call; drives the idleness lapse. */
|
|
133
|
+
lastToolCallAt: number;
|
|
134
|
+
/** Advisory rung latch — one advisory per non-progress streak. */
|
|
135
|
+
warnIssued: boolean;
|
|
136
|
+
/** Hard-rung telemetry latch — one event per non-progress streak. */
|
|
137
|
+
denyTelemetryIssued: boolean;
|
|
138
|
+
/** Baseline for the workspace-diff probe; null until first captured. */
|
|
139
|
+
workspaceBaseline: BackgroundWorkspaceSnapshot | null;
|
|
140
|
+
/** Set on arm; the baseline is captured on the next `toolAfter`. */
|
|
141
|
+
needsWorkspaceBaseline: boolean;
|
|
142
|
+
/** Calls counted since the last workspace probe. */
|
|
143
|
+
callsSinceWorkspaceProbe: number;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* True when `sessionID` is the architect session.
|
|
147
|
+
*
|
|
148
|
+
* Matches the convention in `messages-transform.ts:302-307` and
|
|
149
|
+
* `tool-before.ts:1838-1850`: `activeAgent` wins, the session's own `agentName`
|
|
150
|
+
* is the fallback, and an unknown session is NOT the architect (fail open — a
|
|
151
|
+
* lever that cannot identify its subject stays silent).
|
|
152
|
+
*/
|
|
153
|
+
export declare function isArchitectStallSession(sessionID: string): boolean;
|
|
154
|
+
/**
|
|
155
|
+
* Materialize the architect's `agentSessions` entry.
|
|
156
|
+
*
|
|
157
|
+
* DEVIATION, recorded deliberately: `execution-episode.ts` documents that its
|
|
158
|
+
* setter no-ops for an unknown session so a containment lever cannot
|
|
159
|
+
* materialize session state as a side effect. But the architect legitimately
|
|
160
|
+
* has NO `agentSessions` entry when guardrails' `toolBefore` runs —
|
|
161
|
+
* `resolveSessionAndWindow` returns `null` for it before ever calling
|
|
162
|
+
* `ensureAgentSession` (`tool-before.ts:1842`), and `src/index.ts` only ensures
|
|
163
|
+
* one later in the chain (`:2804`, `:2877`) and conditionally. Without this,
|
|
164
|
+
* arming would silently write nothing, B3's consumer would never fire, and
|
|
165
|
+
* `pushAdvisory` would have no queue — i.e. the producer would be unwired.
|
|
166
|
+
*
|
|
167
|
+
* The agent name is passed ONLY when creating a new entry. Passing it to an
|
|
168
|
+
* existing session triggers `ensureAgentSession`'s rename path
|
|
169
|
+
* (`telemetry.agentActivated`, `delegationActive = false`, circuit reset),
|
|
170
|
+
* which this lever must never cause.
|
|
171
|
+
*/
|
|
172
|
+
declare function ensureArchitectSession(sessionID: string): void;
|
|
173
|
+
/**
|
|
174
|
+
* Canonical role of a `Task` dispatch, or `null` when the call is not a
|
|
175
|
+
* delegation. Uses the canonical resolver rather than string literals so
|
|
176
|
+
* prefixed names (`mega_coder`) and hyphenated aliases resolve correctly.
|
|
177
|
+
*/
|
|
178
|
+
export declare function canonicalDispatchRole(tool: string, args: unknown): string | null;
|
|
179
|
+
/**
|
|
180
|
+
* Tri-state answer to "does the plan still carry an OPEN (`in_progress`) task?".
|
|
181
|
+
*
|
|
182
|
+
* `'unknown'` is NOT a synonym for `'none'` — see the DEVIATION note in the
|
|
183
|
+
* module header. Only `'none'` disarms an episode.
|
|
184
|
+
*/
|
|
185
|
+
export type PlanOpenTaskState = 'open' | 'none' | 'unknown';
|
|
186
|
+
/**
|
|
187
|
+
* Read `.swarm/plan.json` and report whether any task is still `in_progress`.
|
|
188
|
+
*
|
|
189
|
+
* Reads the PROJECTION rather than replaying the ledger, matching the existing
|
|
190
|
+
* cheap in-hook predicates (`delegation-gate.ts:2046`,
|
|
191
|
+
* `context-capsule-inject.ts:86`): this is a containment predicate, not a plan
|
|
192
|
+
* mutation, and invariant 5 only forbids *writing* outside the ledger path.
|
|
193
|
+
* Synchronous and never throws — the callers are inside a `tool.execute.after`
|
|
194
|
+
* hook that must not become a failure surface.
|
|
195
|
+
*/
|
|
196
|
+
export declare function readPlanOpenTaskState(directory: string): PlanOpenTaskState;
|
|
197
|
+
export interface ExecutionStallOptions {
|
|
198
|
+
/**
|
|
199
|
+
* `guardrails.enabled`. Mirrors `GateDenialOptions.enabled` (B1): when a
|
|
200
|
+
* user turns guardrails off, this lever must be fully inert — no arming, no
|
|
201
|
+
* counting, no advisory, no denial.
|
|
202
|
+
*/
|
|
203
|
+
enabled?: boolean;
|
|
204
|
+
/** `guardrails.execution_stall_warn_calls` */
|
|
205
|
+
warnCalls?: number;
|
|
206
|
+
/** `guardrails.execution_stall_stop_calls` */
|
|
207
|
+
stopCalls?: number;
|
|
208
|
+
/** `guardrails.execution_stall_episode_minutes` */
|
|
209
|
+
episodeMinutes?: number;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* The advisory text at the warn rung. Exported so tests pin the wording.
|
|
213
|
+
*/
|
|
214
|
+
export declare function executionStallAdvisoryText(count: number): string;
|
|
215
|
+
/**
|
|
216
|
+
* The denial text at the hard rung. Leading token is the `EXECUTION_STALL`
|
|
217
|
+
* code that B1's `deriveGateDenialCode` keys its streak on.
|
|
218
|
+
*/
|
|
219
|
+
export declare function executionStallDenialText(count: number, normalizedTool: string): string;
|
|
220
|
+
/**
|
|
221
|
+
* `tool.execute.before`, EARLY (before any guardrails gate can throw).
|
|
222
|
+
*
|
|
223
|
+
* Pure bookkeeping: lapse evaluation, episode arming, non-progress counting,
|
|
224
|
+
* and the advisory rung. NEVER throws — the denial is a separate call
|
|
225
|
+
* ({@link enforceExecutionStallDenial}) placed in the handler tail so the
|
|
226
|
+
* circuit-breaker accounting in `tool-before.ts` still runs for a denied call,
|
|
227
|
+
* exactly as C3 required for the PRM hard stop.
|
|
228
|
+
*/
|
|
229
|
+
export declare function observeExecutionStallToolCall(params: {
|
|
230
|
+
sessionID: string;
|
|
231
|
+
tool: string;
|
|
232
|
+
args: unknown;
|
|
233
|
+
callID: string;
|
|
234
|
+
options?: ExecutionStallOptions;
|
|
235
|
+
}): void;
|
|
236
|
+
/**
|
|
237
|
+
* `tool.execute.before`, TAIL. Throws `EXECUTION_STALL: …` when the hard rung
|
|
238
|
+
* is active and the tool is one of the non-productive five.
|
|
239
|
+
*
|
|
240
|
+
* Placed above `if (!resolved) return;` in `tool-before.ts` for the same reason
|
|
241
|
+
* the PRM hard stop is: the architect IS the `resolved === null` case, so a
|
|
242
|
+
* denial below that early return would not exist for the only session this
|
|
243
|
+
* lever targets.
|
|
244
|
+
*/
|
|
245
|
+
export declare function enforceExecutionStallDenial(params: {
|
|
246
|
+
sessionID: string;
|
|
247
|
+
tool: string;
|
|
248
|
+
options?: ExecutionStallOptions;
|
|
249
|
+
}): void;
|
|
250
|
+
/**
|
|
251
|
+
* `tool.execute.after`. Records progress events, arms on a successful
|
|
252
|
+
* `update_task_status(in_progress)`, and runs the periodic workspace probe.
|
|
253
|
+
* NEVER throws.
|
|
254
|
+
*/
|
|
255
|
+
export declare function recordExecutionStallToolAfter(params: {
|
|
256
|
+
sessionID: string;
|
|
257
|
+
tool: string;
|
|
258
|
+
callID: string;
|
|
259
|
+
args?: Record<string, unknown>;
|
|
260
|
+
output: unknown;
|
|
261
|
+
directory: string;
|
|
262
|
+
options?: ExecutionStallOptions;
|
|
263
|
+
}): void;
|
|
264
|
+
/**
|
|
265
|
+
* Test/DI seam (AGENTS.md invariant 7). `now` is the fake-clock seam the
|
|
266
|
+
* idleness-lapse tests drive; the two workspace functions are indirected so a
|
|
267
|
+
* test never spawns git, and `readPlanOpenTaskState` so a test can drive the
|
|
268
|
+
* disarm edge without a plan fixture (the disarm tests exercise the REAL reader
|
|
269
|
+
* against a real `.swarm/plan.json` as well).
|
|
270
|
+
*/
|
|
271
|
+
export declare const _internals: {
|
|
272
|
+
now: () => number;
|
|
273
|
+
captureWorkspaceSnapshot: typeof captureWorkspaceSnapshot;
|
|
274
|
+
changedFilesSinceSnapshot: typeof changedFilesSinceSnapshot;
|
|
275
|
+
ensureArchitectSession: typeof ensureArchitectSession;
|
|
276
|
+
readPlanOpenTaskState: typeof readPlanOpenTaskState;
|
|
277
|
+
};
|
|
278
|
+
export declare const _test_exports: {
|
|
279
|
+
readonly WORKSPACE_PROBE_EVERY_CALLS: 10;
|
|
280
|
+
readonly stateCount: () => number;
|
|
281
|
+
readonly peekState: (sessionID: string) => Readonly<ExecutionStallState> | undefined;
|
|
282
|
+
readonly pendingDispatchRoleCount: () => number;
|
|
283
|
+
readonly reset: () => void;
|
|
284
|
+
};
|
|
285
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PLUGIN-INTERNALS READ GUARD (issue #2063, workstream B4)
|
|
3
|
+
*
|
|
4
|
+
* When a fail-closed gate denies a dispatch, the observed failure mode is an
|
|
5
|
+
* agent that goes looking for the answer inside the *plugin's own installed
|
|
6
|
+
* files* — `node_modules/opencode-swarm/**`, `~/.cache/opencode/packages/...`,
|
|
7
|
+
* `dist/index.js`. That search can never succeed: the installed bundle is not
|
|
8
|
+
* the state the error names, it is not editable from the user's workspace, and
|
|
9
|
+
* every minute spent in it is a minute the blocker is not surfaced to the user.
|
|
10
|
+
*
|
|
11
|
+
* This guard is the mechanical brake for that specific behaviour. It denies
|
|
12
|
+
* `read` / `glob` / `grep` / `bash` calls whose RESOLVED target lands inside the
|
|
13
|
+
* installed package directory.
|
|
14
|
+
*
|
|
15
|
+
* DESIGN CONSTRAINTS (all three are load-bearing):
|
|
16
|
+
*
|
|
17
|
+
* 1. **Explicitly best-effort.** `cd` chains, shell variables, wrapper CLIs and
|
|
18
|
+
* any other indirection evade it. The durable coverage for evasive
|
|
19
|
+
* spelunking is B5 (execution-stall) plus the A4 prompt rule; this guard
|
|
20
|
+
* exists to catch the overwhelmingly common direct form cheaply. It must
|
|
21
|
+
* therefore FAIL OPEN on every kind of resolution uncertainty — a false
|
|
22
|
+
* denial of a legitimate read is far worse than a missed evasion.
|
|
23
|
+
*
|
|
24
|
+
* 2. **Self-development exemption.** When the workspace root IS the
|
|
25
|
+
* opencode-swarm repository, the "installed package" and "the code the user
|
|
26
|
+
* is working on" are the same tree, and the guard would block the plugin's
|
|
27
|
+
* own maintainers from reading their own source. The exemption is keyed on
|
|
28
|
+
* `package.json#name === 'opencode-swarm'` at the workspace root.
|
|
29
|
+
*
|
|
30
|
+
* 3. **Runtime-derived package root.** The root is derived from this module's
|
|
31
|
+
* own location (`import.meta.url`), walking up to the nearest directory
|
|
32
|
+
* whose `package.json` is named `opencode-swarm`. It is NOT hardcoded and
|
|
33
|
+
* NOT derived from the workspace, so it is correct under every cache layout
|
|
34
|
+
* in AGENTS.md invariant 12 and under a plain `node_modules` install.
|
|
35
|
+
*
|
|
36
|
+
* The denial is thrown from the guardrails `tool.execute.before` chain, so it
|
|
37
|
+
* flows through the B1 gate-denial tracker automatically: an agent that keeps
|
|
38
|
+
* retrying the same spelunking read escalates to the STOP directive.
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* The exact denial text. Exported so tests pin the wording rather than a
|
|
42
|
+
* paraphrase, and so the leading `SWARM_INTERNALS_OFF_LIMITS:` token — which
|
|
43
|
+
* `deriveGateDenialCode` (B1) uses as the streak key — cannot drift.
|
|
44
|
+
*/
|
|
45
|
+
export declare const SWARM_INTERNALS_DENIAL_MESSAGE = "SWARM_INTERNALS_OFF_LIMITS: the swarm plugin's installed files are never the fix for a gate error. Fix the dispatch/state the error names, or report the blocker to the user.";
|
|
46
|
+
/**
|
|
47
|
+
* Locate the installed opencode-swarm package root from this module's own
|
|
48
|
+
* location. Computed once (including the `null` "could not determine" result)
|
|
49
|
+
* and cached for the process lifetime — the plugin's own location cannot change
|
|
50
|
+
* while it is loaded.
|
|
51
|
+
*/
|
|
52
|
+
declare function resolvePackageRoot(): string | null;
|
|
53
|
+
/**
|
|
54
|
+
* True when `directory` is an opencode-swarm checkout, i.e. the guard must be
|
|
55
|
+
* inert. Cached per workspace root with a hard size cap (invariant 8).
|
|
56
|
+
*/
|
|
57
|
+
declare function isSelfDevelopmentWorkspace(directory: string): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* True when `target` is `root` or lies beneath it. Pure string containment on
|
|
60
|
+
* already-resolved paths — no `realpath`, because a symlink probe is filesystem
|
|
61
|
+
* I/O on a hot hook path and this guard is explicitly best-effort.
|
|
62
|
+
*/
|
|
63
|
+
export declare function isInsidePackageRoot(root: string, target: string): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Extract the path candidates this guard is willing to reason about.
|
|
66
|
+
*
|
|
67
|
+
* Returns ONLY candidates that are unambiguously absolute (or `~`-anchored).
|
|
68
|
+
* A relative path is deliberately dropped: resolving it would require guessing
|
|
69
|
+
* a base directory, and a wrong guess produces a false denial.
|
|
70
|
+
*/
|
|
71
|
+
export declare function extractGuardedPathCandidates(normalizedTool: string, args: unknown): string[];
|
|
72
|
+
export interface InternalsGuardOptions {
|
|
73
|
+
/**
|
|
74
|
+
* `guardrails.enabled`. Mirrors `GateDenialOptions.enabled` (B1): when the
|
|
75
|
+
* user turns guardrails off, every guardrails behaviour must be inert or the
|
|
76
|
+
* config surface lies. Defaults to enabled.
|
|
77
|
+
*/
|
|
78
|
+
enabled?: boolean;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Deny a read/glob/grep/bash call that targets the installed plugin package.
|
|
82
|
+
*
|
|
83
|
+
* Throws `SWARM_INTERNALS_OFF_LIMITS: …` on a positive match; returns normally
|
|
84
|
+
* (fail open) in every other case, including:
|
|
85
|
+
* - guardrails disabled,
|
|
86
|
+
* - the tool is not one of the guarded five,
|
|
87
|
+
* - the workspace is an opencode-swarm checkout,
|
|
88
|
+
* - the package root could not be derived,
|
|
89
|
+
* - no unambiguously-absolute candidate could be extracted.
|
|
90
|
+
*/
|
|
91
|
+
export declare function enforceInternalsGuard(params: {
|
|
92
|
+
sessionID: string;
|
|
93
|
+
tool: string;
|
|
94
|
+
args: unknown;
|
|
95
|
+
directory: string;
|
|
96
|
+
options?: InternalsGuardOptions;
|
|
97
|
+
}): void;
|
|
98
|
+
/**
|
|
99
|
+
* Test/DI seam (AGENTS.md invariant 7). `moduleUrl`, `existsSync`,
|
|
100
|
+
* `readFileSync` and `homedir` are indirected so a test can stand up a FAKE
|
|
101
|
+
* installed-package tree without `mock.module` on `node:fs`; the two resolvers
|
|
102
|
+
* are indirected so a test can pin a package root without a real install.
|
|
103
|
+
*
|
|
104
|
+
* `resetCaches` exists because both resolutions are memoized for the process
|
|
105
|
+
* lifetime — without it a test that swaps `moduleUrl` would read a value
|
|
106
|
+
* computed by an earlier test.
|
|
107
|
+
*/
|
|
108
|
+
export declare const _internals: {
|
|
109
|
+
moduleUrl: () => string;
|
|
110
|
+
existsSync: (p: string) => boolean;
|
|
111
|
+
readFileSync: (p: string, enc: "utf-8") => string;
|
|
112
|
+
homedir: () => string;
|
|
113
|
+
resolvePackageRoot: typeof resolvePackageRoot;
|
|
114
|
+
isSelfDevelopmentWorkspace: typeof isSelfDevelopmentWorkspace;
|
|
115
|
+
resetCaches: () => void;
|
|
116
|
+
};
|
|
117
|
+
export {};
|
|
@@ -21,11 +21,29 @@ export interface MessagesTransformContext {
|
|
|
21
21
|
requireReviewerAndTestEngineer: boolean;
|
|
22
22
|
/** Shared consecutiveNoToolTurns Map (also used by toolBefore) */
|
|
23
23
|
consecutiveNoToolTurns: Map<string, number>;
|
|
24
|
+
/**
|
|
25
|
+
* Issue #2063 B3 — sessionID → id of the most recent assistant message that
|
|
26
|
+
* was counted by the MEDIUM band of the runaway detector.
|
|
27
|
+
*
|
|
28
|
+
* Keyed on the host message id and NEVER on an array index: compaction
|
|
29
|
+
* rewrites the window, so an index recorded on one turn points at a
|
|
30
|
+
* different message on the next. The marker exists so the counter can be
|
|
31
|
+
* reset when the USER speaks after the counted turn — a user reply means the
|
|
32
|
+
* "model is narrating instead of acting" hypothesis is no longer the right
|
|
33
|
+
* explanation for the silence.
|
|
34
|
+
*/
|
|
35
|
+
lastCountedAssistantMsgId: Map<string, string>;
|
|
24
36
|
}
|
|
25
37
|
type ChatMessageLike = {
|
|
38
|
+
/**
|
|
39
|
+
* `id` is a real host-message field (see `src/hooks/pr-workflow-auto-wake.ts`,
|
|
40
|
+
* which reads `info.id` off host session/message envelopes). It is optional
|
|
41
|
+
* here because synthetic messages this plugin itself unshifts carry no id.
|
|
42
|
+
*/
|
|
26
43
|
info?: {
|
|
27
44
|
role?: string;
|
|
28
45
|
sessionID?: string;
|
|
46
|
+
id?: string;
|
|
29
47
|
};
|
|
30
48
|
parts?: Array<{
|
|
31
49
|
type?: string;
|
|
@@ -39,6 +57,72 @@ type ChatMessageLike = {
|
|
|
39
57
|
* never contained, making the guard permanently inert.
|
|
40
58
|
*/
|
|
41
59
|
export declare const RUNAWAY_OUTPUT_ADVISORY_MARKER = "Model is generating analysis without taking action";
|
|
60
|
+
/**
|
|
61
|
+
* Lower bound of the runaway detector's MEDIUM band (issue #2063 B3), and the
|
|
62
|
+
* same threshold below which a tool-less assistant turn is treated as a short
|
|
63
|
+
* acknowledgement and RESETS the counter.
|
|
64
|
+
*
|
|
65
|
+
* One constant, two call sites, deliberately: the reset boundary and the
|
|
66
|
+
* counting boundary must be the same number or the band between them either
|
|
67
|
+
* double-counts or silently swallows turns. Exported so tests pin the real
|
|
68
|
+
* value instead of a hand-copied literal.
|
|
69
|
+
*
|
|
70
|
+
* The medium band only counts while an execution episode is armed
|
|
71
|
+
* ({@link isExecutionEpisodeArmed}). Ordinary conversation produces plenty of
|
|
72
|
+
* 200–4000 char replies with no tool calls, and treating those as a runaway is
|
|
73
|
+
* the false-positive class this gate exists to prevent. Above 4000 chars the
|
|
74
|
+
* behaviour is unchanged and NOT episode-gated.
|
|
75
|
+
*/
|
|
76
|
+
export declare const RUNAWAY_MEDIUM_MIN = 200;
|
|
77
|
+
/**
|
|
78
|
+
* Issue #2063 B3 — bound on {@link MessagesTransformContext.lastCountedAssistantMsgId}.
|
|
79
|
+
*
|
|
80
|
+
* AGENTS.md invariant 8: session-keyed state needs an explicit eviction
|
|
81
|
+
* strategy. Least-recently-written wins (delete-before-set), matching the no-op
|
|
82
|
+
* detector's LRU in `guardrails/index.ts` and for the same reason — plain
|
|
83
|
+
* insertion order would evict the long-lived architect session first.
|
|
84
|
+
*/
|
|
85
|
+
export declare const MAX_TRACKED_COUNTED_ASSISTANT_MSGS = 200;
|
|
86
|
+
declare function rememberCountedAssistantMsg(map: Map<string, string>, sessionId: string, messageId: string): void;
|
|
87
|
+
/**
|
|
88
|
+
* Prefix identifying a PRM course correction in the advisory queue
|
|
89
|
+
* (issue #2063 C1).
|
|
90
|
+
*
|
|
91
|
+
* Shared with the producer's test so the forward filter and the rendered
|
|
92
|
+
* dedupe key (`[prm:<pattern>:<level>]`, built in `src/prm/index.ts`) cannot
|
|
93
|
+
* drift apart. This file already carries the scar of exactly that drift:
|
|
94
|
+
* {@link RUNAWAY_OUTPUT_ADVISORY_MARKER} exists because a predicate once tested
|
|
95
|
+
* for a string no producer emitted, leaving the guard permanently inert. A
|
|
96
|
+
* hand-copied literal in a fixture would re-arm the same failure.
|
|
97
|
+
*/
|
|
98
|
+
export declare const PRM_ADVISORY_FORWARD_PREFIX = "[prm:";
|
|
99
|
+
/**
|
|
100
|
+
* Tier-0 test seam (zero mocks) for the pure LRU helper above. Production code
|
|
101
|
+
* calls the local binding; this exists so the eviction bound required by
|
|
102
|
+
* AGENTS.md invariant 8 can be asserted directly instead of by driving 200+
|
|
103
|
+
* sessions through the whole handler.
|
|
104
|
+
*/
|
|
105
|
+
export declare const _test_exports: {
|
|
106
|
+
rememberCountedAssistantMsg: typeof rememberCountedAssistantMsg;
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Drain-level byte budget for the architect [ADVISORIES] block (issue #1976).
|
|
110
|
+
*
|
|
111
|
+
* Advisories are prepended AFTER token accounting and therefore escape the
|
|
112
|
+
* context-budget handler; without a bound, a single turn with many producers
|
|
113
|
+
* (or a handful of large ones) can flood the architect prompt — the same
|
|
114
|
+
* failure mode that produced the PR_REVIEW banner flood (55.3% of non-blank
|
|
115
|
+
* lines in a real transcript).
|
|
116
|
+
*
|
|
117
|
+
* This is a defense-in-depth backstop behind the per-producer
|
|
118
|
+
* `pushAdvisory` helper (dedupe + length cap). When the joined block exceeds
|
|
119
|
+
* the budget, the OLDEST entries are dropped (keep-latest) because high-value
|
|
120
|
+
* advisories tend to arrive LATE in a turn — "keep earliest" is a priority
|
|
121
|
+
* inversion called out in the issue. Truncation is disclosed in the block
|
|
122
|
+
* header so the architect knows recent items were retained over earlier ones.
|
|
123
|
+
*/
|
|
124
|
+
export declare const MAX_ADVISORY_BLOCK_BYTES = 6000;
|
|
125
|
+
export declare const ADVISORY_TRUNCATION_NOTE = "[advisory block truncated to keep highest-value recent items]";
|
|
42
126
|
/**
|
|
43
127
|
* Bound a set of advisory strings to a total byte budget, keeping the latest
|
|
44
128
|
* (dropping oldest from the front). Returns the kept entries and whether any
|
|
@@ -64,6 +148,7 @@ export declare function createMessagesTransformHandler(ctx: MessagesTransformCon
|
|
|
64
148
|
role: string;
|
|
65
149
|
agent?: string;
|
|
66
150
|
sessionID?: string;
|
|
151
|
+
id?: string;
|
|
67
152
|
};
|
|
68
153
|
parts: Array<{
|
|
69
154
|
type: string;
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export { createDelegationTrackerHook } from './delegation-tracker';
|
|
|
10
10
|
export { extractCurrentPhase, extractCurrentPhaseFromPlan, extractCurrentTask, extractCurrentTaskFromPlan, extractDecisions, extractIncompleteTasks, extractIncompleteTasksFromPlan, extractPatterns, } from './extractors';
|
|
11
11
|
export { createFullAutoInterceptHook } from './full-auto-intercept';
|
|
12
12
|
export { checkFileAuthority, createGuardrailsHooks, DEFAULT_AGENT_AUTHORITY_RULES, } from './guardrails';
|
|
13
|
-
export { classifyMessage, classifyMessages, containsPlanContent, isDuplicateToolRead, isStaleError, isToolResult, MessagePriority, type MessagePriorityType, type MessageWithParts, } from './message-priority';
|
|
13
|
+
export { classifyMessage, classifyMessages, containsPlanContent, getCompletedToolOutputs, getToolNames, getToolParts, isDuplicateToolRead, isStaleError, isToolResult, MessagePriority, type MessagePriorityType, type MessageWithParts, } from './message-priority';
|
|
14
14
|
export { consolidateSystemMessages } from './messages-transform';
|
|
15
15
|
export { extractModelInfo, NATIVE_MODEL_LIMITS, PROVIDER_CAPS, resolveModelLimit, } from './model-limits';
|
|
16
16
|
export { type CuratorDelegateFactory, createPhaseMonitorHook, } from './phase-monitor';
|
|
@@ -36,13 +36,27 @@ interface MessageInfo {
|
|
|
36
36
|
sessionID?: string;
|
|
37
37
|
modelID?: string;
|
|
38
38
|
providerID?: string;
|
|
39
|
-
toolName?: string;
|
|
40
|
-
toolArgs?: unknown;
|
|
41
39
|
[key: string]: unknown;
|
|
42
40
|
}
|
|
43
41
|
interface MessagePart {
|
|
44
42
|
type?: string;
|
|
45
43
|
text?: string;
|
|
44
|
+
tool?: string;
|
|
45
|
+
state?: ToolStateLike;
|
|
46
|
+
[key: string]: unknown;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Minimal shape of a `ToolPart.state` value. The OpenCode SDK `ToolState` is a
|
|
50
|
+
* discriminated union on `status`; only `completed` carries `output` and only
|
|
51
|
+
* `error` carries `error`. We read defensively and never mutate `state` in
|
|
52
|
+
* place (see context-budget.ts mask/prune logic — those replace the whole
|
|
53
|
+
* ToolPart with a synthetic text part rather than corrupting the union).
|
|
54
|
+
*/
|
|
55
|
+
interface ToolStateLike {
|
|
56
|
+
status?: string;
|
|
57
|
+
output?: string;
|
|
58
|
+
error?: string;
|
|
59
|
+
input?: Record<string, unknown>;
|
|
46
60
|
[key: string]: unknown;
|
|
47
61
|
}
|
|
48
62
|
export interface MessageWithParts {
|
|
@@ -58,16 +72,60 @@ export interface MessageWithParts {
|
|
|
58
72
|
*/
|
|
59
73
|
export declare function containsPlanContent(text: string): boolean;
|
|
60
74
|
/**
|
|
61
|
-
*
|
|
75
|
+
* Returns all `ToolPart` objects in a message's `parts[]`.
|
|
76
|
+
*
|
|
77
|
+
* Per the OpenCode SDK contract (`@opencode-ai/sdk` v1+v2), tool results are
|
|
78
|
+
* delivered as `ToolPart` objects (`part.type === 'tool'`, with `part.tool`
|
|
79
|
+
* and `part.state`) inside a message's `parts[]` array — they are NOT separate
|
|
80
|
+
* `role:'tool'` messages and do not carry an `info.toolName` field. A message
|
|
81
|
+
* may contain multiple tool parts (parallel tool calls).
|
|
82
|
+
*
|
|
83
|
+
* @param message - The message to inspect
|
|
84
|
+
* @returns Array of tool parts (empty if none / malformed)
|
|
85
|
+
*/
|
|
86
|
+
export declare function getToolParts(message: MessageWithParts): MessagePart[];
|
|
87
|
+
/**
|
|
88
|
+
* Returns the completed tool outputs in a message — the countable, maskable
|
|
89
|
+
* payloads (`part.state.output` where `state.status === 'completed'`).
|
|
90
|
+
*
|
|
91
|
+
* Pending/running tools have no output; error tools expose `state.error`
|
|
92
|
+
* (diagnostic signal — never masked, only routed via the stale-error →
|
|
93
|
+
* DISPOSABLE pruning path). Completed outputs are the heavy payloads the
|
|
94
|
+
* context budget must count and may mask/prune.
|
|
95
|
+
*
|
|
96
|
+
* @param message - The message to inspect
|
|
97
|
+
* @returns Array of `{ part, output }` for each completed tool part
|
|
98
|
+
*/
|
|
99
|
+
export declare function getCompletedToolOutputs(message: MessageWithParts): Array<{
|
|
100
|
+
part: MessagePart;
|
|
101
|
+
output: string;
|
|
102
|
+
}>;
|
|
103
|
+
/**
|
|
104
|
+
* Returns the tool names (`part.tool`) for every tool part in a message.
|
|
105
|
+
*
|
|
106
|
+
* @param message - The message to inspect
|
|
107
|
+
* @returns Array of tool name strings (empty if none)
|
|
108
|
+
*/
|
|
109
|
+
export declare function getToolNames(message: MessageWithParts): string[];
|
|
110
|
+
/**
|
|
111
|
+
* Checks if a message carries at least one tool result (a `ToolPart` in its
|
|
112
|
+
* `parts[]`).
|
|
113
|
+
*
|
|
114
|
+
* This detects the real OpenCode SDK shape. The legacy `info.toolName` field
|
|
115
|
+
* never existed on production payloads and was removed (issue #2068).
|
|
62
116
|
*
|
|
63
117
|
* @param message - The message to check
|
|
64
|
-
* @returns true if the message
|
|
118
|
+
* @returns true if the message contains at least one tool part
|
|
65
119
|
*/
|
|
66
120
|
export declare function isToolResult(message: MessageWithParts): boolean;
|
|
67
121
|
/**
|
|
68
122
|
* Checks if two consecutive tool read calls are duplicates
|
|
69
123
|
* (same tool with same first argument).
|
|
70
124
|
*
|
|
125
|
+
* Compares the first tool part in each message (`part.tool` name and the first
|
|
126
|
+
* value of `part.state.input`). Two messages are duplicate reads when both
|
|
127
|
+
* call a read tool whose name contains "read" with the same first input value.
|
|
128
|
+
*
|
|
71
129
|
* @param current - The current message
|
|
72
130
|
* @param previous - The previous message
|
|
73
131
|
* @returns true if this is a duplicate tool read
|