melchizedek-agents 0.18.0 → 0.20.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 (293) hide show
  1. package/.env.example +12 -0
  2. package/CHANGELOG.md +1001 -0
  3. package/DOCUMENTATION.md +252 -65
  4. package/QUICKSTART.md +9 -6
  5. package/README.md +42 -4
  6. package/config/agents/examples/harness.yaml +3 -1
  7. package/config/agents/examples/model_zoo.yaml +10 -9
  8. package/config/agents/syndicateSchema.yaml +53 -16
  9. package/config/agents/templates/account_memory.yaml +2 -6
  10. package/config/agents/templates/case_desk.yaml +3 -9
  11. package/config/agents/templates/draft_review.yaml +3 -9
  12. package/config/agents/templates/intake_extractor.yaml +1 -3
  13. package/config/agents/templates/research_brief.yaml +3 -9
  14. package/config/agents/templates/research_desk.yaml +3 -9
  15. package/config/agents/templates/review_panel.yaml +4 -12
  16. package/config/agents/templates/support_triage.yaml +4 -12
  17. package/config/agents/templates/systems_operator.yaml +2 -6
  18. package/db/hardening.sql +7 -1
  19. package/db/migrations/0013_tool_credentials.sql +260 -0
  20. package/dist/lib/a2a/app.d.ts +70 -6
  21. package/dist/lib/a2a/app.js +107 -2
  22. package/dist/lib/a2a/executor.d.ts +20 -3
  23. package/dist/lib/a2a/executor.js +66 -7
  24. package/dist/lib/a2a/remoteAgent.d.ts +24 -9
  25. package/dist/lib/a2a/remoteAgent.js +31 -14
  26. package/dist/lib/adkPeer.d.ts +63 -0
  27. package/dist/lib/adkPeer.js +240 -0
  28. package/dist/lib/compile.d.ts +159 -30
  29. package/dist/lib/compile.js +215 -134
  30. package/dist/lib/compileAdk.d.ts +32 -0
  31. package/dist/lib/compileAdk.js +142 -0
  32. package/dist/lib/compileNative.d.ts +111 -0
  33. package/dist/lib/compileNative.js +278 -0
  34. package/dist/lib/config.d.ts +6 -5
  35. package/dist/lib/config.js +6 -5
  36. package/dist/lib/doctor.d.ts +28 -1
  37. package/dist/lib/doctor.js +64 -2
  38. package/dist/lib/index.d.ts +6 -1
  39. package/dist/lib/index.js +7 -1
  40. package/dist/lib/loadSyndicate.d.ts +18 -0
  41. package/dist/lib/memory/erase.d.ts +5 -3
  42. package/dist/lib/memory/erase.js +5 -2
  43. package/dist/lib/memory/namespace.d.ts +8 -1
  44. package/dist/lib/memory/namespace.js +12 -6
  45. package/dist/lib/memory/providers.js +2 -1
  46. package/dist/lib/memory/supabaseMemoryService.d.ts +37 -3
  47. package/dist/lib/memory/supabaseMemoryService.js +42 -2
  48. package/dist/lib/model.d.ts +47 -0
  49. package/dist/lib/model.js +37 -0
  50. package/dist/lib/models/adapterResolver.d.ts +115 -0
  51. package/dist/lib/models/adapterResolver.js +137 -0
  52. package/dist/lib/models/adkGeminiAdapter.d.ts +90 -0
  53. package/dist/lib/models/adkGeminiAdapter.js +369 -0
  54. package/dist/lib/models/adkShim.d.ts +111 -0
  55. package/dist/lib/models/adkShim.js +136 -0
  56. package/dist/lib/models/capabilities.d.ts +3 -0
  57. package/dist/lib/models/capabilities.js +33 -23
  58. package/dist/lib/models/chatCompletionsAdapter.d.ts +279 -0
  59. package/dist/lib/models/chatCompletionsAdapter.js +854 -0
  60. package/dist/lib/models/circuitBreaker.d.ts +42 -0
  61. package/dist/lib/models/circuitBreaker.js +70 -0
  62. package/dist/lib/models/claudeAdapter.d.ts +103 -0
  63. package/dist/lib/models/claudeAdapter.js +669 -0
  64. package/dist/lib/models/claudeLlm.d.ts +39 -62
  65. package/dist/lib/models/claudeLlm.js +45 -350
  66. package/dist/lib/models/claudeModels.d.ts +127 -0
  67. package/dist/lib/models/claudeModels.js +237 -0
  68. package/dist/lib/models/contract.d.ts +395 -0
  69. package/dist/lib/models/contract.js +39 -0
  70. package/dist/lib/models/endpoints.js +2 -1
  71. package/dist/lib/models/errorResponse.d.ts +50 -0
  72. package/dist/lib/models/errorResponse.js +48 -0
  73. package/dist/lib/models/fallback.d.ts +18 -11
  74. package/dist/lib/models/fallback.js +54 -43
  75. package/dist/lib/models/fallbackAdapter.d.ts +55 -0
  76. package/dist/lib/models/fallbackAdapter.js +62 -0
  77. package/dist/lib/models/gateway.js +2 -1
  78. package/dist/lib/models/gatewayAdapter.d.ts +51 -0
  79. package/dist/lib/models/gatewayAdapter.js +81 -0
  80. package/dist/lib/models/gatewayLlm.d.ts +12 -33
  81. package/dist/lib/models/gatewayLlm.js +16 -77
  82. package/dist/lib/models/geminiAdapter.d.ts +125 -0
  83. package/dist/lib/models/geminiAdapter.js +854 -0
  84. package/dist/lib/models/geminiState.d.ts +46 -0
  85. package/dist/lib/models/geminiState.js +51 -0
  86. package/dist/lib/models/genaiMapping.d.ts +283 -0
  87. package/dist/lib/models/genaiMapping.js +965 -0
  88. package/dist/lib/models/gptAdapter.d.ts +240 -0
  89. package/dist/lib/models/gptAdapter.js +804 -0
  90. package/dist/lib/models/gptLlm.d.ts +63 -123
  91. package/dist/lib/models/gptLlm.js +89 -603
  92. package/dist/lib/models/grokAdapter.d.ts +86 -0
  93. package/dist/lib/models/grokAdapter.js +142 -0
  94. package/dist/lib/models/grokLlm.d.ts +22 -45
  95. package/dist/lib/models/grokLlm.js +24 -69
  96. package/dist/lib/models/kimiAdapter.d.ts +157 -0
  97. package/dist/lib/models/kimiAdapter.js +203 -0
  98. package/dist/lib/models/kimiLlm.d.ts +15 -107
  99. package/dist/lib/models/kimiLlm.js +19 -155
  100. package/dist/lib/models/ollamaAdapter.d.ts +125 -0
  101. package/dist/lib/models/ollamaAdapter.js +155 -0
  102. package/dist/lib/models/ollamaLlm.d.ts +11 -107
  103. package/dist/lib/models/ollamaLlm.js +15 -153
  104. package/dist/lib/models/openAiCompatibleLlm.d.ts +73 -216
  105. package/dist/lib/models/openAiCompatibleLlm.js +109 -671
  106. package/dist/lib/models/providerState.d.ts +63 -0
  107. package/dist/lib/models/providerState.js +61 -0
  108. package/dist/lib/models/reasoning.d.ts +26 -0
  109. package/dist/lib/models/reasoning.js +59 -0
  110. package/dist/lib/models/registry.d.ts +62 -35
  111. package/dist/lib/models/registry.js +100 -124
  112. package/dist/lib/models/retry.d.ts +2 -1
  113. package/dist/lib/models/retry.js +3 -2
  114. package/dist/lib/models/schemaNormalize.d.ts +98 -0
  115. package/dist/lib/models/schemaNormalize.js +321 -1
  116. package/dist/lib/models/tracedGemini.d.ts +41 -0
  117. package/dist/lib/models/tracedGemini.js +98 -0
  118. package/dist/lib/models/urls.d.ts +11 -0
  119. package/dist/lib/models/urls.js +16 -0
  120. package/dist/lib/net/redirects.d.ts +21 -2
  121. package/dist/lib/net/redirects.js +11 -34
  122. package/dist/lib/observability/audit.d.ts +5 -1
  123. package/dist/lib/observability/lineage.d.ts +14 -0
  124. package/dist/lib/observability/lineage.js +33 -0
  125. package/dist/lib/observability/supabaseSpanExporter.d.ts +10 -4
  126. package/dist/lib/observability/supabaseSpanExporter.js +17 -9
  127. package/dist/lib/observability/tracer.d.ts +26 -8
  128. package/dist/lib/observability/tracer.js +43 -18
  129. package/dist/lib/runtime/adkMemoryBridge.d.ts +46 -0
  130. package/dist/lib/runtime/adkMemoryBridge.js +68 -0
  131. package/dist/lib/runtime/adkSessionBridge.d.ts +78 -0
  132. package/dist/lib/runtime/adkSessionBridge.js +176 -0
  133. package/dist/lib/runtime/approvals.d.ts +5 -0
  134. package/dist/lib/runtime/approvals.js +39 -8
  135. package/dist/lib/runtime/credentials.d.ts +61 -0
  136. package/dist/lib/runtime/credentials.js +67 -0
  137. package/dist/lib/runtime/events.d.ts +229 -0
  138. package/dist/lib/runtime/events.js +274 -0
  139. package/dist/lib/runtime/logging.d.ts +32 -0
  140. package/dist/lib/runtime/logging.js +55 -0
  141. package/dist/lib/runtime/memoryService.d.ts +72 -0
  142. package/dist/lib/runtime/memoryService.js +25 -0
  143. package/dist/lib/runtime/native/agentLoop.d.ts +209 -0
  144. package/dist/lib/runtime/native/agentLoop.js +705 -0
  145. package/dist/lib/runtime/native/compaction.d.ts +103 -0
  146. package/dist/lib/runtime/native/compaction.js +191 -0
  147. package/dist/lib/runtime/native/delegate.d.ts +151 -0
  148. package/dist/lib/runtime/native/delegate.js +256 -0
  149. package/dist/lib/runtime/native/history.d.ts +83 -0
  150. package/dist/lib/runtime/native/history.js +312 -0
  151. package/dist/lib/runtime/native/interrupts.d.ts +129 -0
  152. package/dist/lib/runtime/native/interrupts.js +310 -0
  153. package/dist/lib/runtime/native/request.d.ts +215 -0
  154. package/dist/lib/runtime/native/request.js +585 -0
  155. package/dist/lib/runtime/native/selfCorrection.d.ts +164 -0
  156. package/dist/lib/runtime/native/selfCorrection.js +327 -0
  157. package/dist/lib/runtime/native/step.d.ts +156 -0
  158. package/dist/lib/runtime/native/step.js +263 -0
  159. package/dist/lib/runtime/native/taskMode.d.ts +64 -0
  160. package/dist/lib/runtime/native/taskMode.js +126 -0
  161. package/dist/lib/runtime/native/telemetry.d.ts +75 -0
  162. package/dist/lib/runtime/native/telemetry.js +299 -0
  163. package/dist/lib/runtime/native/tempState.d.ts +31 -0
  164. package/dist/lib/runtime/native/tempState.js +56 -0
  165. package/dist/lib/runtime/nativeTurn.d.ts +86 -0
  166. package/dist/lib/runtime/nativeTurn.js +107 -0
  167. package/dist/lib/runtime/questions.d.ts +29 -7
  168. package/dist/lib/runtime/questions.js +47 -32
  169. package/dist/lib/runtime/runtimeFlag.d.ts +38 -0
  170. package/dist/lib/runtime/runtimeFlag.js +58 -0
  171. package/dist/lib/runtime/sessions.d.ts +166 -0
  172. package/dist/lib/runtime/sessions.js +201 -0
  173. package/dist/lib/runtime/syndicateTurn.d.ts +46 -5
  174. package/dist/lib/runtime/syndicateTurn.js +233 -36
  175. package/dist/lib/runtime/valueDepth.d.ts +23 -0
  176. package/dist/lib/runtime/valueDepth.js +41 -0
  177. package/dist/lib/session/supabaseSessionService.d.ts +65 -18
  178. package/dist/lib/session/supabaseSessionService.js +148 -105
  179. package/dist/lib/session/transcript.d.ts +69 -23
  180. package/dist/lib/session/transcript.js +105 -29
  181. package/dist/lib/skills.js +33 -7
  182. package/dist/lib/storage/postgres/credentialStore.d.ts +20 -0
  183. package/dist/lib/storage/postgres/credentialStore.js +82 -0
  184. package/dist/lib/storage/postgres/index.d.ts +12 -0
  185. package/dist/lib/storage/postgres/index.js +6 -1
  186. package/dist/lib/storage/postgres/sessionService.d.ts +44 -16
  187. package/dist/lib/storage/postgres/sessionService.js +128 -66
  188. package/dist/lib/syndicateSchema.d.ts +49 -0
  189. package/dist/lib/syndicateSchema.js +99 -9
  190. package/dist/lib/toolRegistry.d.ts +26 -2
  191. package/dist/lib/toolRegistry.js +73 -17
  192. package/dist/lib/tools/adkTool.d.ts +81 -0
  193. package/dist/lib/tools/adkTool.js +268 -0
  194. package/dist/lib/tools/auth.d.ts +118 -0
  195. package/dist/lib/tools/auth.js +89 -0
  196. package/dist/lib/tools/collectionsSearchTool.d.ts +8 -7
  197. package/dist/lib/tools/collectionsSearchTool.js +12 -27
  198. package/dist/lib/tools/credentialCipher.d.ts +58 -0
  199. package/dist/lib/tools/credentialCipher.js +116 -0
  200. package/dist/lib/tools/credentialStore.d.ts +77 -0
  201. package/dist/lib/tools/credentialStore.js +246 -0
  202. package/dist/lib/tools/examples.d.ts +43 -0
  203. package/dist/lib/tools/examples.js +62 -0
  204. package/dist/lib/tools/generateImageTool.d.ts +15 -6
  205. package/dist/lib/tools/generateImageTool.js +19 -29
  206. package/dist/lib/tools/inspectImageTool.d.ts +9 -6
  207. package/dist/lib/tools/inspectImageTool.js +13 -17
  208. package/dist/lib/tools/mcpToolFactory.d.ts +42 -1
  209. package/dist/lib/tools/mcpToolFactory.js +46 -47
  210. package/dist/lib/tools/memoryTools.d.ts +61 -0
  211. package/dist/lib/tools/memoryTools.js +119 -0
  212. package/dist/lib/tools/nativeTools.d.ts +38 -0
  213. package/dist/lib/tools/nativeTools.js +43 -0
  214. package/dist/lib/tools/oauthConsent.d.ts +181 -0
  215. package/dist/lib/tools/oauthConsent.js +330 -0
  216. package/dist/lib/tools/openapi/call.d.ts +108 -0
  217. package/dist/lib/tools/openapi/call.js +295 -0
  218. package/dist/lib/tools/openapi/parse.d.ts +147 -0
  219. package/dist/lib/tools/openapi/parse.js +570 -0
  220. package/dist/lib/tools/openapiTools.d.ts +25 -17
  221. package/dist/lib/tools/openapiTools.js +124 -121
  222. package/dist/lib/tools/scienceTools.d.ts +14 -14
  223. package/dist/lib/tools/skillToolset.d.ts +58 -87
  224. package/dist/lib/tools/skillToolset.js +61 -142
  225. package/dist/lib/tools/skills/env.d.ts +58 -0
  226. package/dist/lib/tools/skills/env.js +165 -0
  227. package/dist/lib/tools/skills/executor.d.ts +114 -0
  228. package/dist/lib/tools/skills/executor.js +331 -0
  229. package/dist/lib/tools/skills/frontmatter.d.ts +79 -0
  230. package/dist/lib/tools/skills/frontmatter.js +215 -0
  231. package/dist/lib/tools/skills/loader.d.ts +91 -0
  232. package/dist/lib/tools/skills/loader.js +257 -0
  233. package/dist/lib/tools/skills/tools.d.ts +187 -0
  234. package/dist/lib/tools/skills/tools.js +386 -0
  235. package/dist/lib/tools/taskTools.d.ts +15 -15
  236. package/dist/lib/tools/tool.d.ts +354 -0
  237. package/dist/lib/tools/tool.js +304 -0
  238. package/dist/lib/tools/toolContract.d.ts +89 -33
  239. package/dist/lib/tools/toolContract.js +104 -82
  240. package/dist/lib/tools/urlContextTool.d.ts +4 -1
  241. package/dist/lib/tools/urlContextTool.js +6 -2
  242. package/dist/lib/tools/webExtractTool.d.ts +3 -3
  243. package/dist/lib/tools/webExtractTool.js +2 -1
  244. package/dist/lib/tools/webSearchTool.d.ts +7 -10
  245. package/dist/lib/tools/webSearchTool.js +15 -60
  246. package/dist/lib/tools/wikiTools.d.ts +19 -19
  247. package/dist/lib/tools/wikiTools.js +13 -9
  248. package/dist/lib/tools/xApiSearchTool.d.ts +3 -3
  249. package/dist/lib/tools/xApiSearchTool.js +2 -1
  250. package/dist/lib/tools/xSearchTool.d.ts +8 -10
  251. package/dist/lib/tools/xSearchTool.js +12 -63
  252. package/dist/lib/tools/xaiSearchParams.d.ts +41 -0
  253. package/dist/lib/tools/xaiSearchParams.js +126 -0
  254. package/dist/lib/wiki/agentRun.d.ts +12 -0
  255. package/dist/lib/wiki/agentRun.js +74 -35
  256. package/dist/lib/wiki/entities.d.ts +12 -0
  257. package/dist/lib/wiki/entities.js +45 -3
  258. package/dist/lib/workflow/agentNode.d.ts +153 -0
  259. package/dist/lib/workflow/agentNode.js +365 -0
  260. package/dist/lib/workflow/graph.d.ts +178 -0
  261. package/dist/lib/workflow/graph.js +468 -0
  262. package/dist/lib/workflow/nodeEvents.d.ts +50 -0
  263. package/dist/lib/workflow/nodeEvents.js +101 -0
  264. package/dist/lib/workflow/pause.d.ts +122 -0
  265. package/dist/lib/workflow/pause.js +180 -0
  266. package/dist/lib/workflow/resume.d.ts +166 -0
  267. package/dist/lib/workflow/resume.js +441 -0
  268. package/dist/lib/workflow/route.d.ts +58 -0
  269. package/dist/lib/workflow/route.js +69 -0
  270. package/dist/lib/workflow/scheduler.d.ts +418 -0
  271. package/dist/lib/workflow/scheduler.js +756 -0
  272. package/dist/lib/workflow/toolNode.d.ts +127 -0
  273. package/dist/lib/workflow/toolNode.js +236 -0
  274. package/dist/lib/workflow/turn.d.ts +118 -0
  275. package/dist/lib/workflow/turn.js +216 -0
  276. package/dist/lib/workflow.d.ts +33 -11
  277. package/dist/lib/workflow.js +49 -27
  278. package/dist/lib/workflowConfig.d.ts +7 -2
  279. package/dist/lib/workflowConfig.js +7 -12
  280. package/dist/scripts/a2a_server.js +4 -3
  281. package/dist/scripts/assistant_worker.d.ts +2 -1
  282. package/dist/scripts/assistant_worker.js +9 -4
  283. package/dist/scripts/doctor.d.ts +3 -1
  284. package/dist/scripts/doctor.js +4 -2
  285. package/dist/scripts/syndicate_chat.js +19 -10
  286. package/package.json +9 -3
  287. package/skills/briefs/melchizedek-author.md +3 -2
  288. package/skills/briefs/melchizedek-models.md +13 -9
  289. package/skills/briefs/melchizedek.md +3 -3
  290. package/skills/melchizedek/SKILL.md +2 -1
  291. package/skills/melchizedek-author/SKILL.md +2 -1
  292. package/skills/melchizedek-author/assets/minimal.yaml +2 -0
  293. package/skills/melchizedek-models/SKILL.md +23 -6
@@ -1,6 +1,6 @@
1
1
  /**
2
- * lib/storage/postgres/sessionService.ts — ADK sessions on a direct Postgres
3
- * connection (ADR 0021).
2
+ * lib/storage/postgres/sessionService.ts — sessions on a direct Postgres
3
+ * connection (ADR 0021), for either runtime (ADR 0052, ADR 0058).
4
4
  *
5
5
  * WHY it differs from the Supabase session service:
6
6
  * That one re-uploads a conversation's WHOLE events array on every event.
@@ -15,23 +15,44 @@
15
15
  * event; appending each event once removes that cost, and a DELEGATE
16
16
  * conversation that is replayed to the model gets back exactly what it sent.
17
17
  *
18
+ * TWO FACES, ONE STORE:
19
+ * The class is the engine's SessionService (create, get, list, delete,
20
+ * append) and ADK's BaseSessionService (createSession, getSession, …), so
21
+ * the ADK runtime and the native one read and write the same rows. The ADK
22
+ * methods call the engine's, except appendEvent, which applies the event
23
+ * to the runner's session through ADK's base service (its write-order
24
+ * check included), where append uses applyEvent. Both record it the same
25
+ * way. The rules are lib/runtime/sessions.ts's: a create of an existing
26
+ * id keeps the conversation, `afterTimestamp` is strict and filters
27
+ * before `numRecentEvents` counts, an event whose id the caller's session
28
+ * already holds replaces that row in place (as applyEvent replaces it in
29
+ * the session), and lastUpdateTime is the appended event's timestamp.
30
+ *
18
31
  * Rows share adk_sessions with the Supabase service (same id scheme:
19
32
  * '<appName>:<userId>:<sessionId>'), so erase and the session prune cover
20
33
  * both. Tables: db/migrations/0001_base.sql and 0003_postgres_storage.sql.
21
34
  */
22
35
  import { randomUUID } from 'node:crypto';
23
- import { BaseSessionService, createSession } from '@google/adk';
36
+ import { BaseSessionService } from '../../adkPeer.js';
37
+ import { applyEvent, listPage, listWindow, withoutTempKeys } from '../../runtime/sessions.js';
24
38
  function dbId(appName, userId, sessionId) {
25
39
  return `${appName}:${userId}:${sessionId}`;
26
40
  }
27
- /** The persisted part of a state delta: ADK never stores `temp:` keys. */
41
+ /** The persisted part of a state delta: `temp:` keys are never stored. */
28
42
  export function persistedDelta(delta) {
29
- const out = {};
30
- for (const [k, v] of Object.entries(delta ?? {})) {
31
- if (!k.startsWith('temp:'))
32
- out[k] = v;
33
- }
34
- return out;
43
+ return withoutTempKeys(delta ?? {});
44
+ }
45
+ /** Whether the session already holds an event with this one's id: the append replaces it. */
46
+ function holds(session, event) {
47
+ return Boolean(event.id) && session.events.some((e) => e.id === event.id);
48
+ }
49
+ /** An event's timestamp, or the clock for an event without one. */
50
+ function eventTime(ms) {
51
+ return typeof ms === 'number' && Number.isFinite(ms) ? ms : Date.now();
52
+ }
53
+ /** last_update_time is BIGINT milliseconds. */
54
+ function rowTime(ms) {
55
+ return Math.floor(eventTime(ms));
35
56
  }
36
57
  /**
37
58
  * A conversation written by the Supabase session service keeps its events as
@@ -52,6 +73,18 @@ const IMPORT_LEGACY_EVENTS = `
52
73
  jsonb_array_elements(coalesce(s.events, '[]'::jsonb)) WITH ORDINALITY AS e(elem, ord)
53
74
  WHERE s.id = $1
54
75
  AND NOT EXISTS (SELECT 1 FROM adk_session_events WHERE session_id = $1)`;
76
+ /** The conversation's next event row. Runs under the session's row lock. */
77
+ const INSERT_EVENT = `
78
+ INSERT INTO adk_session_events (session_id, seq, ts, event)
79
+ VALUES ($1, (SELECT coalesce(max(seq), 0) + 1 FROM adk_session_events WHERE session_id = $1), $2, $3::jsonb)`;
80
+ /**
81
+ * An event whose id the session already holds replaces that row in place,
82
+ * as applyEvent replaces it in the session. It reads every event of the
83
+ * conversation, so it runs only when the caller's session held the id.
84
+ */
85
+ const REPLACE_EVENT = `
86
+ UPDATE adk_session_events SET ts = $2, event = $3::jsonb
87
+ WHERE session_id = $1 AND event->>'id' = $4`;
55
88
  export class PostgresSessionService extends BaseSessionService {
56
89
  ttlMs;
57
90
  pool;
@@ -63,28 +96,23 @@ export class PostgresSessionService extends BaseSessionService {
63
96
  expireAt() {
64
97
  return new Date(Date.now() + this.ttlMs).toISOString();
65
98
  }
66
- async createSession(request) {
99
+ // ── The engine's interface (lib/runtime/sessions.ts) ─────────────────────
100
+ async create(request) {
67
101
  const sessionId = request.sessionId || randomUUID();
68
102
  const id = dbId(request.appName, request.userId, sessionId);
103
+ const state = withoutTempKeys(request.state ?? {});
69
104
  const now = Date.now();
70
- // A concurrent create for the same id keeps the row that is already
71
- // there, events and all, instead of resetting it.
105
+ // A create for an id that exists, concurrent or not, keeps the row
106
+ // that is already there, events and all, instead of resetting it.
72
107
  const inserted = await this.pool.query(`INSERT INTO adk_sessions (id, app_name, user_id, state, events, last_update_time, expire_at)
73
108
  VALUES ($1, $2, $3, $4::jsonb, '[]'::jsonb, $5, $6)
74
- ON CONFLICT (id) DO NOTHING`, [id, request.appName, request.userId, JSON.stringify(request.state ?? {}), now, this.expireAt()]);
109
+ ON CONFLICT (id) DO NOTHING`, [id, request.appName, request.userId, JSON.stringify(state), now, this.expireAt()]);
75
110
  if (inserted.rowCount === 0) {
76
- const existing = await this.getSession({ appName: request.appName, userId: request.userId, sessionId });
111
+ const existing = await this.get({ appName: request.appName, userId: request.userId, sessionId });
77
112
  if (existing)
78
113
  return existing;
79
114
  }
80
- return createSession({
81
- id: sessionId,
82
- appName: request.appName,
83
- userId: request.userId,
84
- state: request.state ?? {},
85
- events: [],
86
- lastUpdateTime: now,
87
- });
115
+ return { id: sessionId, appName: request.appName, userId: request.userId, state, events: [], lastUpdateTime: now };
88
116
  }
89
117
  /** Copy a legacy JSON history into rows, once, under the conversation's row lock. */
90
118
  async importLegacyEvents(id) {
@@ -103,8 +131,9 @@ export class PostgresSessionService extends BaseSessionService {
103
131
  client.release();
104
132
  }
105
133
  }
106
- async getSession(request) {
107
- const id = dbId(request.appName, request.userId, request.sessionId);
134
+ /** selectEvents' rules, pushed down: strictly after the timestamp, then the newest N, oldest first. */
135
+ async get(key, options = {}) {
136
+ const id = dbId(key.appName, key.userId, key.sessionId);
108
137
  const row = await this.pool.query(`SELECT app_name, user_id, state, last_update_time,
109
138
  (jsonb_array_length(coalesce(events, '[]'::jsonb)) > 0
110
139
  AND NOT EXISTS (SELECT 1 FROM adk_session_events WHERE session_id = $1)) AS legacy
@@ -113,20 +142,20 @@ export class PostgresSessionService extends BaseSessionService {
113
142
  return undefined;
114
143
  if (row.rows[0].legacy)
115
144
  await this.importLegacyEvents(id);
116
- const cfg = request.config ?? {};
117
145
  const params = [id];
118
146
  let where = 'session_id = $1';
119
- if (cfg.afterTimestamp) {
120
- params.push(cfg.afterTimestamp);
147
+ if (options.afterTimestamp) {
148
+ params.push(options.afterTimestamp);
121
149
  where += ` AND ts > $${params.length}`;
122
150
  }
123
- // The newest N, returned oldest first.
124
- const events = cfg.numRecentEvents
125
- ? await this.pool.query(`SELECT event FROM (SELECT event, seq FROM adk_session_events WHERE ${where} ORDER BY seq DESC LIMIT ${Math.max(0, Math.floor(cfg.numRecentEvents))}) recent ORDER BY seq ASC`, params)
151
+ const n = options.numRecentEvents;
152
+ const recent = typeof n === 'number' && Number.isFinite(n) ? Math.floor(n) : 0;
153
+ const events = recent > 0
154
+ ? await this.pool.query(`SELECT event FROM (SELECT event, seq FROM adk_session_events WHERE ${where} ORDER BY seq DESC LIMIT $${params.length + 1}) recent ORDER BY seq ASC`, [...params, recent])
126
155
  : await this.pool.query(`SELECT event FROM adk_session_events WHERE ${where} ORDER BY seq ASC`, params);
127
156
  const r = row.rows[0];
128
157
  return {
129
- id: request.sessionId,
158
+ id: key.sessionId,
130
159
  appName: r.app_name,
131
160
  userId: r.user_id,
132
161
  state: r.state ?? {},
@@ -135,15 +164,13 @@ export class PostgresSessionService extends BaseSessionService {
135
164
  };
136
165
  }
137
166
  /**
138
- * Same paging contract as the Supabase service: `limit` with `page`
139
- * (1-based, wins) or `offset` (0-based), optional order by last update,
140
- * and the real total alongside the page. Events are omitted.
167
+ * listWindow and listPage's paging, with the real total beside the page.
168
+ * Every user's sessions when `userId` is absent. By last update, ties by
169
+ * id, when an order is asked for; in the order the rows were created
170
+ * otherwise. Events are left out.
141
171
  */
142
- async listSessions(request) {
143
- const { limit, order } = request;
144
- const offset = limit !== undefined && request.page !== undefined
145
- ? (Math.max(1, request.page) - 1) * limit
146
- : (request.offset ?? 0);
172
+ async list(request) {
173
+ const { offset, limit } = listWindow(request);
147
174
  const params = [request.appName];
148
175
  let where = 'app_name = $1';
149
176
  if (request.userId !== undefined) {
@@ -151,9 +178,18 @@ export class PostgresSessionService extends BaseSessionService {
151
178
  where += ` AND user_id = $${params.length}`;
152
179
  }
153
180
  const total = await this.pool.query(`SELECT count(*)::int AS n FROM adk_sessions WHERE ${where}`, params);
154
- const orderBy = order ? `ORDER BY last_update_time ${order === 'asc' ? 'ASC' : 'DESC'}, id` : 'ORDER BY id';
155
- const window = `${limit !== undefined ? `LIMIT ${Math.max(0, Math.floor(limit))}` : ''} OFFSET ${Math.max(0, Math.floor(offset))}`;
156
- const rows = await this.pool.query(`SELECT id, app_name, user_id, state, last_update_time FROM adk_sessions WHERE ${where} ${orderBy} ${window}`, params);
181
+ const orderBy = request.order
182
+ ? `ORDER BY last_update_time ${request.order === 'asc' ? 'ASC' : 'DESC'}, id`
183
+ : 'ORDER BY created_at, id';
184
+ const paged = [...params];
185
+ let window = '';
186
+ if (limit !== undefined) {
187
+ paged.push(limit);
188
+ window += `LIMIT $${paged.length} `;
189
+ }
190
+ paged.push(offset);
191
+ window += `OFFSET $${paged.length}`;
192
+ const rows = await this.pool.query(`SELECT id, app_name, user_id, state, last_update_time FROM adk_sessions WHERE ${where} ${orderBy} ${window}`, paged);
157
193
  const sessions = rows.rows.map((row) => ({
158
194
  id: String(row.id).slice(String(row.app_name).length + String(row.user_id).length + 2),
159
195
  appName: row.app_name,
@@ -162,33 +198,32 @@ export class PostgresSessionService extends BaseSessionService {
162
198
  events: [],
163
199
  lastUpdateTime: Number(row.last_update_time ?? 0),
164
200
  }));
165
- const totalItems = total.rows[0].n;
166
- return {
167
- sessions,
168
- page: limit ? Math.floor(offset / limit) + 1 : 1,
169
- limit: limit ?? totalItems,
170
- totalItems,
171
- totalPages: limit ? Math.max(1, Math.ceil(totalItems / limit)) : 1,
172
- };
201
+ return { sessions, ...listPage(total.rows[0].n, request) };
173
202
  }
174
- async deleteSession(request) {
203
+ async delete(key) {
175
204
  // Events go with it (ON DELETE CASCADE).
176
- await this.pool.query('DELETE FROM adk_sessions WHERE id = $1', [
177
- dbId(request.appName, request.userId, request.sessionId),
178
- ]);
205
+ await this.pool.query('DELETE FROM adk_sessions WHERE id = $1', [dbId(key.appName, key.userId, key.sessionId)]);
179
206
  }
180
- async appendEvent(request) {
181
- const { session, event } = request;
207
+ async append(session, event) {
182
208
  // Streaming fragments are appended whole once complete.
183
209
  if (event.partial)
184
210
  return event;
185
- // ADK's own merge: applies the state delta to the live session, strips
186
- // temp: keys, pushes the event. Must run (see the Supabase service).
187
- await super.appendEvent({ session, event });
188
- session.lastUpdateTime = Date.now();
211
+ const replacing = holds(session, event);
212
+ const stored = applyEvent(session, event);
213
+ await this.record(session, stored, replacing);
214
+ return stored;
215
+ }
216
+ /**
217
+ * One event into the store, in one transaction: its row, the state delta
218
+ * merged into the stored state, and the times. Two copies of a session
219
+ * appending in turn both land, since each writes only its own event.
220
+ */
221
+ async record(session, event, replacing) {
189
222
  const id = dbId(session.appName, session.userId, session.id);
190
223
  const delta = persistedDelta(event.actions?.stateDelta);
191
- const stored = JSON.parse(JSON.stringify(event));
224
+ const ts = typeof event.timestamp === 'number' ? event.timestamp : null;
225
+ const updated = rowTime(event.timestamp);
226
+ const json = JSON.stringify(event);
192
227
  const client = await this.pool.connect();
193
228
  try {
194
229
  await client.query('BEGIN');
@@ -198,19 +233,20 @@ export class PostgresSessionService extends BaseSessionService {
198
233
  if (locked.rowCount === 0) {
199
234
  await client.query(`INSERT INTO adk_sessions (id, app_name, user_id, state, events, last_update_time, expire_at)
200
235
  VALUES ($1, $2, $3, '{}'::jsonb, '[]'::jsonb, $4, $5)
201
- ON CONFLICT (id) DO NOTHING`, [id, session.appName, session.userId, session.lastUpdateTime, this.expireAt()]);
236
+ ON CONFLICT (id) DO NOTHING`, [id, session.appName, session.userId, updated, this.expireAt()]);
202
237
  await client.query('SELECT 1 FROM adk_sessions WHERE id = $1 FOR UPDATE', [id]);
203
238
  }
204
239
  // A conversation the Supabase service wrote: its history first.
205
240
  await client.query(IMPORT_LEGACY_EVENTS, [id]);
206
- await client.query(`INSERT INTO adk_session_events (session_id, seq, ts, event)
207
- VALUES ($1, (SELECT coalesce(max(seq), 0) + 1 FROM adk_session_events WHERE session_id = $1), $2, $3::jsonb)`, [id, typeof event.timestamp === 'number' ? event.timestamp : null, JSON.stringify(stored)]);
241
+ const replaced = replacing ? await client.query(REPLACE_EVENT, [id, ts, json, event.id]) : undefined;
242
+ if (!replaced?.rowCount)
243
+ await client.query(INSERT_EVENT, [id, ts, json]);
208
244
  await client.query(`UPDATE adk_sessions
209
245
  SET state = coalesce(state, '{}'::jsonb) || $2::jsonb,
210
246
  last_update_time = $3,
211
247
  expire_at = $4,
212
248
  updated_at = now()
213
- WHERE id = $1`, [id, JSON.stringify(delta), session.lastUpdateTime, this.expireAt()]);
249
+ WHERE id = $1`, [id, JSON.stringify(delta), updated, this.expireAt()]);
214
250
  await client.query('COMMIT');
215
251
  }
216
252
  catch (err) {
@@ -220,6 +256,32 @@ export class PostgresSessionService extends BaseSessionService {
220
256
  finally {
221
257
  client.release();
222
258
  }
259
+ }
260
+ // ── ADK's BaseSessionService, for the ADK runtime ─────────────────────────
261
+ async createSession(request) {
262
+ return (await this.create(request));
263
+ }
264
+ async getSession(request) {
265
+ const key = { appName: request.appName, userId: request.userId, sessionId: request.sessionId };
266
+ return (await this.get(key, request.config));
267
+ }
268
+ async listSessions(request) {
269
+ return (await this.list(request));
270
+ }
271
+ async deleteSession(request) {
272
+ await this.delete(request);
273
+ }
274
+ async appendEvent(request) {
275
+ const { session, event } = request;
276
+ if (event.partial)
277
+ return event;
278
+ // ADK's own merge: applies the state delta to the live session, strips
279
+ // temp: keys, replaces or pushes the event. It must run: a bare push
280
+ // drops every state write (see the Supabase service).
281
+ const replacing = holds(session, event);
282
+ await super.appendEvent({ session, event });
283
+ session.lastUpdateTime = eventTime(event.timestamp);
284
+ await this.record(session, event, replacing);
223
285
  return event;
224
286
  }
225
287
  }
@@ -24,6 +24,7 @@
24
24
  import { z } from 'zod';
25
25
  import type { SyndicateYamlConfig } from './loadSyndicate.ts';
26
26
  export declare const MEMORY_SYSTEMS: readonly ['internal-only', 'session-only', 'long-term'];
27
+ export declare const REASONING_LEVELS: readonly ['none', 'low', 'medium', 'high'];
27
28
  export declare const SKILL_SCRIPT_MODES: readonly ['none', 'local'];
28
29
  export declare const agentSchema: z.ZodObject<{
29
30
  name: z.ZodString;
@@ -60,7 +61,16 @@ export declare const agentSchema: z.ZodObject<{
60
61
  thinkingBudget: z.ZodOptional<z.ZodNumber>;
61
62
  includeThoughts: z.ZodOptional<z.ZodBoolean>;
62
63
  }, z.core.$loose>>;
64
+ reasoningEffort: z.ZodOptional<z.ZodString>;
63
65
  }, z.core.$loose>>;
66
+ reasoning: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
67
+ high: "high";
68
+ low: "low";
69
+ medium: "medium";
70
+ none: "none";
71
+ }>, z.ZodObject<{
72
+ budget_tokens: z.ZodNumber;
73
+ }, z.core.$strict>]>>;
64
74
  outputSchema: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
65
75
  mcp_server_url: z.ZodOptional<z.ZodString>;
66
76
  mcp_tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -71,6 +81,8 @@ export declare const agentSchema: z.ZodObject<{
71
81
  none: "none";
72
82
  }>>;
73
83
  tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
84
+ env: z.ZodOptional<z.ZodArray<z.ZodString>>;
85
+ secret_env: z.ZodOptional<z.ZodArray<z.ZodString>>;
74
86
  }, z.core.$strict>>;
75
87
  openapi: z.ZodOptional<z.ZodArray<z.ZodObject<{
76
88
  spec: z.ZodString;
@@ -146,7 +158,16 @@ export declare const subagentSchema: z.ZodObject<{
146
158
  thinkingBudget: z.ZodOptional<z.ZodNumber>;
147
159
  includeThoughts: z.ZodOptional<z.ZodBoolean>;
148
160
  }, z.core.$loose>>;
161
+ reasoningEffort: z.ZodOptional<z.ZodString>;
149
162
  }, z.core.$loose>>;
163
+ reasoning: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
164
+ high: "high";
165
+ low: "low";
166
+ medium: "medium";
167
+ none: "none";
168
+ }>, z.ZodObject<{
169
+ budget_tokens: z.ZodNumber;
170
+ }, z.core.$strict>]>>;
150
171
  outputSchema: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
151
172
  mcp_server_url: z.ZodOptional<z.ZodString>;
152
173
  mcp_tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -157,6 +178,8 @@ export declare const subagentSchema: z.ZodObject<{
157
178
  none: "none";
158
179
  }>>;
159
180
  tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
181
+ env: z.ZodOptional<z.ZodArray<z.ZodString>>;
182
+ secret_env: z.ZodOptional<z.ZodArray<z.ZodString>>;
160
183
  }, z.core.$strict>>;
161
184
  openapi: z.ZodOptional<z.ZodArray<z.ZodObject<{
162
185
  spec: z.ZodString;
@@ -236,7 +259,16 @@ export declare const syndicateSchema: z.ZodObject<{
236
259
  thinkingBudget: z.ZodOptional<z.ZodNumber>;
237
260
  includeThoughts: z.ZodOptional<z.ZodBoolean>;
238
261
  }, z.core.$loose>>;
262
+ reasoningEffort: z.ZodOptional<z.ZodString>;
239
263
  }, z.core.$loose>>;
264
+ reasoning: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
265
+ high: "high";
266
+ low: "low";
267
+ medium: "medium";
268
+ none: "none";
269
+ }>, z.ZodObject<{
270
+ budget_tokens: z.ZodNumber;
271
+ }, z.core.$strict>]>>;
240
272
  outputSchema: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
241
273
  mcp_server_url: z.ZodOptional<z.ZodString>;
242
274
  mcp_tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -247,6 +279,8 @@ export declare const syndicateSchema: z.ZodObject<{
247
279
  none: "none";
248
280
  }>>;
249
281
  tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
282
+ env: z.ZodOptional<z.ZodArray<z.ZodString>>;
283
+ secret_env: z.ZodOptional<z.ZodArray<z.ZodString>>;
250
284
  }, z.core.$strict>>;
251
285
  openapi: z.ZodOptional<z.ZodArray<z.ZodObject<{
252
286
  spec: z.ZodString;
@@ -316,7 +350,16 @@ export declare const syndicateSchema: z.ZodObject<{
316
350
  thinkingBudget: z.ZodOptional<z.ZodNumber>;
317
351
  includeThoughts: z.ZodOptional<z.ZodBoolean>;
318
352
  }, z.core.$loose>>;
353
+ reasoningEffort: z.ZodOptional<z.ZodString>;
319
354
  }, z.core.$loose>>;
355
+ reasoning: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
356
+ high: "high";
357
+ low: "low";
358
+ medium: "medium";
359
+ none: "none";
360
+ }>, z.ZodObject<{
361
+ budget_tokens: z.ZodNumber;
362
+ }, z.core.$strict>]>>;
320
363
  outputSchema: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
321
364
  mcp_server_url: z.ZodOptional<z.ZodString>;
322
365
  mcp_tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -327,6 +370,8 @@ export declare const syndicateSchema: z.ZodObject<{
327
370
  none: "none";
328
371
  }>>;
329
372
  tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
373
+ env: z.ZodOptional<z.ZodArray<z.ZodString>>;
374
+ secret_env: z.ZodOptional<z.ZodArray<z.ZodString>>;
330
375
  }, z.core.$strict>>;
331
376
  openapi: z.ZodOptional<z.ZodArray<z.ZodObject<{
332
377
  spec: z.ZodString;
@@ -395,6 +440,8 @@ export declare const syndicateSchema: z.ZodObject<{
395
440
  initial_delay: z.ZodOptional<z.ZodNumber>;
396
441
  max_delay: z.ZodOptional<z.ZodNumber>;
397
442
  backoff_factor: z.ZodOptional<z.ZodNumber>;
443
+ jitter: z.ZodOptional<z.ZodNumber>;
444
+ exceptions: z.ZodOptional<z.ZodArray<z.ZodString>>;
398
445
  }, z.core.$strict>>;
399
446
  timeout: z.ZodOptional<z.ZodNumber>;
400
447
  }, z.core.$strict>>>;
@@ -423,6 +470,8 @@ export declare const syndicateSchema: z.ZodObject<{
423
470
  * snake/camel drift (`output_key` for `outputKey`) count as an exact match.
424
471
  */
425
472
  export declare function suggest(input: string, candidates: readonly string[]): string | undefined;
473
+ /** The generateContentConfig keys `reasoning` replaces (ADR 0047). */
474
+ export declare const REASONING_OLDER_SPELLING: readonly ['thinkingConfig', 'reasoningEffort'];
426
475
  /**
427
476
  * Thrown by validateSyndicateConfig. `problems` holds one `<file>: <path> —
428
477
  * <what>` line per problem (the message lists them all); `issues` holds the
@@ -23,6 +23,7 @@
23
23
  */
24
24
  import { z } from 'zod';
25
25
  import { DEFAULT_ROUTE_KEY, NODE_KINDS, ROUTE_STEP_SUFFIX, START_NAME, elementNames, nodeKind } from './workflowConfig.js';
26
+ import { isSecretShapedEnvName } from './tools/skills/env.js';
26
27
  // ── Leaf rules ───────────────────────────────────────────────────────────────
27
28
  /**
28
29
  * ADK's own rule (validateAgentName in @google/adk base_agent): checked here
@@ -41,7 +42,25 @@ const thinkingConfig = z
41
42
  thinkingBudget: z.number().int().optional().describe('Reasoning token budget. 0 = off, -1 = dynamic.'),
42
43
  includeThoughts: z.boolean().optional().describe('Stream the thinking trace in the response.'),
43
44
  })
44
- .describe('Gemini thinking controls (generateContentConfig.thinkingConfig).');
45
+ .describe('Older spelling of `reasoning` for Gemini and Claude (thinkingLevel or thinkingBudget). Cannot be combined with `reasoning`.');
46
+ export const REASONING_LEVELS = ['none', 'low', 'medium', 'high'];
47
+ /**
48
+ * The provider-neutral reasoning key (ADR 0047): a level, or a token budget.
49
+ * The compiler maps it per provider (lib/compile.ts reasoningConfig).
50
+ */
51
+ const reasoningSchema = z
52
+ .union([
53
+ z.enum(REASONING_LEVELS),
54
+ z.strictObject({
55
+ budget_tokens: z.number().int().min(0).describe('Thinking tokens; 0 = none. Effort-only providers take the nearest level at or above it.'),
56
+ }),
57
+ ], {
58
+ error: (iss) => {
59
+ const hint = typeof iss.input === 'string' ? suggest(iss.input, REASONING_LEVELS) : undefined;
60
+ return `must be one of ${REASONING_LEVELS.join(' | ')}, or { budget_tokens: <integer ≥ 0> } (got ${describeValue(iss.input)}${hint ? ` — did you mean "${hint}"?` : ''})`;
61
+ },
62
+ })
63
+ .describe('How hard this agent reasons, on any provider: none | low | medium | high, or { budget_tokens: <int> }. Replaces generateContentConfig.thinkingConfig and reasoningEffort (ADR 0047).');
45
64
  /**
46
65
  * Loose on purpose: provider-specific fields (e.g. `toolConfig`) pass through
47
66
  * to the SDK. Only the documented fields are typed, so `temperature: hot`
@@ -63,10 +82,15 @@ const generateContentConfig = z
63
82
  .array(z.looseObject({ category: z.string(), threshold: z.string() }))
64
83
  .optional(),
65
84
  thinkingConfig: thinkingConfig.optional(),
85
+ reasoningEffort: z
86
+ .string()
87
+ .optional()
88
+ .describe('Older spelling of `reasoning` for the chat-completions and Responses providers. Cannot be combined with `reasoning`.'),
66
89
  })
67
90
  .describe('Model generation config (@google/genai GenerateContentConfig). Do not set tools here.');
68
91
  // ── Skills ───────────────────────────────────────────────────────────────────
69
92
  export const SKILL_SCRIPT_MODES = ['none', 'local'];
93
+ const envName = z.string().regex(/^[A-Z_][A-Z0-9_]*$/, 'an environment variable name (A–Z, 0–9, _)');
70
94
  /**
71
95
  * Agent Skills for one agent (lib/tools/skillToolset.ts): the frontmatter
72
96
  * index is injected into the instruction at compile time, a skill is read in
@@ -87,10 +111,27 @@ const skillsSchema = z
87
111
  .array(z.string().min(1))
88
112
  .optional()
89
113
  .describe('Registry tool names a skill may unlock through its `allowed-tools` frontmatter once it is loaded. Omitted, no skill unlocks anything.'),
114
+ env: z
115
+ .array(envName)
116
+ .optional()
117
+ .describe('Environment variable NAMES (never values) a skill script gets, beyond PATH, HOME, the temp directory, the locale and the OS essentials (ADR 0086). A name that looks like a secret (KEY, TOKEN, SECRET, PASSWORD, AUTH, DATABASE, …) belongs under secret_env.'),
118
+ secret_env: z
119
+ .array(envName)
120
+ .optional()
121
+ .describe('Secret-shaped environment variable names a skill script gets, listed here so passing a credential to a script is a deliberate, reviewable act (ADR 0086).'),
122
+ })
123
+ .superRefine((skills, ctx) => {
124
+ (skills.env ?? []).forEach((name, j) => {
125
+ if (isSecretShapedEnvName(name)) {
126
+ ctx.addIssue({ code: 'custom', path: ['env', j], message: `'${name}' looks like it holds a secret; a script gets it only if you list it under skills.secret_env (ADR 0086)` });
127
+ }
128
+ });
129
+ if ((skills.env?.length || skills.secret_env?.length) && skills.scripts !== 'local') {
130
+ ctx.addIssue({ code: 'custom', path: [skills.env?.length ? 'env' : 'secret_env'], message: 'variables reach skill scripts only, which run only with scripts: "local"' });
131
+ }
90
132
  })
91
133
  .describe('Agent Skills (SKILL.md directories) this agent reads the way a coding harness does.');
92
134
  // ── OpenAPI ──────────────────────────────────────────────────────────────────
93
- const envName = z.string().regex(/^[A-Z_][A-Z0-9_]*$/, 'an environment variable name (A–Z, 0–9, _)');
94
135
  const openapiEntry = z
95
136
  .strictObject({
96
137
  spec: z.string().min(1).describe('An OpenAPI 3 spec file (.yaml, .yml, .json), relative to this syndicate file. Files only, never URLs.'),
@@ -151,6 +192,7 @@ const agentFields = {
151
192
  disallowTransferToPeers: z.boolean().optional(),
152
193
  outputKey: z.string().optional().describe('Session-state key the final reply is saved under.'),
153
194
  generateContentConfig: generateContentConfig.optional(),
195
+ reasoning: reasoningSchema.optional(),
154
196
  outputSchema: z
155
197
  .record(z.string(), z.unknown())
156
198
  .optional()
@@ -254,12 +296,20 @@ const edgeElement = z.union([
254
296
  .record(z.string().min(1), edgeTargets)
255
297
  .describe('A routing map after a node: `{ <route>: <node or nodes>, default: <node> }`, matched against that node\'s output.'),
256
298
  ]);
299
+ /** An error class name, as ADK matches `exceptions` against one: an identifier. */
300
+ const ERROR_NAME = /^[A-Za-z_$][\w$]*$/;
257
301
  const retrySchema = z
258
302
  .strictObject({
259
303
  max_attempts: z.number().int().positive().optional().describe('Attempts including the first; 1 = no retry. ADK default 5.'),
260
304
  initial_delay: z.number().nonnegative().optional().describe('Seconds before the first retry.'),
261
305
  max_delay: z.number().nonnegative().optional(),
262
306
  backoff_factor: z.number().positive().optional(),
307
+ jitter: z.number().nonnegative().optional().describe('Randomness of the backoff; 0 = none. ADK default 1.'),
308
+ exceptions: z
309
+ .array(z.string().regex(ERROR_NAME, 'an error name, such as TypeError or NodeTimeoutError'))
310
+ .min(1)
311
+ .optional()
312
+ .describe('Error names to retry on (the error\'s class or its `name`); every error when absent.'),
263
313
  })
264
314
  .describe('Retry a node on failure (lib/workflow.ts).');
265
315
  const workflowNodeSchema = z
@@ -271,8 +321,8 @@ const workflowNodeSchema = z
271
321
  max_parallel: z.number().int().positive().optional().describe('Concurrency of map. Default 8.'),
272
322
  tool: z.string().min(1).optional().describe('Run this registry tool with the node input as its arguments.'),
273
323
  route_key: z.string().min(1).optional().describe('Property of a JSON output holding the route. Default "route".'),
274
- retry: retrySchema.optional(),
275
- timeout: z.number().positive().optional().describe('Seconds this node may run before it fails.'),
324
+ retry: retrySchema.optional().describe('Retry this node on failure. Not on a map node: each item runs under its agent\'s own retry.'),
325
+ timeout: z.number().positive().optional().describe('Seconds this node may run before it fails. Not on a map node: each item runs under its agent\'s own timeout.'),
276
326
  })
277
327
  .describe('A declared node (exactly one of ask_user, join, map, tool) or modifiers for an agent node (retry, timeout, route_key).');
278
328
  const workflowSchema = z
@@ -479,13 +529,15 @@ function problemsFromIssues(issues, raw) {
479
529
  continue;
480
530
  }
481
531
  if (issue.code === 'too_small' && issue.origin === 'number') {
482
- out.push({ path: p, message: `must be a positive integer (got ${describeValue(got)})` });
532
+ out.push({ path: p, message: `must be ${Number(issue.minimum) === 0 ? '0 or more' : 'a positive integer'} (got ${describeValue(got)})` });
483
533
  continue;
484
534
  }
485
535
  out.push({ path: p, message: issue.message });
486
536
  }
487
537
  return out;
488
538
  }
539
+ /** The generateContentConfig keys `reasoning` replaces (ADR 0047). */
540
+ export const REASONING_OLDER_SPELLING = ['thinkingConfig', 'reasoningEffort'];
489
541
  const isObj = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
490
542
  /** Same normalisation lib/dispatch.ts uses to match a route to a subagent. */
491
543
  const routeNorm = (s) => s.replace(/[^a-z0-9]/gi, '').toLowerCase();
@@ -588,17 +640,45 @@ function crossFieldProblems(raw) {
588
640
  for (const [i, sub] of (Array.isArray(raw.subagents) ? raw.subagents : []).entries())
589
641
  if (isObj(sub))
590
642
  executionProblems(sub, ['subagents', i], false);
643
+ // One spelling per agent (ADR 0047): `reasoning` is mapped onto the very
644
+ // fields the older spelling sets, so the two together would leave which
645
+ // one wins to merge order.
646
+ const reasoningProblems = (agent, path) => {
647
+ if (agent.reasoning === undefined)
648
+ return;
649
+ if (typeof agent.yaml_reference === 'string' || typeof agent.a2a_agent_url === 'string') {
650
+ out.push({ path: [...path, 'reasoning'], message: 'applies to an inline agent; a nested syndicate (yaml_reference) or a remote agent (a2a_agent_url) sets its own' });
651
+ }
652
+ if (!isObj(agent.generateContentConfig))
653
+ return;
654
+ for (const key of REASONING_OLDER_SPELLING) {
655
+ if (agent.generateContentConfig[key] === undefined)
656
+ continue;
657
+ out.push({
658
+ path: [...path, 'reasoning'],
659
+ message: `cannot be combined with generateContentConfig.${key}; reasoning replaces it, so keep one (ADR 0047)`,
660
+ });
661
+ }
662
+ };
663
+ if (isObj(raw.orchestrator))
664
+ reasoningProblems(raw.orchestrator, ['orchestrator']);
665
+ for (const [i, sub] of subs.entries())
666
+ if (isObj(sub))
667
+ reasoningProblems(sub, ['subagents', i]);
591
668
  if (isObj(raw.orchestrator)) {
592
669
  gateProblems(raw.orchestrator, ['orchestrator'], true);
593
670
  skillProblems(raw.orchestrator, ['orchestrator'], true);
594
671
  questionProblems(raw.orchestrator, ['orchestrator'], true);
595
672
  }
596
673
  const dispatching = isObj(raw.dispatch);
674
+ // A workflow's subagents are its nodes, not delegated tools: a gated call pauses its node, and
675
+ // the walk resumes it (ADR 0098). Skill scripts stay refused inside a workflow (workflowProblems).
676
+ const pausing = dispatching || isObj(raw.workflow);
597
677
  subs.forEach((sub, i) => {
598
678
  if (!isObj(sub))
599
679
  return;
600
- gateProblems(sub, ['subagents', i], dispatching);
601
- skillProblems(sub, ['subagents', i], dispatching);
680
+ gateProblems(sub, ['subagents', i], pausing);
681
+ skillProblems(sub, ['subagents', i], pausing);
602
682
  questionProblems(sub, ['subagents', i], dispatching);
603
683
  const hasRef = typeof sub.yaml_reference === 'string';
604
684
  const hasRemote = typeof sub.a2a_agent_url === 'string';
@@ -714,6 +794,15 @@ function workflowProblems(raw, subs) {
714
794
  if (kind !== 'map' && entry.max_parallel !== undefined) {
715
795
  out.push({ path: ['workflow', 'nodes', name, 'max_parallel'], message: 'max_parallel applies to map only' });
716
796
  }
797
+ // A map item runs under its agent's own modifiers, as on ADK (ADR 0089, ADR 0103): the map entry's would be applied by neither runtime.
798
+ if (kind === 'map') {
799
+ for (const key of ['retry', 'timeout']) {
800
+ if (entry[key] === undefined)
801
+ continue;
802
+ const target = entry.map;
803
+ out.push({ path: ['workflow', 'nodes', name, key], message: `${key} on a map node is not applied: each item runs under its agent's own ${key}; set it on nodes.${target}` });
804
+ }
805
+ }
717
806
  }
718
807
  for (const name of agentNames) {
719
808
  if (name.endsWith(ROUTE_STEP_SUFFIX))
@@ -785,8 +874,9 @@ function workflowProblems(raw, subs) {
785
874
  for (const [path, agent] of agents) {
786
875
  if (!isObj(agent))
787
876
  continue;
788
- if (Array.isArray(agent.require_approval) && agent.require_approval.length) {
789
- out.push({ path: [...path, 'require_approval'], message: 'approval gates are not supported inside a workflow yet' });
877
+ // A map item cannot pause: the walk resumes a paused agent node, not an item of a map (ADR 0094, ADR 0098).
878
+ if (typeof agent.name === 'string' && mapped.has(agent.name) && Array.isArray(agent.require_approval) && agent.require_approval.length) {
879
+ out.push({ path: [...path, 'require_approval'], message: 'approval gates are not supported on an agent a map node runs: a map item cannot pause the walk' });
790
880
  }
791
881
  if (isObj(agent.skills) && agent.skills.scripts === 'local') {
792
882
  out.push({ path: [...path, 'skills', 'scripts'], message: 'skill scripts (an approval pause) are not supported inside a workflow yet' });