@namzu/sdk 62.12.0 → 62.14.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 (248) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/agents/file-definitions.d.ts +7 -0
  3. package/dist/agents/file-definitions.d.ts.map +1 -1
  4. package/dist/agents/file-definitions.js +20 -0
  5. package/dist/agents/file-definitions.js.map +1 -1
  6. package/dist/agents/index.d.ts +1 -1
  7. package/dist/agents/index.d.ts.map +1 -1
  8. package/dist/agents/index.js +1 -1
  9. package/dist/agents/index.js.map +1 -1
  10. package/dist/bridge/acp/server.d.ts +8 -1
  11. package/dist/bridge/acp/server.d.ts.map +1 -1
  12. package/dist/bridge/acp/server.js +12 -0
  13. package/dist/bridge/acp/server.js.map +1 -1
  14. package/dist/connector/index.d.ts +1 -1
  15. package/dist/connector/index.d.ts.map +1 -1
  16. package/dist/connector/index.js +1 -1
  17. package/dist/connector/index.js.map +1 -1
  18. package/dist/connector/mcp/client.d.ts +96 -0
  19. package/dist/connector/mcp/client.d.ts.map +1 -1
  20. package/dist/connector/mcp/client.js +488 -13
  21. package/dist/connector/mcp/client.js.map +1 -1
  22. package/dist/connector/mcp/index.d.ts +1 -0
  23. package/dist/connector/mcp/index.d.ts.map +1 -1
  24. package/dist/connector/mcp/index.js +1 -0
  25. package/dist/connector/mcp/index.js.map +1 -1
  26. package/dist/connector/mcp/tasks.d.ts +40 -0
  27. package/dist/connector/mcp/tasks.d.ts.map +1 -0
  28. package/dist/connector/mcp/tasks.js +55 -0
  29. package/dist/connector/mcp/tasks.js.map +1 -0
  30. package/dist/constants/data-epoch.d.ts +18 -0
  31. package/dist/constants/data-epoch.d.ts.map +1 -0
  32. package/dist/constants/data-epoch.js +18 -0
  33. package/dist/constants/data-epoch.js.map +1 -0
  34. package/dist/constants/index.d.ts +1 -0
  35. package/dist/constants/index.d.ts.map +1 -1
  36. package/dist/constants/index.js +1 -0
  37. package/dist/constants/index.js.map +1 -1
  38. package/dist/constants/mcp/index.d.ts +1 -1
  39. package/dist/constants/mcp/index.d.ts.map +1 -1
  40. package/dist/constants/mcp/index.js +5 -0
  41. package/dist/constants/mcp/index.js.map +1 -1
  42. package/dist/constants/telemetry/index.d.ts +12 -0
  43. package/dist/constants/telemetry/index.d.ts.map +1 -1
  44. package/dist/constants/telemetry/index.js +12 -0
  45. package/dist/constants/telemetry/index.js.map +1 -1
  46. package/dist/eval/bootstrap.d.ts +122 -0
  47. package/dist/eval/bootstrap.d.ts.map +1 -0
  48. package/dist/eval/bootstrap.js +227 -0
  49. package/dist/eval/bootstrap.js.map +1 -0
  50. package/dist/eval/compare.d.ts +109 -0
  51. package/dist/eval/compare.d.ts.map +1 -0
  52. package/dist/eval/compare.js +324 -0
  53. package/dist/eval/compare.js.map +1 -0
  54. package/dist/eval/disclosure.d.ts +70 -0
  55. package/dist/eval/disclosure.d.ts.map +1 -0
  56. package/dist/eval/disclosure.js +64 -0
  57. package/dist/eval/disclosure.js.map +1 -0
  58. package/dist/eval/experiment.d.ts +23 -0
  59. package/dist/eval/experiment.d.ts.map +1 -1
  60. package/dist/eval/experiment.js +87 -15
  61. package/dist/eval/experiment.js.map +1 -1
  62. package/dist/eval/from-turn.d.ts.map +1 -1
  63. package/dist/eval/from-turn.js +5 -0
  64. package/dist/eval/from-turn.js.map +1 -1
  65. package/dist/eval/index.d.ts +13 -2
  66. package/dist/eval/index.d.ts.map +1 -1
  67. package/dist/eval/index.js +6 -1
  68. package/dist/eval/index.js.map +1 -1
  69. package/dist/eval/report-stats.d.ts +77 -0
  70. package/dist/eval/report-stats.d.ts.map +1 -0
  71. package/dist/eval/report-stats.js +184 -0
  72. package/dist/eval/report-stats.js.map +1 -0
  73. package/dist/eval/run-options.d.ts +30 -0
  74. package/dist/eval/run-options.d.ts.map +1 -0
  75. package/dist/eval/run-options.js +20 -0
  76. package/dist/eval/run-options.js.map +1 -0
  77. package/dist/eval/run-summary.d.ts +36 -0
  78. package/dist/eval/run-summary.d.ts.map +1 -0
  79. package/dist/eval/run-summary.js +57 -0
  80. package/dist/eval/run-summary.js.map +1 -0
  81. package/dist/eval/scorers.d.ts +32 -0
  82. package/dist/eval/scorers.d.ts.map +1 -1
  83. package/dist/eval/scorers.js +77 -0
  84. package/dist/eval/scorers.js.map +1 -1
  85. package/dist/eval/types.d.ts +74 -0
  86. package/dist/eval/types.d.ts.map +1 -1
  87. package/dist/manager/session/turn-recorder.d.ts +3 -0
  88. package/dist/manager/session/turn-recorder.d.ts.map +1 -1
  89. package/dist/manager/session/turn-recorder.js +18 -2
  90. package/dist/manager/session/turn-recorder.js.map +1 -1
  91. package/dist/plugin/index.d.ts +1 -1
  92. package/dist/plugin/index.d.ts.map +1 -1
  93. package/dist/plugin/index.js +1 -1
  94. package/dist/plugin/index.js.map +1 -1
  95. package/dist/plugin/loader.d.ts +11 -0
  96. package/dist/plugin/loader.d.ts.map +1 -1
  97. package/dist/plugin/loader.js +15 -2
  98. package/dist/plugin/loader.js.map +1 -1
  99. package/dist/public-runtime.d.ts +10 -6
  100. package/dist/public-runtime.d.ts.map +1 -1
  101. package/dist/public-runtime.js +13 -5
  102. package/dist/public-runtime.js.map +1 -1
  103. package/dist/public-types.d.ts +2 -0
  104. package/dist/public-types.d.ts.map +1 -1
  105. package/dist/registry/command/kernel-commands.d.ts +3 -0
  106. package/dist/registry/command/kernel-commands.d.ts.map +1 -1
  107. package/dist/registry/command/kernel-commands.js +76 -5
  108. package/dist/registry/command/kernel-commands.js.map +1 -1
  109. package/dist/runtime/query/cancelled-before-start.d.ts.map +1 -1
  110. package/dist/runtime/query/cancelled-before-start.js +2 -0
  111. package/dist/runtime/query/cancelled-before-start.js.map +1 -1
  112. package/dist/runtime/query/index.d.ts.map +1 -1
  113. package/dist/runtime/query/index.js +17 -0
  114. package/dist/runtime/query/index.js.map +1 -1
  115. package/dist/runtime/query/iteration/index.d.ts +18 -1
  116. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  117. package/dist/runtime/query/iteration/index.js +80 -1
  118. package/dist/runtime/query/iteration/index.js.map +1 -1
  119. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  120. package/dist/runtime/query/iteration/phases/tool-review.js +18 -1
  121. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  122. package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
  123. package/dist/runtime/query/iteration/stream-turn.js +5 -0
  124. package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
  125. package/dist/runtime/query/result.d.ts.map +1 -1
  126. package/dist/runtime/query/result.js +2 -0
  127. package/dist/runtime/query/result.js.map +1 -1
  128. package/dist/runtime/query/turn/answer-claims.d.ts +52 -0
  129. package/dist/runtime/query/turn/answer-claims.d.ts.map +1 -0
  130. package/dist/runtime/query/turn/answer-claims.js +348 -0
  131. package/dist/runtime/query/turn/answer-claims.js.map +1 -0
  132. package/dist/runtime/query/turn/outcome-ledger.d.ts +7 -0
  133. package/dist/runtime/query/turn/outcome-ledger.d.ts.map +1 -1
  134. package/dist/runtime/query/turn/outcome-ledger.js +13 -0
  135. package/dist/runtime/query/turn/outcome-ledger.js.map +1 -1
  136. package/dist/skills/index.d.ts +1 -1
  137. package/dist/skills/index.d.ts.map +1 -1
  138. package/dist/skills/index.js +1 -1
  139. package/dist/skills/index.js.map +1 -1
  140. package/dist/skills/loader.d.ts +15 -1
  141. package/dist/skills/loader.d.ts.map +1 -1
  142. package/dist/skills/loader.js +17 -3
  143. package/dist/skills/loader.js.map +1 -1
  144. package/dist/store/goal/index.d.ts +3 -1
  145. package/dist/store/goal/index.d.ts.map +1 -1
  146. package/dist/store/goal/index.js +6 -0
  147. package/dist/store/goal/index.js.map +1 -1
  148. package/dist/telemetry/genai.d.ts +98 -0
  149. package/dist/telemetry/genai.d.ts.map +1 -0
  150. package/dist/telemetry/genai.js +189 -0
  151. package/dist/telemetry/genai.js.map +1 -0
  152. package/dist/telemetry/runtime-accessors.d.ts +19 -0
  153. package/dist/telemetry/runtime-accessors.d.ts.map +1 -1
  154. package/dist/telemetry/runtime-accessors.js +43 -1
  155. package/dist/telemetry/runtime-accessors.js.map +1 -1
  156. package/dist/tools/goal/confirmation.d.ts +154 -0
  157. package/dist/tools/goal/confirmation.d.ts.map +1 -0
  158. package/dist/tools/goal/confirmation.js +290 -0
  159. package/dist/tools/goal/confirmation.js.map +1 -0
  160. package/dist/tools/goal/index.d.ts +4 -1
  161. package/dist/tools/goal/index.d.ts.map +1 -1
  162. package/dist/tools/goal/index.js +27 -2
  163. package/dist/tools/goal/index.js.map +1 -1
  164. package/dist/tools/hand-back/index.d.ts +48 -0
  165. package/dist/tools/hand-back/index.d.ts.map +1 -0
  166. package/dist/tools/hand-back/index.js +129 -0
  167. package/dist/tools/hand-back/index.js.map +1 -0
  168. package/dist/tools/schedules/schedule-tool.d.ts.map +1 -1
  169. package/dist/tools/schedules/schedule-tool.js +15 -1
  170. package/dist/tools/schedules/schedule-tool.js.map +1 -1
  171. package/dist/tools/schedules/types.d.ts +10 -0
  172. package/dist/tools/schedules/types.d.ts.map +1 -1
  173. package/dist/toolsets/manager.d.ts.map +1 -1
  174. package/dist/toolsets/manager.js +12 -0
  175. package/dist/toolsets/manager.js.map +1 -1
  176. package/dist/types/acp/index.d.ts +18 -0
  177. package/dist/types/acp/index.d.ts.map +1 -1
  178. package/dist/types/connector/mcp.d.ts +18 -0
  179. package/dist/types/connector/mcp.d.ts.map +1 -1
  180. package/dist/types/goal/index.d.ts +12 -0
  181. package/dist/types/goal/index.d.ts.map +1 -1
  182. package/dist/types/session/config.d.ts +8 -0
  183. package/dist/types/session/config.d.ts.map +1 -1
  184. package/dist/types/session/hand-back.d.ts +29 -0
  185. package/dist/types/session/hand-back.d.ts.map +1 -0
  186. package/dist/types/session/hand-back.js +11 -0
  187. package/dist/types/session/hand-back.js.map +1 -0
  188. package/dist/types/session/outcome.d.ts +37 -0
  189. package/dist/types/session/outcome.d.ts.map +1 -1
  190. package/dist/types/session/outcome.js.map +1 -1
  191. package/dist/types/session/turn.d.ts +19 -1
  192. package/dist/types/session/turn.d.ts.map +1 -1
  193. package/dist/types/session/turn.js.map +1 -1
  194. package/package.json +1 -1
  195. package/src/agents/file-definitions.ts +21 -0
  196. package/src/agents/index.ts +1 -0
  197. package/src/bridge/acp/server.ts +20 -0
  198. package/src/connector/index.ts +1 -0
  199. package/src/connector/mcp/client.ts +531 -14
  200. package/src/connector/mcp/index.ts +1 -0
  201. package/src/connector/mcp/tasks.ts +81 -0
  202. package/src/constants/data-epoch.ts +17 -0
  203. package/src/constants/index.ts +1 -0
  204. package/src/constants/mcp/index.ts +6 -1
  205. package/src/constants/telemetry/index.ts +13 -0
  206. package/src/eval/bootstrap.ts +314 -0
  207. package/src/eval/compare.ts +473 -0
  208. package/src/eval/disclosure.ts +136 -0
  209. package/src/eval/experiment.ts +127 -15
  210. package/src/eval/from-turn.ts +5 -0
  211. package/src/eval/index.ts +51 -0
  212. package/src/eval/report-stats.ts +257 -0
  213. package/src/eval/run-options.ts +49 -0
  214. package/src/eval/run-summary.ts +104 -0
  215. package/src/eval/scorers.ts +83 -0
  216. package/src/eval/types.ts +76 -0
  217. package/src/manager/session/turn-recorder.ts +23 -2
  218. package/src/plugin/index.ts +7 -1
  219. package/src/plugin/loader.ts +16 -2
  220. package/src/public-runtime.ts +70 -0
  221. package/src/public-types.ts +9 -0
  222. package/src/registry/command/kernel-commands.ts +94 -6
  223. package/src/runtime/query/cancelled-before-start.ts +2 -0
  224. package/src/runtime/query/index.ts +20 -0
  225. package/src/runtime/query/iteration/index.ts +85 -1
  226. package/src/runtime/query/iteration/phases/tool-review.ts +24 -1
  227. package/src/runtime/query/iteration/stream-turn.ts +10 -0
  228. package/src/runtime/query/result.ts +2 -0
  229. package/src/runtime/query/turn/answer-claims.ts +413 -0
  230. package/src/runtime/query/turn/outcome-ledger.ts +12 -0
  231. package/src/skills/index.ts +1 -0
  232. package/src/skills/loader.ts +21 -3
  233. package/src/store/goal/index.ts +11 -0
  234. package/src/telemetry/genai.ts +202 -0
  235. package/src/telemetry/runtime-accessors.ts +40 -1
  236. package/src/tools/goal/confirmation.ts +375 -0
  237. package/src/tools/goal/index.ts +37 -1
  238. package/src/tools/hand-back/index.ts +150 -0
  239. package/src/tools/schedules/schedule-tool.ts +19 -1
  240. package/src/tools/schedules/types.ts +10 -0
  241. package/src/toolsets/manager.ts +22 -0
  242. package/src/types/acp/index.ts +20 -0
  243. package/src/types/connector/mcp.ts +18 -0
  244. package/src/types/goal/index.ts +13 -0
  245. package/src/types/session/config.ts +8 -0
  246. package/src/types/session/hand-back.ts +31 -0
  247. package/src/types/session/outcome.ts +42 -0
  248. package/src/types/session/turn.ts +19 -1
@@ -8,7 +8,7 @@ import {
8
8
  StaleGoalError,
9
9
  } from '../../store/goal/index.js'
10
10
  import type { HostCommandDescriptor } from '../../types/command/index.js'
11
- import type { GoalRef, SessionGoal } from '../../types/goal/index.js'
11
+ import type { GoalConfirmedBy, GoalRef, SessionGoal } from '../../types/goal/index.js'
12
12
  import type { SessionId, TenantId } from '../../types/ids/index.js'
13
13
  import type { Skill } from '../../types/skills/index.js'
14
14
  import { isInvocableBy, skillInvocation } from '../../types/skills/index.js'
@@ -48,9 +48,12 @@ export interface GoalCommandScope {
48
48
  readonly tenantId: TenantId
49
49
  /** Process-local permission for automatic continuation. */
50
50
  readonly activation?: SessionGoalActivation
51
+ /** What a goal that does not choose is confirmed by; shown by `/goal status`. Default `model`. */
52
+ readonly defaultConfirmedBy?: GoalConfirmedBy
51
53
  }
52
54
 
53
- const GOAL_ARGUMENTS = '[status|set <objective>|<objective>|edit <objective>|pause|resume|clear]'
55
+ const GOAL_ARGUMENTS =
56
+ '[status|set [--confirmed-by record|model] <objective>|<objective>|edit <objective>|pause|resume|clear]'
54
57
  const GOAL_USAGE = `Usage: /goal ${GOAL_ARGUMENTS}`
55
58
 
56
59
  type ParsedGoalCommand =
@@ -78,6 +81,60 @@ function parseGoalCommand(args: readonly string[]): ParsedGoalCommand {
78
81
  return { kind: 'create', objective: input }
79
82
  }
80
83
 
84
+ type ConfirmedByFlag =
85
+ | {
86
+ readonly kind: 'ok'
87
+ readonly args: readonly string[]
88
+ readonly confirmedBy?: GoalConfirmedBy
89
+ }
90
+ | { readonly kind: 'invalid' }
91
+
92
+ /**
93
+ * Takes `--confirmed-by <model|record>` (or `=`) out of the arguments, but only
94
+ * where a flag stands: at the front, right after `set`, or closing the command. In the middle the words
95
+ * belong to the objective, so a goal about the flag itself keeps its text.
96
+ */
97
+ function takeConfirmedBy(args: readonly string[]): ConfirmedByFlag {
98
+ const head: string[] = []
99
+ let i = 0
100
+ if ((args[0] ?? '').toLowerCase() === 'set') {
101
+ head.push(args[0] ?? '')
102
+ i = 1
103
+ }
104
+ let found: GoalConfirmedBy | undefined
105
+ for (; i < args.length; i++) {
106
+ const arg = args[i] ?? ''
107
+ let value: string
108
+ if (arg === '--confirmed-by') {
109
+ value = args[i + 1] ?? ''
110
+ i++
111
+ } else if (arg.startsWith('--confirmed-by=')) {
112
+ value = arg.slice('--confirmed-by='.length)
113
+ } else {
114
+ break
115
+ }
116
+ if (value !== 'model' && value !== 'record') return { kind: 'invalid' }
117
+ found = value
118
+ }
119
+ let tail = args.slice(i)
120
+ // A flag may also close the command; in the middle its words are the objective's.
121
+ const last = tail[tail.length - 1] ?? ''
122
+ const beforeLast = tail[tail.length - 2] ?? ''
123
+ let closing: string | undefined
124
+ if (last.startsWith('--confirmed-by=')) {
125
+ closing = last.slice('--confirmed-by='.length)
126
+ tail = tail.slice(0, -1)
127
+ } else if (beforeLast === '--confirmed-by') {
128
+ closing = last
129
+ tail = tail.slice(0, -2)
130
+ }
131
+ if (closing !== undefined) {
132
+ if (closing !== 'model' && closing !== 'record') return { kind: 'invalid' }
133
+ found = closing
134
+ }
135
+ return { kind: 'ok', args: [...head, ...tail], ...(found ? { confirmedBy: found } : {}) }
136
+ }
137
+
81
138
  function goalRef(goal: SessionGoal): GoalRef {
82
139
  return { id: goal.id, revision: goal.revision }
83
140
  }
@@ -92,10 +149,19 @@ function goalCommands(goal: SessionGoal, armed: boolean): string {
92
149
  return '/goal set <objective>, /goal clear'
93
150
  }
94
151
 
95
- function renderGoal(title: string, goal: SessionGoal, armed: boolean): string {
152
+ function renderGoal(
153
+ title: string,
154
+ goal: SessionGoal,
155
+ armed: boolean,
156
+ defaultConfirmedBy: GoalConfirmedBy = 'model',
157
+ ): string {
96
158
  return [
97
159
  title,
98
160
  `Status: ${goal.phase}`,
161
+ ...(goal.confirmedBy === 'record' ||
162
+ (goal.confirmedBy === undefined && defaultConfirmedBy === 'record')
163
+ ? ['Confirmed by: the record of the work']
164
+ : []),
99
165
  `Automatic continuation: ${armed ? 'enabled' : 'paused'}`,
100
166
  `Automatic turns: ${goal.roundsAdmitted} / ${goal.maxGoalRounds}`,
101
167
  ...(goal.blockedReason
@@ -108,7 +174,12 @@ function renderGoal(title: string, goal: SessionGoal, armed: boolean): string {
108
174
  }
109
175
 
110
176
  function renderScopedGoal(title: string, goal: SessionGoal, scope: GoalCommandScope): string {
111
- return renderGoal(title, goal, scope.activation?.isArmed(scope.sessionId, goal) ?? false)
177
+ return renderGoal(
178
+ title,
179
+ goal,
180
+ scope.activation?.isArmed(scope.sessionId, goal) ?? false,
181
+ scope.defaultConfirmedBy,
182
+ )
112
183
  }
113
184
 
114
185
  function missingGoal(action: string) {
@@ -125,7 +196,20 @@ async function runGoalCommand(scope: GoalCommandScope | undefined, args: readonl
125
196
  reason: 'Goals need a durable session. This host did not supply one for the current command.',
126
197
  }
127
198
  }
128
- const command = parseGoalCommand(args)
199
+ const flag = takeConfirmedBy(args)
200
+ if (flag.kind === 'invalid') {
201
+ return {
202
+ kind: 'refused' as const,
203
+ reason: `--confirmed-by takes record or model.\n${GOAL_USAGE}`,
204
+ }
205
+ }
206
+ const command = parseGoalCommand(flag.args)
207
+ if (flag.confirmedBy !== undefined && command.kind !== 'create') {
208
+ return {
209
+ kind: 'refused' as const,
210
+ reason: `--confirmed-by is chosen when a goal is set.\n${GOAL_USAGE}`,
211
+ }
212
+ }
129
213
  try {
130
214
  const current = await scope.store.getGoal(scope.sessionId, scope.tenantId)
131
215
  switch (command.kind) {
@@ -154,7 +238,11 @@ async function runGoalCommand(scope: GoalCommandScope | undefined, args: readonl
154
238
  }
155
239
  }
156
240
  const created = await scope.store.createGoal(
157
- { sessionId: scope.sessionId, objective: command.objective },
241
+ {
242
+ sessionId: scope.sessionId,
243
+ objective: command.objective,
244
+ ...(flag.confirmedBy !== undefined ? { confirmedBy: flag.confirmedBy } : {}),
245
+ },
158
246
  scope.tenantId,
159
247
  )
160
248
  scope.activation?.arm(created)
@@ -1,5 +1,6 @@
1
1
  import type { TurnRecorder } from '../../manager/session/turn-recorder.js'
2
2
  import { GENAI, NAMZU, agentTurnSpanName, parentContext } from '../../telemetry/attributes.js'
3
+ import { agentRunAttributes } from '../../telemetry/genai.js'
3
4
  import { getTracer } from '../../telemetry/runtime-accessors.js'
4
5
  import type { MessageId } from '../../types/ids/index.js'
5
6
  import { type Message, createSystemMessage } from '../../types/message/index.js'
@@ -82,6 +83,7 @@ export async function* settlePreStartCancellation(
82
83
  [GENAI.AGENT_ID]: params.agentId,
83
84
  [GENAI.REQUEST_MODEL]: turnConfig.model,
84
85
  [GENAI.SYSTEM]: params.provider.id,
86
+ ...agentRunAttributes(params.provider.id),
85
87
  })
86
88
 
87
89
  const push = (message: Message, ids: ReadonlyMap<Message, MessageId>): void => {
@@ -36,10 +36,16 @@ import {
36
36
  parentContext,
37
37
  serializeSpan,
38
38
  } from '../../telemetry/attributes.js'
39
+ import { agentRunAttributes } from '../../telemetry/genai.js'
39
40
  import { getTracer } from '../../telemetry/runtime-accessors.js'
40
41
  import { buildAdvisoryTools } from '../../tools/advisory/index.js'
41
42
  import { SearchToolsTool } from '../../tools/builtins/search-tools.js'
42
43
  import { createStructuredOutputTool } from '../../tools/builtins/structuredOutput.js'
44
+ import {
45
+ HAND_BACK_GUIDANCE,
46
+ HAND_BACK_GUIDANCE_CONTRIBUTION_ID,
47
+ buildHandBackTool,
48
+ } from '../../tools/hand-back/index.js'
43
49
  import { buildTaskTools } from '../../tools/task/index.js'
44
50
  import { ToolManager } from '../../toolsets/manager.js'
45
51
  import { toolset } from '../../toolsets/toolset.js'
@@ -1283,6 +1289,10 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
1283
1289
  for (const tool of taskTools) addRuntimeTool(tool, 'deferred')
1284
1290
  }
1285
1291
 
1292
+ // The way out of a task that cannot be done as specified. Only when the host
1293
+ // turned it on; with it off the tool is absent and the prompt is as before.
1294
+ if (turnConfig.handBack === true) addRuntimeTool(buildHandBackTool(), 'active')
1295
+
1286
1296
  // `search_tools` is added only when something could actually be
1287
1297
  // deferred — one of the runtime tools above, or a caller toolset that
1288
1298
  // declared itself `deferred(...)`. Nothing deferred means nothing for
@@ -1789,6 +1799,15 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
1789
1799
  // The turn's own registry when the host passed none, so a hook's
1790
1800
  // annotation has somewhere to land.
1791
1801
  const promptContributions = params.promptContributions ?? new PromptContributionRegistry()
1802
+ if (turnConfig.handBack === true) {
1803
+ promptContributions.replace({
1804
+ id: HAND_BACK_GUIDANCE_CONTRIBUTION_ID,
1805
+ placement: 'static',
1806
+ render: () => HAND_BACK_GUIDANCE,
1807
+ })
1808
+ } else {
1809
+ promptContributions.unregister(HAND_BACK_GUIDANCE_CONTRIBUTION_ID)
1810
+ }
1792
1811
 
1793
1812
  const promptBuilder = new PromptBuilder({
1794
1813
  systemPrompt: params.systemPrompt,
@@ -2110,6 +2129,7 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
2110
2129
  [GENAI.AGENT_ID]: params.agentId,
2111
2130
  [GENAI.REQUEST_MODEL]: turnConfig.model,
2112
2131
  [GENAI.SYSTEM]: params.provider.id,
2132
+ ...agentRunAttributes(params.provider.id),
2113
2133
  })
2114
2134
 
2115
2135
  let sandbox: Sandbox | undefined
@@ -29,6 +29,8 @@ import {
29
29
  } from '../../../telemetry/attributes.js'
30
30
  import { getTracer } from '../../../telemetry/runtime-accessors.js'
31
31
  import { STRUCTURED_OUTPUT_TOOL_NAME } from '../../../tools/builtins/structuredOutput.js'
32
+ import { SESSION_GOAL_TOOL_NAMES } from '../../../tools/goal/index.js'
33
+ import { HAND_BACK_TOOL_NAME, parseHandBack } from '../../../tools/hand-back/index.js'
32
34
  import type { ToolManager } from '../../../toolsets/manager.js'
33
35
  import { RECALLED_MEMORY_ENTRY_KEY, recalledMemoryIds } from '../../../turn/memory-recall.js'
34
36
  import { type CostInfo, type TokenUsage, promptSizeOf } from '../../../types/common/index.js'
@@ -48,6 +50,7 @@ import {
48
50
  import { classifyProviderError, readOutputOverflow } from '../../../types/provider/errors.js'
49
51
  import type { ChatCompletionResponse, ReasoningEffort } from '../../../types/provider/index.js'
50
52
  import type { AnswerReview, AnswerReviewContext } from '../../../types/session/answer-review.js'
53
+ import type { HandBack } from '../../../types/session/hand-back.js'
51
54
  import type {
52
55
  RecalledMemory,
53
56
  SessionEvent,
@@ -136,6 +139,9 @@ import {
136
139
  } from './step-shaping.js'
137
140
  import { streamProviderTurn } from './stream-turn.js'
138
141
 
142
+ /** Notes a host may add before a turn ends (`TurnConfig.outcome.beforeEnd`) in one turn. */
143
+ const MAX_BEFORE_END_NOTES = 16
144
+
139
145
  /** Capped steps in a row after which the per-step cap stops applying for the turn's next step. */
140
146
  const MAX_CAPPED_STREAK = 3
141
147
 
@@ -339,6 +345,7 @@ export class IterationOrchestrator {
339
345
  private readonly completionAudit?: CompletionAudit
340
346
  /** What the turn changed and what checked it; see {@link OutcomeLedger}. Kept whether or not the host asked. */
341
347
  private readonly outcomeLedger: OutcomeLedger
348
+ private beforeEndNotes = 0
342
349
  private outcomeHydrated = false
343
350
  /** The next step must call a tool, at low effort: set by the stall monitor or a capped step. */
344
351
  private forceActionNextStep = false
@@ -2016,6 +2023,14 @@ export class IterationOrchestrator {
2016
2023
  recorder.pushMessage(createRuntimeContextMessage(auditReminder, 'progress-nudge'))
2017
2024
  continue
2018
2025
  }
2026
+ const endNote =
2027
+ !forceFinalize && closingStopReason === undefined
2028
+ ? await this.askBeforeEnd()
2029
+ : undefined
2030
+ if (endNote !== undefined) {
2031
+ recorder.pushMessage(createRuntimeContextMessage(endNote, 'progress-nudge'))
2032
+ continue
2033
+ }
2019
2034
  if (!forceFinalize && closingStopReason === undefined) {
2020
2035
  const feedback = yield* this.askStopHooks()
2021
2036
  const stoppedByHook = this.ctx.toolExecutor.takeHalt?.()
@@ -2074,6 +2089,28 @@ export class IterationOrchestrator {
2074
2089
  if (reviewOutcome.decision === 'stop') {
2075
2090
  return
2076
2091
  }
2092
+
2093
+ // The model handed the task back to a person. Nothing else ran in
2094
+ // this step (its sibling calls were refused), and no further model
2095
+ // call is made: the turn ends here, as a turn that ended on its own.
2096
+ const handedBack = this.takeHandBack(reviewOutcome.results, response)
2097
+ if (handedBack !== undefined) {
2098
+ this.ctx.log.info('The model handed the task back — ending turn', {
2099
+ [NAMZU.TURN_ID]: recorder.turnId,
2100
+ [NAMZU.ITERATION]: iterationNum,
2101
+ 'namzu.hand_back.kind': handedBack.kind,
2102
+ })
2103
+ recorder.setHandedBack(handedBack)
2104
+ recorder.setStopReason('end_turn')
2105
+ await this.ctx.emitEvent({
2106
+ type: 'iteration_completed',
2107
+ turnId: recorder.turnId,
2108
+ iteration: iterationNum,
2109
+ hasToolCalls: true,
2110
+ })
2111
+ yield* this.ctx.drainPending()
2112
+ break
2113
+ }
2077
2114
  const needsCorrection = await this.recordToolStructuredFailure(
2078
2115
  reviewOutcome.results,
2079
2116
  iterationNum,
@@ -3406,8 +3443,14 @@ export class IterationOrchestrator {
3406
3443
  else this.outcomeLedger.restore(restored.snapshot)
3407
3444
  }
3408
3445
 
3409
- /** Whether a call is one the outcome record may treat as only looking. */
3446
+ /**
3447
+ * Whether a call is one the outcome record may treat as only looking. The
3448
+ * goal tools write the goal's own record, never the workspace; counting a
3449
+ * report of completion as a possible change would make every check before it
3450
+ * read as not known to be fresh.
3451
+ */
3410
3452
  private lookedOnly(toolName: string, input: unknown): boolean {
3453
+ if ((SESSION_GOAL_TOOL_NAMES as readonly string[]).includes(toolName)) return true
3411
3454
  try {
3412
3455
  return !callChangedState(toolName, this.ctx.tools.get(toolName)?.isReadOnly?.(input), false)
3413
3456
  } catch {
@@ -3474,6 +3517,47 @@ export class IterationOrchestrator {
3474
3517
  }
3475
3518
  }
3476
3519
 
3520
+ /**
3521
+ * The host's say before the turn ends (`TurnConfig.outcome.beforeEnd`): a note
3522
+ * for the model, or `undefined` to let the turn end. Bounded, and a hook that
3523
+ * fails never fails the turn.
3524
+ */
3525
+ async askBeforeEnd(): Promise<string | undefined> {
3526
+ const hook = this.ctx.turnConfig.outcome?.beforeEnd
3527
+ if (hook === undefined || this.beforeEndNotes >= MAX_BEFORE_END_NOTES) return undefined
3528
+ try {
3529
+ const note = await hook(async () => {
3530
+ this.hydrateOutcome()
3531
+ return await this.outcomeLedger.preview()
3532
+ })
3533
+ if (typeof note !== 'string' || note.trim() === '') return undefined
3534
+ this.beforeEndNotes += 1
3535
+ return note
3536
+ } catch (err) {
3537
+ this.ctx.log.warn('The host hook before the turn ends failed; the turn ends', {
3538
+ 'exception.message': toErrorMessage(err),
3539
+ })
3540
+ return undefined
3541
+ }
3542
+ }
3543
+
3544
+ /**
3545
+ * The hand-back this step made, when the setting is on and `hand_back_task`
3546
+ * succeeded; the first one when the model made several (the rest were refused).
3547
+ */
3548
+ private takeHandBack(
3549
+ results: readonly ToolCallOutcome[],
3550
+ response: ChatCompletionResponse,
3551
+ ): HandBack | undefined {
3552
+ if (this.ctx.turnConfig.handBack !== true) return undefined
3553
+ const hit = results.find(
3554
+ (r) => r.toolName === HAND_BACK_TOOL_NAME && r.isError !== true && r.skipped !== true,
3555
+ )
3556
+ if (!hit) return undefined
3557
+ const call = (response.message.toolCalls ?? []).find((c) => c.id === hit.toolCallId)
3558
+ return call ? parseHandBack(call.function.arguments) : undefined
3559
+ }
3560
+
3477
3561
  /** The outcome record as the turn leaves it; `undefined` when none was kept. */
3478
3562
  async takeOutcome(): Promise<TurnOutcome | undefined> {
3479
3563
  try {
@@ -1,5 +1,10 @@
1
1
  import { allowRuleScope } from '../../../../authorization/gate.js'
2
2
  import type { AuthorizationGate } from '../../../../authorization/index.js'
3
+ import {
4
+ HAND_BACK_SIBLING_REFUSAL,
5
+ HAND_BACK_TOOL_NAME,
6
+ parseHandBack,
7
+ } from '../../../../tools/hand-back/index.js'
3
8
  import { isTrustedReadOnly } from '../../../../tools/trusted-read-only.js'
4
9
  import type { ToolSourceRef } from '../../../../toolsets/types.js'
5
10
  import type {
@@ -259,7 +264,25 @@ export async function* runToolReview(
259
264
  * with the tracker's refusal instead of being run — the one denial the
260
265
  * tracker makes, on top of whatever the caller already denied.
261
266
  */
262
- const withRepeatRefusals = (denials?: ToolCallDenials): ToolCallDenials | undefined => {
267
+ const withRepeatRefusals = (given?: ToolCallDenials): ToolCallDenials | undefined => {
268
+ // A step that hands the task back runs nothing else: the first hand-back
269
+ // call is the step, every other call in it is refused.
270
+ let denials = given
271
+ if (ctx.turnConfig?.handBack === true) {
272
+ const handBack = toolCallSummaries.find(
273
+ (summary) =>
274
+ summary.name === HAND_BACK_TOOL_NAME && parseHandBack(summary.input) !== undefined,
275
+ )
276
+ if (handBack) {
277
+ let siblings: Map<string, string> | undefined
278
+ for (const summary of toolCallSummaries) {
279
+ if (summary.id === handBack.id || given?.has(summary.id)) continue
280
+ siblings ??= new Map(given ?? [])
281
+ siblings.set(summary.id, HAND_BACK_SIBLING_REFUSAL)
282
+ }
283
+ denials = siblings ?? given
284
+ }
285
+ }
263
286
  const tracker = ctx.repeatCalls
264
287
  if (!tracker) return denials
265
288
  let merged: Map<string, string> | undefined
@@ -15,6 +15,12 @@ import {
15
15
  toolCallFramingViolation,
16
16
  } from '../../../provider/tool-call-framing.js'
17
17
  import { GENAI, NAMZU, chatSpanName, parentContext } from '../../../telemetry/attributes.js'
18
+ import {
19
+ chatStartAttributes,
20
+ chatUsageAttributes,
21
+ errorTypeAttributes,
22
+ genAiErrorType,
23
+ } from '../../../telemetry/genai.js'
18
24
  import {
19
25
  recordModelDuration,
20
26
  recordTimeToFirstToken,
@@ -200,6 +206,7 @@ async function settleCancelledTurn(args: {
200
206
  [GENAI.USAGE_OUTPUT_TOKENS]: args.usage.completionTokens,
201
207
  [NAMZU.CACHE_READ_TOKENS]: args.usage.cachedTokens ?? 0,
202
208
  [NAMZU.CACHE_WRITE_TOKENS]: args.usage.cacheWriteTokens ?? 0,
209
+ ...chatUsageAttributes(args.usage),
203
210
  })
204
211
  args.span.setStatus({ code: SpanStatusCode.OK })
205
212
  args.span.end()
@@ -277,6 +284,7 @@ export async function* streamProviderTurn(
277
284
  ? { [GENAI.REQUEST_TEMPERATURE]: params.temperature }
278
285
  : {}),
279
286
  ...(params.maxTokens !== undefined ? { [GENAI.REQUEST_MAX_TOKENS]: params.maxTokens } : {}),
287
+ ...chatStartAttributes(provider.id),
280
288
  })
281
289
 
282
290
  const messageId = announceAs ?? generateMessageId()
@@ -1053,6 +1061,7 @@ export async function* streamProviderTurn(
1053
1061
  yield* drainPending()
1054
1062
 
1055
1063
  if (streamError && !recoveredToolInputFromStreamError) {
1064
+ chatSpan.setAttributes(errorTypeAttributes(genAiErrorType(streamCause)))
1056
1065
  chatSpan.setStatus({ code: SpanStatusCode.ERROR, message: streamError })
1057
1066
  chatSpan.end()
1058
1067
 
@@ -1132,6 +1141,7 @@ export async function* streamProviderTurn(
1132
1141
  [GENAI.USAGE_OUTPUT_TOKENS]: usage.completionTokens,
1133
1142
  [NAMZU.CACHE_READ_TOKENS]: usage.cachedTokens ?? 0,
1134
1143
  [NAMZU.CACHE_WRITE_TOKENS]: usage.cacheWriteTokens ?? 0,
1144
+ ...chatUsageAttributes(usage),
1135
1145
  })
1136
1146
  chatSpan.setStatus({ code: SpanStatusCode.OK })
1137
1147
  chatSpan.end()
@@ -5,6 +5,7 @@ import { isCallerAbortError, isProviderRequestError } from '../../provider/error
5
5
  import { TokenBudgetAdmissionError } from '../../provider/token-budget.js'
6
6
  import type { ActivityStore } from '../../store/activity/memory.js'
7
7
  import { GENAI, NAMZU } from '../../telemetry/attributes.js'
8
+ import { errorTypeAttributes, genAiErrorType } from '../../telemetry/genai.js'
8
9
  import { explainError } from '../../types/errors/catalog.js'
9
10
  import { toPlatformError } from '../../types/errors/index.js'
10
11
  import type { CheckpointId } from '../../types/hitl/index.js'
@@ -211,6 +212,7 @@ export class ResultAssembler {
211
212
  rootSpan.setAttributes({
212
213
  [NAMZU.TURN_STATUS]: 'error',
213
214
  [NAMZU.ITERATION]: recorder.currentIteration,
215
+ ...errorTypeAttributes(genAiErrorType(err)),
214
216
  })
215
217
  rootSpan.setStatus({ code: SpanStatusCode.ERROR, message: errorMessage })
216
218
  rootSpan.recordException(err instanceof Error ? err : new Error(errorMessage))