@namzu/sdk 61.4.0 → 62.0.0

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.
Files changed (302) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/dist/agents/QueryAgent.d.ts.map +1 -1
  3. package/dist/agents/QueryAgent.js +2 -0
  4. package/dist/agents/QueryAgent.js.map +1 -1
  5. package/dist/bridge/acp/permission.d.ts +8 -0
  6. package/dist/bridge/acp/permission.d.ts.map +1 -1
  7. package/dist/bridge/acp/permission.js.map +1 -1
  8. package/dist/bridge/acp/server.d.ts.map +1 -1
  9. package/dist/bridge/acp/server.js +8 -1
  10. package/dist/bridge/acp/server.js.map +1 -1
  11. package/dist/constants/tools/index.d.ts +2 -0
  12. package/dist/constants/tools/index.d.ts.map +1 -1
  13. package/dist/constants/tools/index.js +23 -0
  14. package/dist/constants/tools/index.js.map +1 -1
  15. package/dist/eval/harness-protection.js +1 -1
  16. package/dist/eval/harness-protection.js.map +1 -1
  17. package/dist/learning/cycle.d.ts +5 -3
  18. package/dist/learning/cycle.d.ts.map +1 -1
  19. package/dist/learning/cycle.js +11 -13
  20. package/dist/learning/cycle.js.map +1 -1
  21. package/dist/learning/guidance.d.ts +74 -0
  22. package/dist/learning/guidance.d.ts.map +1 -0
  23. package/dist/learning/guidance.js +259 -0
  24. package/dist/learning/guidance.js.map +1 -0
  25. package/dist/learning/skill.d.ts +2 -2
  26. package/dist/learning/skill.d.ts.map +1 -1
  27. package/dist/learning/skill.js +6 -6
  28. package/dist/learning/skill.js.map +1 -1
  29. package/dist/learning/store.d.ts.map +1 -1
  30. package/dist/learning/store.js +6 -6
  31. package/dist/learning/store.js.map +1 -1
  32. package/dist/pals/communication/ingress-schema.d.ts +8 -8
  33. package/dist/pals/communication/schema.d.ts +2 -2
  34. package/dist/peers/client.d.ts +2 -3
  35. package/dist/peers/client.d.ts.map +1 -1
  36. package/dist/peers/client.js +2 -3
  37. package/dist/peers/client.js.map +1 -1
  38. package/dist/peers/endpoint.d.ts +1 -2
  39. package/dist/peers/endpoint.d.ts.map +1 -1
  40. package/dist/peers/endpoint.js +1 -2
  41. package/dist/peers/endpoint.js.map +1 -1
  42. package/dist/peers/protocol.d.ts.map +1 -1
  43. package/dist/peers/protocol.js +1 -0
  44. package/dist/peers/protocol.js.map +1 -1
  45. package/dist/prompt/coding-agent-doctrine.d.ts +4 -4
  46. package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
  47. package/dist/prompt/coding-agent-doctrine.js +5 -3
  48. package/dist/prompt/coding-agent-doctrine.js.map +1 -1
  49. package/dist/prompt/index.d.ts +0 -4
  50. package/dist/prompt/index.d.ts.map +1 -1
  51. package/dist/prompt/index.js +0 -2
  52. package/dist/prompt/index.js.map +1 -1
  53. package/dist/provider/fallback.d.ts.map +1 -1
  54. package/dist/provider/fallback.js +3 -0
  55. package/dist/provider/fallback.js.map +1 -1
  56. package/dist/provider/idle-timeout.d.ts.map +1 -1
  57. package/dist/provider/idle-timeout.js +5 -0
  58. package/dist/provider/idle-timeout.js.map +1 -1
  59. package/dist/provider/model-window-suffix.d.ts.map +1 -1
  60. package/dist/provider/model-window-suffix.js +1 -0
  61. package/dist/provider/model-window-suffix.js.map +1 -1
  62. package/dist/provider/retry.d.ts.map +1 -1
  63. package/dist/provider/retry.js +5 -0
  64. package/dist/provider/retry.js.map +1 -1
  65. package/dist/provider/step-stopped.d.ts +19 -0
  66. package/dist/provider/step-stopped.d.ts.map +1 -0
  67. package/dist/provider/step-stopped.js +25 -0
  68. package/dist/provider/step-stopped.js.map +1 -0
  69. package/dist/provider/token-budget.d.ts.map +1 -1
  70. package/dist/provider/token-budget.js +6 -1
  71. package/dist/provider/token-budget.js.map +1 -1
  72. package/dist/public-runtime.d.ts +3 -14
  73. package/dist/public-runtime.d.ts.map +1 -1
  74. package/dist/public-runtime.js +3 -14
  75. package/dist/public-runtime.js.map +1 -1
  76. package/dist/public-tools.d.ts +0 -2
  77. package/dist/public-tools.d.ts.map +1 -1
  78. package/dist/public-tools.js +0 -2
  79. package/dist/public-tools.js.map +1 -1
  80. package/dist/public-types.d.ts +3 -21
  81. package/dist/public-types.d.ts.map +1 -1
  82. package/dist/runtime/query/iteration/index.d.ts +17 -0
  83. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  84. package/dist/runtime/query/iteration/index.js +168 -5
  85. package/dist/runtime/query/iteration/index.js.map +1 -1
  86. package/dist/runtime/query/iteration/phases/per-call-review.d.ts.map +1 -1
  87. package/dist/runtime/query/iteration/phases/per-call-review.js +4 -5
  88. package/dist/runtime/query/iteration/phases/per-call-review.js.map +1 -1
  89. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  90. package/dist/runtime/query/iteration/phases/tool-review.js +6 -3
  91. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  92. package/dist/runtime/query/iteration/stream-turn.d.ts +9 -1
  93. package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
  94. package/dist/runtime/query/iteration/stream-turn.js +48 -2
  95. package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
  96. package/dist/runtime/query/resume-pending.d.ts.map +1 -1
  97. package/dist/runtime/query/resume-pending.js +7 -4
  98. package/dist/runtime/query/resume-pending.js.map +1 -1
  99. package/dist/runtime/query/review-policy.d.ts +40 -8
  100. package/dist/runtime/query/review-policy.d.ts.map +1 -1
  101. package/dist/runtime/query/review-policy.js +139 -24
  102. package/dist/runtime/query/review-policy.js.map +1 -1
  103. package/dist/runtime/query/turn/completion-audit.d.ts +32 -0
  104. package/dist/runtime/query/turn/completion-audit.d.ts.map +1 -0
  105. package/dist/runtime/query/turn/completion-audit.js +56 -0
  106. package/dist/runtime/query/turn/completion-audit.js.map +1 -0
  107. package/dist/runtime/query/turn/stall-monitor.d.ts +78 -0
  108. package/dist/runtime/query/turn/stall-monitor.d.ts.map +1 -0
  109. package/dist/runtime/query/turn/stall-monitor.js +101 -0
  110. package/dist/runtime/query/turn/stall-monitor.js.map +1 -0
  111. package/dist/session/paths.d.ts +1 -2
  112. package/dist/session/paths.d.ts.map +1 -1
  113. package/dist/session/paths.js +1 -4
  114. package/dist/session/paths.js.map +1 -1
  115. package/dist/tools/builtins/bash.d.ts +2 -1
  116. package/dist/tools/builtins/bash.d.ts.map +1 -1
  117. package/dist/tools/builtins/bash.js +5 -2
  118. package/dist/tools/builtins/bash.js.map +1 -1
  119. package/dist/tools/builtins/content-fingerprint.d.ts +13 -0
  120. package/dist/tools/builtins/content-fingerprint.d.ts.map +1 -1
  121. package/dist/tools/builtins/content-fingerprint.js +25 -0
  122. package/dist/tools/builtins/content-fingerprint.js.map +1 -1
  123. package/dist/tools/builtins/edit.d.ts.map +1 -1
  124. package/dist/tools/builtins/edit.js +7 -2
  125. package/dist/tools/builtins/edit.js.map +1 -1
  126. package/dist/tools/builtins/shell-read-credit.d.ts +15 -0
  127. package/dist/tools/builtins/shell-read-credit.d.ts.map +1 -0
  128. package/dist/tools/builtins/shell-read-credit.js +161 -0
  129. package/dist/tools/builtins/shell-read-credit.js.map +1 -0
  130. package/dist/tools/paths.d.ts +15 -2
  131. package/dist/tools/paths.d.ts.map +1 -1
  132. package/dist/tools/paths.js +17 -2
  133. package/dist/tools/paths.js.map +1 -1
  134. package/dist/turn/evidence-recall.d.ts +1 -1
  135. package/dist/turn/evidence-recall.d.ts.map +1 -1
  136. package/dist/turn/evidence-recall.js +5 -14
  137. package/dist/turn/evidence-recall.js.map +1 -1
  138. package/dist/types/acp/index.d.ts +6 -0
  139. package/dist/types/acp/index.d.ts.map +1 -1
  140. package/dist/types/agent/base.d.ts +4 -0
  141. package/dist/types/agent/base.d.ts.map +1 -1
  142. package/dist/types/hitl/index.d.ts +8 -0
  143. package/dist/types/hitl/index.d.ts.map +1 -1
  144. package/dist/types/hitl/index.js.map +1 -1
  145. package/dist/types/ids/index.d.ts +1 -1
  146. package/dist/types/message/index.d.ts +1 -1
  147. package/dist/types/message/index.d.ts.map +1 -1
  148. package/dist/types/message/index.js +1 -0
  149. package/dist/types/message/index.js.map +1 -1
  150. package/dist/types/provider/interface.d.ts +12 -0
  151. package/dist/types/provider/interface.d.ts.map +1 -1
  152. package/dist/types/session/config.d.ts +25 -0
  153. package/dist/types/session/config.d.ts.map +1 -1
  154. package/dist/types/session/records.d.ts.map +1 -1
  155. package/dist/types/session/records.js +2 -0
  156. package/dist/types/session/records.js.map +1 -1
  157. package/dist/types/session/turn.d.ts +1 -1
  158. package/dist/types/session/turn.d.ts.map +1 -1
  159. package/dist/types/session/turn.js.map +1 -1
  160. package/package.json +1 -1
  161. package/src/agents/QueryAgent.ts +2 -0
  162. package/src/bridge/acp/permission.ts +8 -0
  163. package/src/bridge/acp/server.ts +7 -1
  164. package/src/constants/tools/index.ts +27 -0
  165. package/src/eval/harness-protection.ts +1 -1
  166. package/src/learning/cycle.ts +15 -16
  167. package/src/learning/guidance.ts +339 -0
  168. package/src/learning/skill.ts +7 -7
  169. package/src/learning/store.ts +5 -6
  170. package/src/peers/client.ts +2 -3
  171. package/src/peers/endpoint.ts +1 -2
  172. package/src/peers/protocol.ts +1 -0
  173. package/src/prompt/coding-agent-doctrine.ts +5 -3
  174. package/src/prompt/index.ts +0 -4
  175. package/src/provider/fallback.ts +7 -0
  176. package/src/provider/idle-timeout.ts +8 -0
  177. package/src/provider/model-window-suffix.ts +1 -0
  178. package/src/provider/retry.ts +8 -0
  179. package/src/provider/step-stopped.ts +27 -0
  180. package/src/provider/token-budget.ts +6 -1
  181. package/src/public-runtime.ts +6 -28
  182. package/src/public-tools.ts +0 -3
  183. package/src/public-types.ts +5 -137
  184. package/src/runtime/query/iteration/index.ts +207 -5
  185. package/src/runtime/query/iteration/phases/per-call-review.ts +7 -5
  186. package/src/runtime/query/iteration/phases/tool-review.ts +5 -3
  187. package/src/runtime/query/iteration/stream-turn.ts +51 -1
  188. package/src/runtime/query/resume-pending.ts +11 -4
  189. package/src/runtime/query/review-policy.ts +157 -24
  190. package/src/runtime/query/turn/completion-audit.ts +71 -0
  191. package/src/runtime/query/turn/stall-monitor.ts +158 -0
  192. package/src/session/paths.ts +2 -6
  193. package/src/tools/builtins/bash.ts +6 -2
  194. package/src/tools/builtins/content-fingerprint.ts +25 -0
  195. package/src/tools/builtins/edit.ts +11 -2
  196. package/src/tools/builtins/shell-read-credit.ts +159 -0
  197. package/src/tools/paths.ts +18 -2
  198. package/src/turn/evidence-recall.ts +7 -15
  199. package/src/types/acp/index.ts +6 -0
  200. package/src/types/agent/base.ts +4 -0
  201. package/src/types/hitl/index.ts +8 -0
  202. package/src/types/ids/index.ts +1 -1
  203. package/src/types/message/index.ts +1 -0
  204. package/src/types/provider/interface.ts +13 -0
  205. package/src/types/session/config.ts +27 -0
  206. package/src/types/session/records.ts +2 -0
  207. package/src/types/session/turn.ts +2 -1
  208. package/dist/learning/deprecated.d.ts +0 -86
  209. package/dist/learning/deprecated.d.ts.map +0 -1
  210. package/dist/learning/deprecated.js +0 -22
  211. package/dist/learning/deprecated.js.map +0 -1
  212. package/dist/manager/resident/activity.d.ts +0 -74
  213. package/dist/manager/resident/activity.d.ts.map +0 -1
  214. package/dist/manager/resident/activity.js +0 -125
  215. package/dist/manager/resident/activity.js.map +0 -1
  216. package/dist/manager/resident/agenda.d.ts +0 -167
  217. package/dist/manager/resident/agenda.d.ts.map +0 -1
  218. package/dist/manager/resident/agenda.js +0 -588
  219. package/dist/manager/resident/agenda.js.map +0 -1
  220. package/dist/manager/resident/consumption.d.ts +0 -98
  221. package/dist/manager/resident/consumption.d.ts.map +0 -1
  222. package/dist/manager/resident/consumption.js +0 -237
  223. package/dist/manager/resident/consumption.js.map +0 -1
  224. package/dist/manager/resident/delivery-window.d.ts +0 -28
  225. package/dist/manager/resident/delivery-window.d.ts.map +0 -1
  226. package/dist/manager/resident/delivery-window.js +0 -80
  227. package/dist/manager/resident/delivery-window.js.map +0 -1
  228. package/dist/manager/resident/evidence-recall.d.ts +0 -30
  229. package/dist/manager/resident/evidence-recall.d.ts.map +0 -1
  230. package/dist/manager/resident/evidence-recall.js +0 -297
  231. package/dist/manager/resident/evidence-recall.js.map +0 -1
  232. package/dist/manager/resident/history-disk.d.ts +0 -10
  233. package/dist/manager/resident/history-disk.d.ts.map +0 -1
  234. package/dist/manager/resident/history-disk.js +0 -50
  235. package/dist/manager/resident/history-disk.js.map +0 -1
  236. package/dist/manager/resident/history.d.ts +0 -115
  237. package/dist/manager/resident/history.d.ts.map +0 -1
  238. package/dist/manager/resident/history.js +0 -203
  239. package/dist/manager/resident/history.js.map +0 -1
  240. package/dist/manager/resident/host.d.ts +0 -112
  241. package/dist/manager/resident/host.d.ts.map +0 -1
  242. package/dist/manager/resident/host.js +0 -254
  243. package/dist/manager/resident/host.js.map +0 -1
  244. package/dist/manager/resident/initiative.d.ts +0 -86
  245. package/dist/manager/resident/initiative.d.ts.map +0 -1
  246. package/dist/manager/resident/initiative.js +0 -131
  247. package/dist/manager/resident/initiative.js.map +0 -1
  248. package/dist/manager/resident/loop.d.ts +0 -49
  249. package/dist/manager/resident/loop.d.ts.map +0 -1
  250. package/dist/manager/resident/loop.js +0 -75
  251. package/dist/manager/resident/loop.js.map +0 -1
  252. package/dist/manager/resident/outbox.d.ts +0 -124
  253. package/dist/manager/resident/outbox.d.ts.map +0 -1
  254. package/dist/manager/resident/outbox.js +0 -216
  255. package/dist/manager/resident/outbox.js.map +0 -1
  256. package/dist/manager/resident/proposal.d.ts +0 -65
  257. package/dist/manager/resident/proposal.d.ts.map +0 -1
  258. package/dist/manager/resident/proposal.js +0 -77
  259. package/dist/manager/resident/proposal.js.map +0 -1
  260. package/dist/manager/resident/store.d.ts +0 -108
  261. package/dist/manager/resident/store.d.ts.map +0 -1
  262. package/dist/manager/resident/store.js +0 -205
  263. package/dist/manager/resident/store.js.map +0 -1
  264. package/dist/manager/resident/tool-evidence.d.ts +0 -112
  265. package/dist/manager/resident/tool-evidence.d.ts.map +0 -1
  266. package/dist/manager/resident/tool-evidence.js +0 -289
  267. package/dist/manager/resident/tool-evidence.js.map +0 -1
  268. package/dist/prompt/resident-learning.d.ts +0 -29
  269. package/dist/prompt/resident-learning.d.ts.map +0 -1
  270. package/dist/prompt/resident-learning.js +0 -126
  271. package/dist/prompt/resident-learning.js.map +0 -1
  272. package/dist/prompt/resident-step.d.ts +0 -42
  273. package/dist/prompt/resident-step.d.ts.map +0 -1
  274. package/dist/prompt/resident-step.js +0 -137
  275. package/dist/prompt/resident-step.js.map +0 -1
  276. package/dist/tools/resident-history.d.ts +0 -12
  277. package/dist/tools/resident-history.d.ts.map +0 -1
  278. package/dist/tools/resident-history.js +0 -82
  279. package/dist/tools/resident-history.js.map +0 -1
  280. package/dist/tools/resident-tool-evidence.d.ts +0 -9
  281. package/dist/tools/resident-tool-evidence.d.ts.map +0 -1
  282. package/dist/tools/resident-tool-evidence.js +0 -61
  283. package/dist/tools/resident-tool-evidence.js.map +0 -1
  284. package/src/learning/deprecated.ts +0 -124
  285. package/src/manager/resident/activity.ts +0 -211
  286. package/src/manager/resident/agenda.ts +0 -937
  287. package/src/manager/resident/consumption.ts +0 -335
  288. package/src/manager/resident/delivery-window.ts +0 -99
  289. package/src/manager/resident/evidence-recall.ts +0 -369
  290. package/src/manager/resident/history-disk.ts +0 -63
  291. package/src/manager/resident/history.ts +0 -348
  292. package/src/manager/resident/host.ts +0 -390
  293. package/src/manager/resident/initiative.ts +0 -231
  294. package/src/manager/resident/loop.ts +0 -119
  295. package/src/manager/resident/outbox.ts +0 -341
  296. package/src/manager/resident/proposal.ts +0 -119
  297. package/src/manager/resident/store.ts +0 -279
  298. package/src/manager/resident/tool-evidence.ts +0 -455
  299. package/src/prompt/resident-learning.ts +0 -152
  300. package/src/prompt/resident-step.ts +0 -182
  301. package/src/tools/resident-history.ts +0 -88
  302. package/src/tools/resident-tool-evidence.ts +0 -72
@@ -0,0 +1,339 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { readdir } from 'node:fs/promises'
3
+ import { join } from 'node:path'
4
+ import { isDeepStrictEqual } from 'node:util'
5
+ import { z } from 'zod'
6
+ import { DiskRecordStore } from '../store/kv/record-store.js'
7
+ import { DiskRevisionRecordStore, revisionFileSegment } from '../store/kv/revision-record-store.js'
8
+ import { defineSchema } from '../store/schema.js'
9
+ import type { TenantId } from '../types/ids/index.js'
10
+ import { asTenantId } from '../utils/id.js'
11
+ import {
12
+ type LearningEvidence,
13
+ type LearningProfileUpdate,
14
+ type LearningSkillCandidate,
15
+ type LearningSkillEvaluation,
16
+ type LearningState,
17
+ freezeLearning,
18
+ learningEvidenceSchema,
19
+ learningSchema,
20
+ promoteLearningSkill,
21
+ restoreLearningSkill,
22
+ reviseLearningProfile,
23
+ } from './skill.js'
24
+
25
+ /**
26
+ * @experimental One immutable revision of the guidance a learning target has
27
+ * earned. `learning` is absent until the first profile change or activation.
28
+ */
29
+ export interface LearningGuidanceState {
30
+ readonly tenantId: string
31
+ readonly agentKey: string
32
+ readonly revision: number
33
+ readonly learning?: LearningState
34
+ }
35
+
36
+ /** @experimental A competing writer advanced the guidance revision; read it again before retrying. */
37
+ export class LearningGuidanceConflictError extends Error {
38
+ constructor() {
39
+ super('Learning guidance changed; read the current guidance before retrying.')
40
+ this.name = 'LearningGuidanceConflictError'
41
+ }
42
+ }
43
+
44
+ const guidanceSchema = z.object({
45
+ tenantId: z.string().uuid(),
46
+ agentKey: z.string().min(1).max(200),
47
+ revision: z.number().int().positive().safe(),
48
+ learning: learningSchema.optional(),
49
+ })
50
+
51
+ const guidanceRecordSchema = defineSchema({
52
+ kind: 'learning-guidance',
53
+ current: 1,
54
+ migrations: {},
55
+ })
56
+
57
+ /**
58
+ * The record format learning guidance was kept in before it had a store of its
59
+ * own. Its `kind` is the identifier stamped into those files, so it keeps its
60
+ * spelling; only the fields learning used are read from it.
61
+ */
62
+ const earlierRecordSchema = defineSchema({
63
+ kind: 'resident-agenda',
64
+ current: 6,
65
+ migrations: {
66
+ 1: (record) => record,
67
+ 2: (record) => record,
68
+ 3: (record) => record,
69
+ 4: (record) => record,
70
+ 5: (record) => record,
71
+ },
72
+ })
73
+
74
+ const earlierFieldsSchema = z.object({
75
+ tenantId: z.string().uuid(),
76
+ agentKey: z.string().min(1).max(200),
77
+ revision: z.number().int().positive().safe(),
78
+ learning: learningSchema.optional(),
79
+ })
80
+
81
+ /** Marker written beside the revisions once an earlier store has been carried over. */
82
+ const ADOPTED_MARKER = 'adopted.json'
83
+ const REVISION_FILE = /^([1-9][0-9]{0,15})\.json$/
84
+
85
+ /** A key as one bounded path segment, the way the earlier store addressed it. */
86
+ function keySegment(key: string): string {
87
+ const encoded = revisionFileSegment(key)
88
+ return encoded.length <= 255
89
+ ? encoded
90
+ : `~sha256-${createHash('sha256').update(encoded).digest('hex')}`
91
+ }
92
+
93
+ /**
94
+ * Where guidance was kept before it had a store of its own, for one scope:
95
+ * `<root>/<tenant>/<key>/agenda`. Pass the result as `adoptFrom` to carry it over.
96
+ */
97
+ export function earlierGuidanceDirectory(
98
+ root: string,
99
+ scope: { readonly tenantId: string; readonly agentKey: string },
100
+ ): string {
101
+ return join(root, asTenantId(scope.tenantId), keySegment(scope.agentKey), 'agenda')
102
+ }
103
+
104
+ /** @experimental Where a guidance store lives and what it may carry over on first use. */
105
+ export interface DiskLearningGuidanceOptions {
106
+ /**
107
+ * A directory holding guidance in the earlier record format
108
+ * (see {@link earlierGuidanceDirectory}). On the first use of this store,
109
+ * every revision found there is copied into it with its revision number, and
110
+ * a marker is written beside them. The earlier files are only read, never
111
+ * changed or removed, so an older version keeps working on them.
112
+ */
113
+ readonly adoptFrom?: string
114
+ }
115
+
116
+ /**
117
+ * @experimental Revisioned, evaluated guidance for one tenant and one scope key,
118
+ * on a trusted local filesystem. Every change is an immutable revision published
119
+ * exclusively, so two processes cannot both commit the same one, and an older
120
+ * revision stays readable to roll one skill back.
121
+ */
122
+ export class DiskLearningGuidance {
123
+ private readonly records = new DiskRevisionRecordStore<LearningGuidanceState>(
124
+ guidanceRecordSchema,
125
+ 'learning guidance',
126
+ (record) => record.revision,
127
+ )
128
+ private readonly historyRecords = new DiskRecordStore<LearningGuidanceState>(guidanceRecordSchema)
129
+ private readonly location
130
+ private readonly tenantId: TenantId
131
+ private readonly agentKey: string
132
+ private readonly directory: string
133
+ private readonly adoptFrom: string | undefined
134
+ private adopting: Promise<void> | undefined
135
+
136
+ constructor(
137
+ directory: string,
138
+ scope: { tenantId: TenantId | string; agentKey: string },
139
+ options: DiskLearningGuidanceOptions = {},
140
+ ) {
141
+ this.tenantId = asTenantId(scope.tenantId)
142
+ this.agentKey = z.string().min(1).max(200).parse(scope.agentKey)
143
+ this.directory = directory
144
+ this.adoptFrom = options.adoptFrom
145
+ this.location = {
146
+ legacyPath: join(directory, 'state.json'),
147
+ revisionsDir: join(directory, 'revisions'),
148
+ publishLegacyProjection: false,
149
+ }
150
+ }
151
+
152
+ private checked(record: LearningGuidanceState): LearningGuidanceState {
153
+ const state = guidanceSchema.parse(record)
154
+ if (state.tenantId !== this.tenantId || state.agentKey !== this.agentKey)
155
+ throw new Error('Guidance does not match the bound tenant and scope key.')
156
+ return Object.freeze({
157
+ ...state,
158
+ ...(state.learning ? { learning: freezeLearning(state.learning) } : {}),
159
+ })
160
+ }
161
+
162
+ /** Carry an earlier store over once per process, and once ever per directory. */
163
+ private ready(): Promise<void> {
164
+ if (!this.adoptFrom) return Promise.resolve()
165
+ this.adopting ??= this.adopt(this.adoptFrom).catch((error) => {
166
+ this.adopting = undefined
167
+ throw error
168
+ })
169
+ return this.adopting
170
+ }
171
+
172
+ private async adopt(from: string): Promise<void> {
173
+ const marker = new DiskRecordStore<unknown>(guidanceRecordSchema)
174
+ if ((await marker.read(join(this.directory, ADOPTED_MARKER))) !== null) return
175
+ const earlier = new DiskRecordStore<unknown>(earlierRecordSchema)
176
+ let names: string[]
177
+ try {
178
+ names = await readdir(join(from, 'revisions'))
179
+ } catch (error) {
180
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return
181
+ throw error
182
+ }
183
+ const revisions = names
184
+ .flatMap((name) => {
185
+ const match = REVISION_FILE.exec(name)
186
+ return match ? [Number(match[1])] : []
187
+ })
188
+ .sort((a, b) => a - b)
189
+ if (!revisions.length) return
190
+ const head = revisions[revisions.length - 1] as number
191
+ if (revisions.length !== head)
192
+ throw new Error(
193
+ `Earlier guidance in ${from} has gaps between revisions 1 and ${head}; it was not carried over.`,
194
+ )
195
+ for (const revision of revisions) {
196
+ const current = await this.records.read(this.location)
197
+ if ((current?.revision ?? 0) >= revision) continue
198
+ const raw = await earlier.read(join(from, 'revisions', `${revision}.json`))
199
+ const fields = earlierFieldsSchema.parse(raw)
200
+ if (
201
+ fields.revision !== revision ||
202
+ fields.tenantId !== this.tenantId ||
203
+ fields.agentKey !== this.agentKey
204
+ )
205
+ throw new Error(
206
+ `Earlier guidance revision ${revision} in ${from} does not match this tenant and scope key; it was not carried over.`,
207
+ )
208
+ const record = this.checked({
209
+ tenantId: fields.tenantId,
210
+ agentKey: fields.agentKey,
211
+ revision,
212
+ ...(fields.learning ? { learning: fields.learning } : {}),
213
+ })
214
+ try {
215
+ await this.records.transact(this.location, (latest) => {
216
+ if ((latest?.revision ?? 0) !== revision - 1) throw new LearningGuidanceConflictError()
217
+ return { record, result: undefined }
218
+ })
219
+ } catch (error) {
220
+ // A concurrent process carrying over the same revision wins identically.
221
+ const after = await this.records.read(this.location)
222
+ if ((after?.revision ?? 0) < revision) throw error
223
+ }
224
+ }
225
+ await marker.write(join(this.directory, ADOPTED_MARKER), {
226
+ from,
227
+ revisions: head,
228
+ adoptedAt: new Date().toISOString(),
229
+ })
230
+ }
231
+
232
+ async read(): Promise<LearningGuidanceState | null> {
233
+ await this.ready()
234
+ const record = await this.records.read(this.location)
235
+ return record === null ? null : this.checked(record)
236
+ }
237
+
238
+ /** One immutable revision, as it was committed. */
239
+ async readRevision(revision: number): Promise<LearningGuidanceState | null> {
240
+ z.number().int().positive().safe().parse(revision)
241
+ await this.ready()
242
+ const record = await this.historyRecords.read(
243
+ join(this.location.revisionsDir, `${revision}.json`),
244
+ )
245
+ if (record === null) return null
246
+ const state = this.checked(record)
247
+ if (state.revision !== revision)
248
+ throw new Error('Guidance revision filename and body disagree.')
249
+ return state
250
+ }
251
+
252
+ /** Create the empty first revision. A second creator gets a conflict. */
253
+ async create(): Promise<LearningGuidanceState> {
254
+ await this.ready()
255
+ const record = this.checked({
256
+ tenantId: this.tenantId,
257
+ agentKey: this.agentKey,
258
+ revision: 1,
259
+ })
260
+ return this.records.transact(this.location, (current) => {
261
+ if (current !== null) throw new LearningGuidanceConflictError()
262
+ return { record, result: record }
263
+ })
264
+ }
265
+
266
+ /** The current guidance, created empty when none exists yet. Safe to race. */
267
+ async open(): Promise<LearningGuidanceState> {
268
+ const existing = await this.read()
269
+ if (existing) return existing
270
+ try {
271
+ return await this.create()
272
+ } catch (error) {
273
+ const raced = await this.read()
274
+ if (!raced) throw error
275
+ return raced
276
+ }
277
+ }
278
+
279
+ private async learn(
280
+ expected: LearningGuidanceState,
281
+ learning: LearningState,
282
+ ): Promise<LearningGuidanceState> {
283
+ await this.ready()
284
+ const snapshot = this.checked(expected)
285
+ return this.records.transact(this.location, (record) => {
286
+ if (record === null) throw new LearningGuidanceConflictError()
287
+ const current = this.checked(record)
288
+ if (current.revision !== snapshot.revision) throw new LearningGuidanceConflictError()
289
+ if (!isDeepStrictEqual(current.learning, snapshot.learning))
290
+ throw new LearningGuidanceConflictError()
291
+ const next = this.checked({ ...current, learning, revision: current.revision + 1 })
292
+ return { record: next, result: next }
293
+ })
294
+ }
295
+
296
+ /** Append a host-evidenced correction to identity or preferences. */
297
+ async updateProfile(
298
+ expected: LearningGuidanceState,
299
+ input: LearningProfileUpdate,
300
+ ): Promise<LearningGuidanceState> {
301
+ const snapshot = this.checked(expected)
302
+ return this.learn(snapshot, reviseLearningProfile(snapshot.learning, input))
303
+ }
304
+
305
+ /** Bind evaluated guidance to the current learning version. */
306
+ async promoteSkill(
307
+ expected: LearningGuidanceState,
308
+ candidate: LearningSkillCandidate,
309
+ evaluation: LearningSkillEvaluation,
310
+ evidence: LearningEvidence,
311
+ ): Promise<LearningGuidanceState> {
312
+ const snapshot = this.checked(expected)
313
+ return this.learn(
314
+ snapshot,
315
+ promoteLearningSkill(snapshot.learning, candidate, evaluation, evidence),
316
+ )
317
+ }
318
+
319
+ /** Restore one skill from an immutable earlier revision while keeping the current profile. */
320
+ async rollbackSkill(
321
+ expected: LearningGuidanceState,
322
+ name: string,
323
+ fromRevision: number,
324
+ evidence: LearningEvidence,
325
+ ): Promise<LearningGuidanceState> {
326
+ const snapshot = this.checked(expected)
327
+ const checkedEvidence = learningEvidenceSchema.parse(evidence)
328
+ z.number().int().positive().safe().max(snapshot.revision).parse(fromRevision)
329
+ z.string()
330
+ .regex(/^[a-z0-9][a-z0-9_-]{0,63}$/)
331
+ .parse(name)
332
+ const historical = await this.readRevision(fromRevision)
333
+ if (!historical) throw new Error('Skill rollback revision is missing.')
334
+ return this.learn(
335
+ snapshot,
336
+ restoreLearningSkill(snapshot.learning, historical.learning, name, checkedEvidence),
337
+ )
338
+ }
339
+ }
@@ -119,11 +119,11 @@ export const learningSchema = z
119
119
  )
120
120
  context.addIssue({
121
121
  code: z.ZodIssueCode.custom,
122
- message: 'Invalid resident learning identity or content digest.',
122
+ message: 'Invalid learning identity or content digest.',
123
123
  })
124
124
  })
125
125
 
126
- /** @experimental Versioned behavioral overlay; the original resident mandate remains unchanged. */
126
+ /** @experimental Versioned behavioral overlay; the original identity remains unchanged. */
127
127
  export interface LearningState {
128
128
  readonly revision: number
129
129
  readonly identity?: { readonly text: string; readonly evidence: LearningEvidence }
@@ -219,7 +219,7 @@ function base(current: LearningState | undefined, evidence: LearningEvidence): L
219
219
  }
220
220
  }
221
221
 
222
- /** Internal pure profile transition; storage owns exact agenda CAS and idle admission. */
222
+ /** Internal pure profile transition; storage owns the exact-revision check. */
223
223
  export function reviseLearningProfile(
224
224
  current: LearningState | undefined,
225
225
  input: LearningProfileUpdate,
@@ -306,7 +306,7 @@ export function promoteLearningSkill(
306
306
  batch.candidate.length > 128 ||
307
307
  batch.attributions.length > 128
308
308
  )
309
- throw new Error('Resident skill evaluation is bounded to 64 tasks per round.')
309
+ throw new Error('Skill evaluation is bounded to 64 tasks per round.')
310
310
  // Recorded evidence must consistently identify success and complete measurements.
311
311
  for (const trial of [...batch.baseline, ...batch.candidate]) {
312
312
  if (
@@ -322,7 +322,7 @@ export function promoteLearningSkill(
322
322
  protection,
323
323
  )
324
324
  if (review.decision !== 'accept')
325
- throw new Error(`Resident skill promotion ${review.decision}: ${review.reason}`)
325
+ throw new Error(`Skill promotion ${review.decision}: ${review.reason}`)
326
326
  const skill: LearnedSkill = {
327
327
  ...candidate,
328
328
  hash,
@@ -358,7 +358,7 @@ export function restoreLearningSkill(
358
358
  ): LearningState {
359
359
  name.parse(skillName)
360
360
  if (!current?.skills.some((s) => s.name === skillName))
361
- throw new Error('No active resident skill to roll back.')
361
+ throw new Error('No active skill to roll back.')
362
362
  const next = base(current, evidenceFor(current, inputEvidence))
363
363
  const previous = historical
364
364
  ? freezeLearning(historical).skills.find((s) => s.name === skillName)
@@ -398,7 +398,7 @@ export function projectLearning(
398
398
  options: LearningProjectionOptions,
399
399
  ): LearningProjection {
400
400
  if (!Number.isSafeInteger(options.maxChars) || options.maxChars < 0 || options.maxChars > 64_000)
401
- throw new TypeError('Resident learning context requires maxChars between 0 and 64000.')
401
+ throw new TypeError('Learning context requires maxChars between 0 and 64000.')
402
402
  const selected = z.array(name).max(16).parse(options.skillNames)
403
403
  const purpose = z.enum(['task', 'exploration']).parse(options.purpose ?? 'task')
404
404
  if (new Set(selected).size !== selected.length) throw new Error('Duplicate selected skill names.')
@@ -286,7 +286,7 @@ export class SqliteLearningStore {
286
286
  row.project_id !== this.scope.projectId ||
287
287
  row.agent_key !== this.scope.agentKey
288
288
  )
289
- throw new Error('Learning cycle does not belong to this tenant, project and resident.')
289
+ throw new Error('Learning cycle does not belong to this tenant, project and scope key.')
290
290
  return row
291
291
  }
292
292
 
@@ -524,7 +524,7 @@ export class SqliteLearningStore {
524
524
  (event.data.tenantId !== this.scope.tenantId ||
525
525
  event.data.agentKey !== this.scope.agentKey)
526
526
  )
527
- throw new Error('Learning start does not match the bound resident.')
527
+ throw new Error('Learning start does not match the bound scope key.')
528
528
  const parent =
529
529
  event.data.parentCycleId === undefined ? null : uuid.parse(event.data.parentCycleId)
530
530
  if (parent && !this.owned(db, parent))
@@ -843,11 +843,10 @@ export async function runStoredLearningFromObservations(
843
843
  options: LearningDiscoveryOptions,
844
844
  ): Promise<LearningDiscoveryResult> {
845
845
  options.signal.throwIfAborted()
846
- const agenda = await options.agenda.read()
847
- if (!agenda || agenda.paused || agenda.pursuits.some((p) => p.state.phase === 'running'))
848
- throw new Error('Learning discovery requires an unpaused agenda without running pursuits.')
846
+ const guidance = await options.guidance.read()
847
+ if (!guidance) throw new Error('Learning discovery requires existing learning guidance.')
849
848
  const targets = options.evaluators.map((target) => {
850
- const skill = agenda.learning?.skills.find((s) => s.name === target.skillName)
849
+ const skill = guidance.learning?.skills.find((s) => s.name === target.skillName)
851
850
  return { ...target, baselineRevision: skill ? hashLearningSkill(skill) : 'none' }
852
851
  })
853
852
  const observation = await store.selectObservation(targets)
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * A client for one connection of `namzu-peer/1`: connect, write one request
3
3
  * line, read one response line, close. Mirrors the shape of this
4
- * repository's other local control clients (`queryRunner` in
5
- * `packages/cli/src/integrations/resident/runner-control.ts`,
6
- * `callEndpoint` in `packages/cli/src/schedule/daemon/endpoint.ts`): an
4
+ * repository's other local control clients (`callEndpoint` in
5
+ * `packages/cli/src/schedule/daemon/endpoint.ts`): an
7
6
  * authenticated response PROVES responsiveness, and its absence never proves
8
7
  * the peer is dead — only that this attempt could not reach it.
9
8
  *
@@ -3,8 +3,7 @@
3
3
  * session, newline-delimited JSON framing, a closed op set (design §1.1,
4
4
  * §1.3). Mirrors the shape of this repository's other local control
5
5
  * endpoints (`startEndpoint` in
6
- * `packages/cli/src/schedule/daemon/endpoint.ts`, `createRunnerControlServer`
7
- * in `packages/cli/src/integrations/resident/runner-control.ts`): a total
6
+ * `packages/cli/src/schedule/daemon/endpoint.ts`): a total
8
7
  * read deadline per connection (not an inactivity timer a slow sender can
9
8
  * reset), a bounded connection count, and a destroyed socket rather than an
10
9
  * error response for anything malformed — request bytes and tokens are
@@ -36,6 +36,7 @@ export const DEFAULT_PEER_READ_DEADLINE_MS = 2_000
36
36
  /** Concurrent connections a `createPeerEndpoint` server accepts before refusing new ones. */
37
37
  export const MAX_PEER_CONNECTIONS = 16
38
38
 
39
+ // 'resident' is no longer announced; it stays so an older peer's record still parses.
39
40
  export const PeerSessionKindSchema = z.enum(['tui', 'exec', 'resident', 'scheduled'])
40
41
  export type PeerSessionKind = z.infer<typeof PeerSessionKindSchema>
41
42
 
@@ -21,7 +21,7 @@
21
21
  * `CODING_AGENT_WORKING_DOCTRINE` names only the builtins and is meant for the
22
22
  * parent and for every delegated sub-agent, because a delegated task edits the
23
23
  * same repository under the same rules. `CODING_AGENT_DELEGATION_DOCTRINE`
24
- * names `task_create` and `Agent`, which a sub-agent does not have, and so is
24
+ * names `Agent` and speaks of the task list, which a sub-agent does not have, and so is
25
25
  * for the parent only. An adversarial review caught a draft ordering children
26
26
  * to plan with a tool they could not call; a rule about a tool the reader does
27
27
  * not have is not guidance, it is an instruction to fail.
@@ -38,6 +38,7 @@ export const CODING_AGENT_WORKING_DOCTRINE = `## How you work
38
38
 
39
39
  ### Delivering work
40
40
  - Act when you have enough information. Take established facts and the user's decisions as settled.
41
+ - Begin editing once you know the first change, and refine as you go. If you stop before changing anything, the next step starts from the same code.
41
42
  - The requested scope is the deliverable: deliver it at the size and on the subject it was asked. Read ambiguity the way a careful colleague would; make the routine judgment calls yourself and check in only when different readings would produce materially different work.
42
43
  - Finish the whole task, not just the easy parts. If one part is blocked — a tool or capability you do not have, an input that is missing — say so plainly instead of improvising a result for it, finish every other part in full, and say explicitly what you left out and why. Scaling the work down is the user's call.
43
44
  - If you find a real problem with the task as stated, say so in a sentence or two, then keep going under a stated assumption. Reserve blocking questions for cases where proceeding under any assumption would be unsafe or would make the work useless if wrong.
@@ -59,6 +60,7 @@ export const CODING_AGENT_WORKING_DOCTRINE = `## How you work
59
60
  - Before editing, use the relevant file content already available from a successful read or write; if it is unavailable or may be stale, read the needed portion. Read additional surrounding code when needed to match its naming and idiom. Respect project instructions and tool-specific prerequisites even when they require a fresh read.
60
61
  - Prefer the dedicated tools over shell equivalents (\`read\` over \`cat\`, \`grep\` over a shell grep, \`glob\` over \`find\`, \`edit\` over \`sed -i\`, \`write\` over a shell redirect), because their structured inputs and focused results are easier to check. Use \`bash\` when the task needs a shell; permissions apply the same way.
61
62
  - Independent tool calls go in one response: reading three files, or running a grep and a glob, does not need three turns. Wait for a mutation to finish before an observation or another action that depends on it; runtime ordering and concurrency limits remain authoritative.
63
+ - For a defect, reproduce it first with a small script or a failing test, and keep it. Make the change, then rerun that reproduction. Afterwards run the project's tests and its lint or format check, add tests for behaviour you introduced, and say where any throwaway file you made is. Say in your report what you ran and what you did not run.
62
64
  - Match verification to the task and its risk. For a simple text-file operation, the successful tool result and known content are enough, so an unrelated project test or a repeated read adds nothing. For code changes, run the relevant tests, typecheck or lint that would catch a mistake, and follow required project checks. Claim only the checks you ran.
63
65
  - Describe the state you leave the working tree in: say where scratch files are, and which files outside the request you touched.
64
66
 
@@ -74,11 +76,11 @@ export const CODING_AGENT_WORKING_DOCTRINE = `## How you work
74
76
  - Keep the final reply short and specific: what changed, what was verified, what is left.`
75
77
 
76
78
  /**
77
- * Parent-only: rules about `task_create` / `task_update` and `Agent`, which a
79
+ * Parent-only: rules about the task list and `Agent`, which a
78
80
  * delegated sub-agent does not have. See the module comment for the split.
79
81
  */
80
82
  export const CODING_AGENT_DELEGATION_DOCTRINE = `### Planning and delegating
81
- - For work of three or more steps — several files, several distinct stages, anything you would write a checklist for — open a task list with \`task_create\` and keep it current with \`task_update\`, marking each step done when it is done rather than at the end. Skip it for a single inspect-edit-test cycle or a two-step job: the list is for the user to follow, and a list of one or two items tells them nothing.
83
+ - A task list is optional. For work of three or more distinct steps — several files, several separate stages — you may keep one: exactly one step marked in progress at a time, each marked done when it is done rather than at the end, and the list cleared away once everything is done. Do not make one for a single inspect-edit-test cycle or a two-step job; a list of one or two items tells the user nothing. Keeping a list is not progress on the task itself.
82
84
  - When the \`Agent\` tool is available, delegate independent work through it and give each delegation a short, specific description; the user watches those descriptions, not the prompts. Brief each sub-agent with four things: the objective; what is out of bounds; which tools and sources to use; and the shape of the reply you want, such as a short summary with a file path for anything long. A fresh sub-agent starts with none of your context and returns what you ask for.
83
85
  - When the \`Agent\` tool lists a \`fork\` type, use \`subagent_type: "fork"\` when a side task would fill your context with output you will not need again, such as an open-ended survey. A fork starts from this conversation under your own instructions, so its prompt is only the directive: what to do and where its scope ends, without the background. Never write or guess what a fork will find before its report arrives. A fork cannot start forks of its own.
84
86
  - When delegated agents finish, tell the user what each one found in your next message. Their results reach you and not the user, so a result you do not pass on stays unseen.
@@ -23,7 +23,3 @@ export {
23
23
  codingAgentDoctrineContribution,
24
24
  } from './coding-agent-doctrine.js'
25
25
  export type { CodingAgentDoctrineOptions } from './coding-agent-doctrine.js'
26
- export { createResidentStepContributions } from './resident-step.js'
27
- export type { ResidentStepPromptOptions } from './resident-step.js'
28
- export { createResidentStepContext } from './resident-learning.js'
29
- export type { ResidentStepContextOptions, ResidentStepContextBundle } from './resident-learning.js'
@@ -554,6 +554,13 @@ export function withProviderFallback(
554
554
  const serving = members[cursor] ?? first
555
555
  return serving.provider.supportsImageInputFor?.(serving.model ?? model) ?? true
556
556
  },
557
+ // A chain takes a forced choice only if every member does: the request may
558
+ // be served by any of them, and the one that refuses would fail the step.
559
+ acceptsForcedToolChoiceFor: (model: string, thinking?: ThinkingConfig) =>
560
+ members.every(
561
+ (member) =>
562
+ member.provider.acceptsForcedToolChoiceFor?.(member.model ?? model, thinking) ?? true,
563
+ ),
557
564
  reasoningEffortLevelsFor: (model: string, thinking?: ThinkingConfig) =>
558
565
  commonReasoningEffortLevels(members, model, thinking),
559
566
  // Present on the chain wrapper for the same reason the menu method is:
@@ -223,6 +223,14 @@ export function withStreamIdleTimeout(
223
223
  supportsImageInputFor: (model: string) => provider.supportsImageInputFor?.(model) ?? true,
224
224
  }
225
225
  : {}),
226
+ ...(provider.acceptsForcedToolChoiceFor
227
+ ? {
228
+ acceptsForcedToolChoiceFor: (
229
+ model: string,
230
+ thinking?: Parameters<NonNullable<LLMProvider['acceptsForcedToolChoiceFor']>>[1],
231
+ ) => provider.acceptsForcedToolChoiceFor?.(model, thinking) ?? true,
232
+ }
233
+ : {}),
226
234
  ...(provider.reasoningEffortLevelsFor
227
235
  ? {
228
236
  reasoningEffortLevelsFor: (
@@ -18,6 +18,7 @@ const MODEL_FIRST_METHODS: ReadonlySet<string> = new Set([
18
18
  'supportsImageInputFor',
19
19
  'reasoningEffortLevelsFor',
20
20
  'reasoningEffortDefaultFor',
21
+ 'acceptsForcedToolChoiceFor',
21
22
  'effortLevelsFor',
22
23
  'resolveContextWindow',
23
24
  'refusalFallbackFor',
@@ -384,6 +384,14 @@ export function withProviderRetry(
384
384
  supportsImageInputFor: (model: string) => provider.supportsImageInputFor?.(model) ?? true,
385
385
  }
386
386
  : {}),
387
+ ...(provider.acceptsForcedToolChoiceFor
388
+ ? {
389
+ acceptsForcedToolChoiceFor: (
390
+ model: string,
391
+ thinking?: Parameters<NonNullable<LLMProvider['acceptsForcedToolChoiceFor']>>[1],
392
+ ) => provider.acceptsForcedToolChoiceFor?.(model, thinking) ?? true,
393
+ }
394
+ : {}),
387
395
  ...(provider.reasoningEffortLevelsFor
388
396
  ? {
389
397
  reasoningEffortLevelsFor: (
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The reason a step is stopped on purpose, by the loop that is reading it.
3
+ *
4
+ * A step that has spent its whole reasoning allowance is cut short: the loop
5
+ * stops reading the stream and aborts the request it opened. That abort is the
6
+ * loop's own decision, not a caller's stop and not a transport failure, so it
7
+ * carries this class as its reason. A provider wrapper that sees an aborted
8
+ * request can tell the two apart with {@link isStepStopped} and must not retry
9
+ * it, fail over from it, or report it as an error.
10
+ */
11
+ export class StepStopped extends Error {
12
+ override readonly name = 'StepStopped'
13
+
14
+ constructor() {
15
+ super('the step was stopped by the loop that was reading it')
16
+ }
17
+ }
18
+
19
+ /** Whether an abort reason, or an error that carries one, is a deliberate step stop. */
20
+ export function isStepStopped(value: unknown): value is StepStopped {
21
+ return value instanceof StepStopped
22
+ }
23
+
24
+ /** Whether a request signal was aborted by a deliberate step stop. */
25
+ export function stepWasStopped(signal: AbortSignal | undefined): boolean {
26
+ return signal?.aborted === true && isStepStopped(signal.reason)
27
+ }
@@ -6,6 +6,7 @@ import { ProviderError, classifyProviderError } from '../types/provider/errors.j
6
6
  import type { LLMProvider, StreamChunk } from '../types/provider/index.js'
7
7
  import { cancelCauseOf } from '../types/session/cancel-cause.js'
8
8
  import { isProviderRequestError } from './errors.js'
9
+ import { isStepStopped } from './step-stopped.js'
9
10
 
10
11
  export class TokenBudgetAdmissionError extends ProviderError {
11
12
  constructor() {
@@ -117,6 +118,7 @@ export function withTokenBudget(provider: LLMProvider, budget: SessionTokenBudge
117
118
  }
118
119
  if (Object.keys(chunk.delta).length > 0) outputObserved = true
119
120
  if (typeof chunk.delta.content === 'string') streamedChars += chunk.delta.content.length
121
+ streamedChars += chunk.delta.reasoning?.text?.length ?? 0
120
122
  if (chunk.error) throw new Error(chunk.error)
121
123
  yield chunk
122
124
  }
@@ -161,10 +163,13 @@ export function withTokenBudget(provider: LLMProvider, budget: SessionTokenBudge
161
163
  // because of work somebody chose to stop. The estimate can under-count
162
164
  // what the provider billed for the part it never reported; a failure
163
165
  // that was not a cancellation keeps its marker as before.
166
+ //
167
+ // A step the loop stopped on purpose (it reached its reasoning cap) is
168
+ // closed the same way: nobody chose to lose track of that spend.
164
169
  if (
165
170
  usage === undefined &&
166
171
  signal?.aborted === true &&
167
- cancelCauseOf(signal.reason) === 'user' &&
172
+ (cancelCauseOf(signal.reason) === 'user' || isStepStopped(signal.reason)) &&
168
173
  !uncertainRetry
169
174
  ) {
170
175
  const prompt = estimateMessagesTokens(params.messages)