melchizedek-agents 1.14.0 → 1.15.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 (51) hide show
  1. package/.env.example +9 -1
  2. package/CHANGELOG.md +74 -0
  3. package/DOCUMENTATION.md +22 -0
  4. package/config/agents/syndicate.schema.json +66 -44
  5. package/config/agents/syndicateSchema.yaml +11 -6
  6. package/dist/lib/a2a/app.d.ts +8 -0
  7. package/dist/lib/a2a/app.js +2 -2
  8. package/dist/lib/compile.d.ts +4 -4
  9. package/dist/lib/compileNative.js +4 -1
  10. package/dist/lib/envSyndicate.d.ts +35 -0
  11. package/dist/lib/envSyndicate.js +70 -0
  12. package/dist/lib/loadSyndicate.d.ts +19 -7
  13. package/dist/lib/loadSyndicate.js +14 -5
  14. package/dist/lib/models/capabilities.d.ts +2 -0
  15. package/dist/lib/models/capabilities.js +2 -0
  16. package/dist/lib/models/chatCompletionsAdapter.js +13 -0
  17. package/dist/lib/models/claudeAdapter.js +15 -0
  18. package/dist/lib/models/contextWindow.d.ts +32 -0
  19. package/dist/lib/models/contextWindow.js +73 -0
  20. package/dist/lib/models/contract.d.ts +15 -1
  21. package/dist/lib/models/errorResponse.d.ts +4 -0
  22. package/dist/lib/models/errorResponse.js +25 -0
  23. package/dist/lib/models/geminiAdapter.js +9 -0
  24. package/dist/lib/models/genaiMapping.js +10 -1
  25. package/dist/lib/models/gptAdapter.d.ts +1 -1
  26. package/dist/lib/models/gptAdapter.js +3 -0
  27. package/dist/lib/models/jevAdapter.js +2 -0
  28. package/dist/lib/registry.d.ts +23 -9
  29. package/dist/lib/registry.js +57 -3
  30. package/dist/lib/runtime/native/agentLoop.js +25 -3
  31. package/dist/lib/runtime/native/compaction.d.ts +91 -42
  32. package/dist/lib/runtime/native/compaction.js +188 -86
  33. package/dist/lib/runtime/native/history.d.ts +52 -1
  34. package/dist/lib/runtime/native/history.js +80 -6
  35. package/dist/lib/runtime/native/recall.d.ts +9 -0
  36. package/dist/lib/runtime/native/recall.js +42 -0
  37. package/dist/lib/runtime/native/request.d.ts +8 -2
  38. package/dist/lib/runtime/native/request.js +30 -9
  39. package/dist/lib/storage/postgres/index.d.ts +3 -0
  40. package/dist/lib/storage/postgres/index.js +2 -0
  41. package/dist/lib/syndicateSchema.d.ts +16 -12
  42. package/dist/lib/syndicateSchema.js +10 -12
  43. package/dist/lib/tools/memoryTools.js +3 -0
  44. package/dist/lib/tools/tool.d.ts +9 -2
  45. package/dist/lib/tools/toolContract.d.ts +1 -1
  46. package/dist/lib/tools/toolContract.js +4 -2
  47. package/dist/lib/tools/webExtractTool.js +4 -0
  48. package/dist/scripts/a2a_server.js +28 -4
  49. package/dist/scripts/registry.d.ts +7 -4
  50. package/dist/scripts/registry.js +17 -5
  51. package/package.json +2 -1
package/.env.example CHANGED
@@ -251,9 +251,17 @@ PUBLIC_URL=
251
251
  # byok: each caller's X-API-Key funds its X-Provider. With A2A_AUTH=secret,
252
252
  # byok also scopes data by a hash of that key (the pre-0.16 behaviour).
253
253
  # A2A_KEY_MODE=server
254
- # Bare agent ids that load from the adk_agent_registry table (comma list).
254
+ # Bare agent ids that load from the adk_agent_registry table (comma list),
255
+ # over DATABASE_URL when it is set, else over the Supabase API.
255
256
  # Every other bare id is a file; registry:<id> always means the registry.
256
257
  # A2A_REGISTRY_AGENTS=
258
+ # The whole syndicate file in one variable, base64 (base64 < agent.yaml), for a
259
+ # host that only takes variables (the published image without a database). It
260
+ # is served as MELCHIZEDEK_AGENT_ID (default agent) and as the default, from a
261
+ # private directory that becomes the agents directory, so MELCHIZEDEK_AGENTS_DIR
262
+ # must be unset. Nested yaml_references travel in its bundled_references.
263
+ # MELCHIZEDEK_AGENT_YAML=
264
+ # MELCHIZEDEK_AGENT_ID=agent
257
265
 
258
266
  # ── A2A server: binding, limits, lifecycle (all optional) ─────────────────────
259
267
  # Without A2A_SERVER_SECRET the server binds 127.0.0.1 only. To bind another
package/CHANGELOG.md CHANGED
@@ -4,6 +4,80 @@ Consumers of the package read this file; it records changes to the
4
4
  **published API surface** (the exports map in `package.json`, the bins,
5
5
  the starter pack and the templates), not the repo's full history.
6
6
 
7
+ ## 1.15.0 — 2026-11-02
8
+
9
+ **Breaking — read before upgrading**
10
+
11
+ - **Compaction and masking are on for every agent** (ADR 0152, 0154). A
12
+ `context:` block used to be allowed on delegates only and nothing
13
+ compacted unless it set `compact_after_tokens`. Now every agent compacts
14
+ when a request passes 60% of its model's context window
15
+ (`lib/models/contextWindow.ts` holds the table; an unknown model counts
16
+ as 100k; an Ollama model reads its own context length, default 4,096),
17
+ and a tool result older than the last 6 events and longer than 1,000
18
+ characters goes to the model as a stub it can fetch back with
19
+ `recall_tool_result`. `context: off` on an agent restores the old
20
+ behaviour; `compact_after_tokens: 0` and `mask_over_chars: 0` switch
21
+ the two off separately; `keep_recent_events` and `summary_model` are
22
+ the other keys. A request the provider refuses as too long is retried
23
+ once after a forced compaction.
24
+ - **A `defineTool` result is capped at 20,000 characters** unless the spec
25
+ sets `maxResultChars` (0 lifts the cap). A tool that returned more now
26
+ returns the head and a note saying so.
27
+
28
+ **The prompt prefix is stable** (ADR 0151). Text that changes between an
29
+ agent's requests (`preload_memory`'s recall) leaves the system prompt for
30
+ `ModelRequest.turnNotes`, which every adapter places after the history, so
31
+ a provider's prefix cache covers the tools, the system prompt and the
32
+ history. An `InstructionTool` marked `volatile: true` writes there. The
33
+ Claude adapter sends `cache_control: { type: 'ephemeral' }` on Anthropic's
34
+ own API.
35
+
36
+ **The summary is sectioned and anchored** (ADR 0153): Intent, Stated facts,
37
+ Decisions, Artifacts, Open, Last state, written from the previous summary
38
+ and the new span only, tool calls and results included, within 1,200 words.
39
+ A summary the model fails to write is skipped (the turn goes on with the
40
+ full history, and the span records `compaction.skipped`) instead of
41
+ replacing the history with nothing. A compacted event carries
42
+ `summaryFormat: 'sections/1'`; the summary's own model call carries
43
+ `llm.purpose: compaction` and `llm.compaction.events`.
44
+
45
+ **`CapabilityReport.contextWindow`** says how many tokens a model's context
46
+ holds, when the table knows the model.
47
+
48
+ **The server image is published with every release** (ADR 0149).
49
+ `ghcr.io/jhwadman/melchizedek-serve:<version>` (and `:latest`) for
50
+ linux/amd64 and linux/arm64, with a build-provenance attestation; the
51
+ GitHub Release carries its digest as `melchizedek-serve-v<version>.digest`.
52
+ Pull by that digest.
53
+
54
+ **A syndicate in one environment variable.** `melchizedek-serve` (and
55
+ `startServer` from `melchizedek-agents/server`) reads
56
+ `MELCHIZEDEK_AGENT_YAML`, the whole syndicate file base64-encoded, and
57
+ serves it as `MELCHIZEDEK_AGENT_ID` (default `agent`) and as the default
58
+ syndicate, from a private directory that becomes the agents directory. A
59
+ definition that does not load stops the boot. It refuses
60
+ `MELCHIZEDEK_AGENTS_DIR` beside it, a syndicate argument other than its
61
+ own, and a nested `yaml_reference` its `bundled_references` does not carry.
62
+ Unset, nothing changes.
63
+
64
+ **The `Dockerfile` builds again.** `npm ci` ran the package's own
65
+ `prepare` (the build) before the sources were copied, so `docker build`
66
+ failed; both stages now install with `--ignore-scripts`.
67
+
68
+ **The registry works on any Postgres** (ADR 0150). With `DATABASE_URL`, the
69
+ server loads `registry:<id>` and `A2A_REGISTRY_AGENTS` rows from that
70
+ database (in `MELCHIZEDEK_DB_SCHEMA` when it names one) instead of over the
71
+ Supabase API, and `melchizedek-registry` publishes, rolls back and retires
72
+ there too. Without `DATABASE_URL`, nothing changes. In the library:
73
+ `melchizedek-agents/registry` exports `postgresRegistryReader(pool)` and its
74
+ functions take a supabase-js client or anything with pg's `query` (a
75
+ `pg.Pool`, PGlite); `loadSyndicateFromRegistry(id, { registry })` reads
76
+ through a `RegistryReader`; `postgresStorage` returns `registry`, and
77
+ `createA2AApp` reads `storage.registry` when it is given. A deployment that
78
+ sets both `DATABASE_URL` and Supabase credentials, for different databases,
79
+ now reads the registry from `DATABASE_URL`'s.
80
+
7
81
  ## 1.14.0 — 2026-11-01
8
82
 
9
83
  **Files in a message.** The A2A server takes inline file parts (`raw`, or 0.3's
package/DOCUMENTATION.md CHANGED
@@ -1342,6 +1342,19 @@ local conversation keeps talking to the same remote conversation.
1342
1342
 
1343
1343
  `Dockerfile` builds the compiled server and runs it as a non-root user with
1344
1344
  a health check; `compose.yaml` adds optional Ollama and Phoenix (traces).
1345
+ Every release also publishes it as `ghcr.io/jhwadman/melchizedek-serve:<version>`
1346
+ for linux/amd64 and linux/arm64, with a build-provenance attestation; the
1347
+ release carries the digest (`melchizedek-serve-v<version>.digest`), and a
1348
+ deployment should pull by that digest. The image needs no agents directory:
1349
+ `MELCHIZEDEK_AGENT_YAML` carries the syndicate file base64-encoded and serves
1350
+ it as `MELCHIZEDEK_AGENT_ID` (`agent`), or `A2A_REGISTRY_AGENTS` loads it from
1351
+ the registry.
1352
+
1353
+ ```bash
1354
+ docker run -p 4000:4000 -e A2A_SERVER_SECRET=… -e GOOGLE_GENAI_API_KEY=… \
1355
+ -e MELCHIZEDEK_AGENT_YAML="$(base64 < agent.yaml)" ghcr.io/jhwadman/melchizedek-serve@sha256:…
1356
+ ```
1357
+
1345
1358
  `npx melchizedek-db print|apply|status` installs and checks the database.
1346
1359
  Set `PUBLIC_URL`, `A2A_SERVER_SECRET`, `A2A_AUTH`, `A2A_SERVED_AGENTS`,
1347
1360
  `A2A_TRUST_PROXY` and the provider keys from your secret manager; give the orchestrator's stop timeout at least
@@ -1421,6 +1434,15 @@ npx melchizedek-registry rollback desk 3 --note "v4 misroutes refunds"
1421
1434
  npx melchizedek-registry retire desk --yes # stop serving; history stays
1422
1435
  ```
1423
1436
 
1437
+ The server reads the registry from the database its storage is on: over
1438
+ `DATABASE_URL` when it is set (any Postgres with the migrations, in
1439
+ `MELCHIZEDEK_DB_SCHEMA` when that names a private schema), else over the
1440
+ Supabase API with `SUPABASE_URL` and `SUPABASE_SERVICE_ROLE_KEY`.
1441
+ `melchizedek-registry` reaches it the same way, `DATABASE_URL` first, and
1442
+ the library's functions take either a supabase-js client or a `pg.Pool`
1443
+ (`postgresRegistryReader(pool)` is the read side, which `postgresStorage`
1444
+ supplies as `registry`).
1445
+
1424
1446
  `publish` and `rollback` validate against the same schema the loader uses
1425
1447
  before anything is written. The author recorded is
1426
1448
  `MELCHIZEDEK_REGISTRY_AUTHOR`, else the local user name; a direct write
@@ -1013,31 +1013,42 @@
1013
1013
  "additionalProperties": false
1014
1014
  },
1015
1015
  "context": {
1016
- "description": "Summarize a long conversation instead of overflowing the window. The orchestrator of a delegate syndicate only.",
1017
- "type": "object",
1018
- "properties": {
1019
- "compact_after_tokens": {
1020
- "type": "integer",
1021
- "minimum": 1000,
1022
- "maximum": 9007199254740991,
1023
- "description": "Compact when the last request's prompt passed this many tokens."
1024
- },
1025
- "keep_recent_events": {
1026
- "description": "Events kept verbatim after the summary. Default 6.",
1027
- "type": "integer",
1028
- "minimum": 1,
1029
- "maximum": 100
1016
+ "description": "What the model is sent of a long conversation: older tool results as recallable stubs, and a summary of earlier turns past a share of the window; both on by default. \"off\" sends everything as it is.",
1017
+ "anyOf": [
1018
+ {
1019
+ "type": "object",
1020
+ "properties": {
1021
+ "compact_after_tokens": {
1022
+ "description": "Compact when the last request's prompt passed this many tokens. Default: 60% of the model's context window; 0 never compacts.",
1023
+ "type": "integer",
1024
+ "minimum": 0,
1025
+ "maximum": 9007199254740991
1026
+ },
1027
+ "keep_recent_events": {
1028
+ "description": "Events kept verbatim after the summary, and before masking. Default 6.",
1029
+ "type": "integer",
1030
+ "minimum": 1,
1031
+ "maximum": 100
1032
+ },
1033
+ "summary_model": {
1034
+ "description": "The model that writes the summary. Default: the agent's own.",
1035
+ "type": "string",
1036
+ "minLength": 1
1037
+ },
1038
+ "mask_over_chars": {
1039
+ "description": "An older tool result longer than this many characters is sent as a stub the model can recall. Default 1000; 0 masks none.",
1040
+ "type": "integer",
1041
+ "minimum": 0,
1042
+ "maximum": 9007199254740991
1043
+ }
1044
+ },
1045
+ "additionalProperties": false
1030
1046
  },
1031
- "summary_model": {
1032
- "description": "The model that writes the summary. Default: the agent's own.",
1047
+ {
1033
1048
  "type": "string",
1034
- "minLength": 1
1049
+ "const": "off"
1035
1050
  }
1036
- },
1037
- "required": [
1038
- "compact_after_tokens"
1039
- ],
1040
- "additionalProperties": false
1051
+ ]
1041
1052
  },
1042
1053
  "examples": {
1043
1054
  "description": "Few-shot exchanges (input, output) added to every request's instruction.",
@@ -2129,31 +2140,42 @@
2129
2140
  "additionalProperties": false
2130
2141
  },
2131
2142
  "context": {
2132
- "description": "Summarize a long conversation instead of overflowing the window. The orchestrator of a delegate syndicate only.",
2133
- "type": "object",
2134
- "properties": {
2135
- "compact_after_tokens": {
2136
- "type": "integer",
2137
- "minimum": 1000,
2138
- "maximum": 9007199254740991,
2139
- "description": "Compact when the last request's prompt passed this many tokens."
2140
- },
2141
- "keep_recent_events": {
2142
- "description": "Events kept verbatim after the summary. Default 6.",
2143
- "type": "integer",
2144
- "minimum": 1,
2145
- "maximum": 100
2143
+ "description": "What the model is sent of a long conversation: older tool results as recallable stubs, and a summary of earlier turns past a share of the window; both on by default. \"off\" sends everything as it is.",
2144
+ "anyOf": [
2145
+ {
2146
+ "type": "object",
2147
+ "properties": {
2148
+ "compact_after_tokens": {
2149
+ "description": "Compact when the last request's prompt passed this many tokens. Default: 60% of the model's context window; 0 never compacts.",
2150
+ "type": "integer",
2151
+ "minimum": 0,
2152
+ "maximum": 9007199254740991
2153
+ },
2154
+ "keep_recent_events": {
2155
+ "description": "Events kept verbatim after the summary, and before masking. Default 6.",
2156
+ "type": "integer",
2157
+ "minimum": 1,
2158
+ "maximum": 100
2159
+ },
2160
+ "summary_model": {
2161
+ "description": "The model that writes the summary. Default: the agent's own.",
2162
+ "type": "string",
2163
+ "minLength": 1
2164
+ },
2165
+ "mask_over_chars": {
2166
+ "description": "An older tool result longer than this many characters is sent as a stub the model can recall. Default 1000; 0 masks none.",
2167
+ "type": "integer",
2168
+ "minimum": 0,
2169
+ "maximum": 9007199254740991
2170
+ }
2171
+ },
2172
+ "additionalProperties": false
2146
2173
  },
2147
- "summary_model": {
2148
- "description": "The model that writes the summary. Default: the agent's own.",
2174
+ {
2149
2175
  "type": "string",
2150
- "minLength": 1
2176
+ "const": "off"
2151
2177
  }
2152
- },
2153
- "required": [
2154
- "compact_after_tokens"
2155
- ],
2156
- "additionalProperties": false
2178
+ ]
2157
2179
  },
2158
2180
  "examples": {
2159
2181
  "description": "Few-shot exchanges (input, output) added to every request's instruction.",
@@ -368,14 +368,19 @@ orchestrator:
368
368
  # run_code:
369
369
  # attach: true
370
370
 
371
- # [optional] Summarize a long conversation instead of overflowing the
372
- # window: past `compact_after_tokens` (the last request's prompt), earlier
373
- # turns become one summary and the last `keep_recent_events` stay
374
- # verbatim. The full history stays stored. Orchestrator of a delegate
375
- # syndicate only (ADR 0033).
371
+ # [optional] What the model is sent of a long conversation; both parts are
372
+ # on by default, and `context: off` turns both off. A tool result older
373
+ # than the last `keep_recent_events` events and longer than
374
+ # `mask_over_chars` goes as a stub the model can read back with
375
+ # recall_tool_result (0 masks none). Past `compact_after_tokens` (default
376
+ # 60% of the model's context window; 0 never compacts) the earlier turns
377
+ # become one sectioned summary, and a prompt that overflows the window
378
+ # anyway is compacted and retried once. The full history stays stored
379
+ # either way (ADR 0152, ADR 0153, ADR 0154).
376
380
  # context:
377
- # compact_after_tokens: 60000
381
+ # mask_over_chars: 1000
378
382
  # keep_recent_events: 6
383
+ # compact_after_tokens: 60000
379
384
  # summary_model: "gemini-3.5-flash-lite" # default: this agent's model
380
385
 
381
386
  # [optional] "task" on a WORKFLOW NODE: the agent works with its tools
@@ -46,6 +46,7 @@ import type { TaskStore } from '@a2a-js/sdk/server';
46
46
  import type { CompileOptions } from '../compile.ts';
47
47
  import type { SessionService } from '../runtime/sessions.ts';
48
48
  import type { MemoryService } from '../runtime/memoryService.ts';
49
+ import type { RegistryReader } from '../loadSyndicate.ts';
49
50
  import type { SyndicateYamlConfig } from '../loadSyndicate.ts';
50
51
  import type { ProviderEndpoint, ProviderId } from '../models/registry.ts';
51
52
  import type { TurnLock } from './turnLock.ts';
@@ -128,6 +129,13 @@ export interface A2AAppOptions {
128
129
  * route answers 501.
129
130
  */
130
131
  feedback?: FeedbackStore;
132
+ /**
133
+ * Where registry rows load from (`registry:<id>`, `registryAgents`): a
134
+ * direct connection to the database holding adk_agent_registry.
135
+ * postgresStorage supplies one on its pool. Without it they load over
136
+ * the Supabase API (SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY).
137
+ */
138
+ registry?: RegistryReader;
131
139
  /**
132
140
  * One turn at a time per conversation across instances (postgresStorage
133
141
  * supplies an advisory lock). Default: a lock in this process.
@@ -320,7 +320,7 @@ export async function createA2AApp(options) {
320
320
  };
321
321
  // ── The default syndicate ──────────────────────────────────────────────────
322
322
  const config = checkOAuthHosts(options.defaultSyndicate.startsWith('registry:')
323
- ? await loadSyndicateFromRegistry(options.defaultSyndicate.slice('registry:'.length), { bindings: bindings() })
323
+ ? await loadSyndicateFromRegistry(options.defaultSyndicate.slice('registry:'.length), { bindings: bindings(), ...(options.storage?.registry ? { registry: options.storage.registry } : {}) })
324
324
  : loadSyndicate(options.defaultSyndicate, { bindings: bindings() }), options.defaultSyndicate);
325
325
  // ── Persistence ────────────────────────────────────────────────────────────
326
326
  let durableSessions;
@@ -976,7 +976,7 @@ export async function createA2AApp(options) {
976
976
  const registryId = agentId.startsWith('registry:') ? agentId.slice('registry:'.length) : registryIds.has(agentId) ? agentId : undefined;
977
977
  if (registryId) {
978
978
  try {
979
- const loaded = await loadSyndicateFromRegistry(registryId, { bindings: bindings() });
979
+ const loaded = await loadSyndicateFromRegistry(registryId, { bindings: bindings(), ...(options.storage?.registry ? { registry: options.storage.registry } : {}) });
980
980
  return { config: checkOAuthHosts(loaded, `registry:${registryId}`), source: `registry:${registryId}` };
981
981
  }
982
982
  catch (err) {
@@ -70,8 +70,8 @@ export declare function withReasoning(agent: {
70
70
  generateContentConfig?: object;
71
71
  }, model: string | undefined): Record<string, unknown> | undefined;
72
72
  /** An agent's `context:` block and its default, owned by the compactor (ADR 0033). */
73
- export type { ContextConfig } from './runtime/native/compaction.ts';
74
- import type { ContextConfig } from './runtime/native/compaction.ts';
73
+ export type { ContextConfig, ContextSetting } from './runtime/native/compaction.ts';
74
+ import type { ContextSetting } from './runtime/native/compaction.ts';
75
75
  export { DEFAULT_KEEP_RECENT_EVENTS } from './runtime/native/compaction.ts';
76
76
  /** True when any agent of a syndicate may pause for approval: a gated tool, or skill scripts. */
77
77
  export declare function declaresApprovals(config: SyndicateYamlConfig): boolean;
@@ -202,8 +202,8 @@ export interface AgentSpec {
202
202
  disallowTransferToPeers?: boolean;
203
203
  /** `code_execution: gemini`. */
204
204
  codeExecution?: 'gemini';
205
- /** `context:`: compaction (ADR 0033). */
206
- context?: ContextConfig;
205
+ /** `context:`: masking and compaction (ADR 0033, ADR 0154), or `off`. */
206
+ context?: ContextSetting;
207
207
  /** `mode: task`. */
208
208
  mode?: 'task';
209
209
  /** The syndicate's `max_concurrency`, on its orchestrator (ADR 0116): delegated calls one step runs at once. */
@@ -147,6 +147,9 @@ export function compileNative(spec) {
147
147
  agent.fallbackModel = spec.fallbackModel;
148
148
  if (spec.context)
149
149
  agent.context = spec.context;
150
+ // The YAML's id when the adapter's differs: the context-window table reads it (ADR 0154).
151
+ if (spec.modelId && spec.modelId !== model)
152
+ agent.modelId = spec.modelId;
150
153
  if (spec.maxConcurrency !== undefined)
151
154
  agent.maxConcurrency = spec.maxConcurrency;
152
155
  if (spec.metadata)
@@ -271,7 +274,7 @@ export function nativeAdapterFor(opts = {}, spec) {
271
274
  if (id && !known.has(id))
272
275
  known.set(id, s.resolvedModel);
273
276
  }
274
- for (const id of [s.fallbackModel, s.context?.summary_model]) {
277
+ for (const id of [s.fallbackModel, typeof s.context === 'object' ? s.context.summary_model : undefined]) {
275
278
  if (!id || known.has(id) || !opts.resolveModel)
276
279
  continue;
277
280
  const resolved = opts.resolveModel(id);
@@ -0,0 +1,35 @@
1
+ /**
2
+ * lib/envSyndicate.ts — a syndicate handed to the server in the environment.
3
+ *
4
+ * A container started from the published server image has no agents
5
+ * directory of its own and, without a database, no registry to load from.
6
+ * `MELCHIZEDEK_AGENT_YAML` carries the whole syndicate file, base64-encoded
7
+ * (`base64 < agent.yaml`), so a host that only takes environment variables
8
+ * can still run one. `MELCHIZEDEK_AGENT_ID` names it (default `agent`): the
9
+ * id callers address at `/<id>/` and list in `A2A_SERVED_AGENTS`.
10
+ *
11
+ * The file is written into a fresh private directory (0700, the file 0600)
12
+ * that becomes the agents directory, then loaded once, so a bad definition
13
+ * stops the boot instead of failing the first request. Nested
14
+ * `yaml_reference`s have no files to load from here: each must be carried in
15
+ * the definition's own `bundled_references`, as a registry
16
+ * version carries them, or the boot names the missing ones.
17
+ *
18
+ * Imports only node built-ins and lib/loadSyndicate.ts.
19
+ */
20
+ /** The id a syndicate from the environment is served under unless MELCHIZEDEK_AGENT_ID says otherwise. */
21
+ export declare const ENV_AGENT_ID = "agent";
22
+ export interface EnvSyndicate {
23
+ /** The private directory the file was written to; the server's agents directory. */
24
+ agentsDir: string;
25
+ /** The served id. */
26
+ id: string;
27
+ /** `<id>.yaml`, the default syndicate's file name. */
28
+ file: string;
29
+ }
30
+ /**
31
+ * The syndicate in `MELCHIZEDEK_AGENT_YAML`, written to disk and checked, or
32
+ * undefined when the variable is unset. Throws naming the variable and, for
33
+ * a definition that does not load, the loader's own reason.
34
+ */
35
+ export declare function envSyndicate(env?: NodeJS.ProcessEnv, parent?: string): EnvSyndicate | undefined;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * lib/envSyndicate.ts — a syndicate handed to the server in the environment.
3
+ *
4
+ * A container started from the published server image has no agents
5
+ * directory of its own and, without a database, no registry to load from.
6
+ * `MELCHIZEDEK_AGENT_YAML` carries the whole syndicate file, base64-encoded
7
+ * (`base64 < agent.yaml`), so a host that only takes environment variables
8
+ * can still run one. `MELCHIZEDEK_AGENT_ID` names it (default `agent`): the
9
+ * id callers address at `/<id>/` and list in `A2A_SERVED_AGENTS`.
10
+ *
11
+ * The file is written into a fresh private directory (0700, the file 0600)
12
+ * that becomes the agents directory, then loaded once, so a bad definition
13
+ * stops the boot instead of failing the first request. Nested
14
+ * `yaml_reference`s have no files to load from here: each must be carried in
15
+ * the definition's own `bundled_references`, as a registry
16
+ * version carries them, or the boot names the missing ones.
17
+ *
18
+ * Imports only node built-ins and lib/loadSyndicate.ts.
19
+ */
20
+ import { mkdtempSync, writeFileSync } from 'node:fs';
21
+ import { tmpdir } from 'node:os';
22
+ import { join } from 'node:path';
23
+ import { loadSyndicate } from './loadSyndicate.js';
24
+ /** The id a syndicate from the environment is served under unless MELCHIZEDEK_AGENT_ID says otherwise. */
25
+ export const ENV_AGENT_ID = 'agent';
26
+ /** Same charset as an A2A agent id (lib/a2a/app.ts isValidAgentId), without `:` and `.`, which name registry rows and files. */
27
+ const AGENT_ID = /^[A-Za-z0-9_-]{1,120}$/;
28
+ const BASE64 = /^[A-Za-z0-9+/_-]+={0,2}$/;
29
+ /**
30
+ * The syndicate in `MELCHIZEDEK_AGENT_YAML`, written to disk and checked, or
31
+ * undefined when the variable is unset. Throws naming the variable and, for
32
+ * a definition that does not load, the loader's own reason.
33
+ */
34
+ export function envSyndicate(env = process.env, parent = tmpdir()) {
35
+ const encoded = env.MELCHIZEDEK_AGENT_YAML?.replace(/\s+/g, '');
36
+ if (!encoded)
37
+ return undefined;
38
+ const id = env.MELCHIZEDEK_AGENT_ID?.trim() || ENV_AGENT_ID;
39
+ if (!AGENT_ID.test(id)) {
40
+ throw new Error('MELCHIZEDEK_AGENT_ID must be 1-120 letters, digits, _ or -.');
41
+ }
42
+ if (env.MELCHIZEDEK_AGENTS_DIR?.trim()) {
43
+ throw new Error('MELCHIZEDEK_AGENT_YAML and MELCHIZEDEK_AGENTS_DIR are both set: the syndicate comes from one place. Unset one of them.');
44
+ }
45
+ // Node's base64 decoder takes the URL-safe alphabet too.
46
+ const bytes = BASE64.test(encoded) ? Buffer.from(encoded, 'base64') : undefined;
47
+ if (!bytes?.length) {
48
+ throw new Error('MELCHIZEDEK_AGENT_YAML must be the syndicate file base64-encoded (base64 < agent.yaml), not the YAML itself.');
49
+ }
50
+ const agentsDir = mkdtempSync(join(parent, 'melchizedek-agent-'));
51
+ const file = `${id}.yaml`;
52
+ writeFileSync(join(agentsDir, file), bytes, { mode: 0o600 });
53
+ let config;
54
+ try {
55
+ config = loadSyndicate(file, { agentsDir, shippedFallback: false });
56
+ }
57
+ catch (err) {
58
+ const message = err instanceof Error ? err.message : String(err);
59
+ throw new Error(`MELCHIZEDEK_AGENT_YAML does not load: ${message}`);
60
+ }
61
+ const bundled = config.bundled_references ?? {};
62
+ const missing = (config.subagents ?? [])
63
+ .map((s) => s.yaml_reference)
64
+ .filter((ref) => typeof ref === 'string' && !Object.hasOwn(bundled, ref));
65
+ if (missing.length) {
66
+ throw new Error(`MELCHIZEDEK_AGENT_YAML references nested syndicates it does not carry: ${missing.join(', ')}. `
67
+ + 'Put each in the definition\'s bundled_references, or serve from the registry (A2A_REGISTRY_AGENTS).');
68
+ }
69
+ return { agentsDir, id, file };
70
+ }
@@ -5,7 +5,7 @@ import type { OpenApiConfig } from './tools/openapiTools.ts';
5
5
  import type { McpAuthConfig } from './tools/oauthTools.ts';
6
6
  import type { McpTransportKind } from './tools/mcpTransports.ts';
7
7
  import type { McpServerConfig } from './tools/mcpToolFactory.ts';
8
- import type { ContextConfig, ExampleConfig } from './compile.ts';
8
+ import type { ContextSetting, ExampleConfig } from './compile.ts';
9
9
  export type { OpenApiConfig } from './tools/openapiTools.ts';
10
10
  export type { SkillsConfig } from './tools/skillToolset.ts';
11
11
  import type { WorkflowConfig } from './workflowConfig.ts';
@@ -182,8 +182,8 @@ export interface AgentYamlConfig {
182
182
  run_code?: {
183
183
  attach?: boolean;
184
184
  };
185
- /** Compact a long conversation into a summary past a token threshold (ADR 0033). Delegate orchestrator only. */
186
- context?: ContextConfig;
185
+ /** What the model is sent of a long conversation: masking and compaction, both on by default (ADR 0033, ADR 0152, ADR 0154), or `off`. */
186
+ context?: ContextSetting;
187
187
  /** `task`: the agent works until it calls finish_task; its arguments are the node's output. Workflow nodes only. */
188
188
  mode?: 'task';
189
189
  /** Few-shot exchanges added to every request's instruction (ADK's ExampleTool). */
@@ -415,8 +415,20 @@ export declare function nestedLoader(config: SyndicateYamlConfig, fromFiles?: (r
415
415
  */
416
416
  export declare function validateRegistryConfig(config: Record<string, any>): void;
417
417
  /**
418
- * Loads a syndicate configuration from the Supabase registry.
419
- * @param registryId The document ID in the adk_agent_registry table
420
- * @param options Load options (bindings and overrides)
418
+ * Where registry rows are read from over a direct database connection
419
+ * (DATABASE_URL): `postgresRegistryReader(pool)` in lib/registry.ts, which
420
+ * postgresStorage supplies as `registry`.
421
421
  */
422
- export declare function loadSyndicateFromRegistry(registryId: string, options?: LoadSyndicateOptions): Promise<SyndicateYamlConfig>;
422
+ export interface RegistryReader {
423
+ /** The active definition of `id`, or with `version` that stored version; undefined when there is none. */
424
+ definition(id: string, version?: number): Promise<unknown>;
425
+ }
426
+ /**
427
+ * Loads a syndicate configuration from the registry: through `registry`
428
+ * (a direct Postgres connection) when given, else over the Supabase API.
429
+ * @param registryId The document ID in the adk_agent_registry table; `<id>@<version>` loads one stored version
430
+ * @param options Load options (bindings and overrides), and the reader
431
+ */
432
+ export declare function loadSyndicateFromRegistry(registryId: string, options?: LoadSyndicateOptions & {
433
+ registry?: RegistryReader;
434
+ }): Promise<SyndicateYamlConfig>;
@@ -400,13 +400,22 @@ export function validateRegistryConfig(config) {
400
400
  }
401
401
  }
402
402
  /**
403
- * Loads a syndicate configuration from the Supabase registry.
404
- * @param registryId The document ID in the adk_agent_registry table
405
- * @param options Load options (bindings and overrides)
403
+ * Loads a syndicate configuration from the registry: through `registry`
404
+ * (a direct Postgres connection) when given, else over the Supabase API.
405
+ * @param registryId The document ID in the adk_agent_registry table; `<id>@<version>` loads one stored version
406
+ * @param options Load options (bindings and overrides), and the reader
406
407
  */
407
408
  export async function loadSyndicateFromRegistry(registryId, options = {}) {
409
+ const { registry, ...loadOptions } = options;
410
+ if (registry) {
411
+ const pinnedRef = /^(.+)@(\d+)$/.exec(registryId);
412
+ const definition = pinnedRef ? await registry.definition(pinnedRef[1], Number(pinnedRef[2])) : await registry.definition(registryId);
413
+ if (definition === undefined || definition === null)
414
+ throw new Error(`Registry document ${registryId} not found`);
415
+ return resolveAndValidate(definition, `registry:${registryId}`, loadOptions);
416
+ }
408
417
  if (!hasSupabaseCredentials()) {
409
- throw new Error('Supabase credentials not available for registry loading.');
418
+ throw new Error('No registry to load from: set DATABASE_URL (a direct Postgres connection), or SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY.');
410
419
  }
411
420
  const supabaseUrl = process.env.SUPABASE_URL;
412
421
  const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
@@ -427,5 +436,5 @@ export async function loadSyndicateFromRegistry(registryId, options = {}) {
427
436
  }
428
437
  // The registry row is the live copy of a file (scripts/deploy_agent.ts), so
429
438
  // it answers to the same schema; the label says where the bad copy lives.
430
- return resolveAndValidate(data.yaml_content, `registry:${registryId}`, options);
439
+ return resolveAndValidate(data.yaml_content, `registry:${registryId}`, loadOptions);
431
440
  }
@@ -48,6 +48,8 @@ export interface CapabilityReport {
48
48
  dropped: string[];
49
49
  /** Declared client-side tools; portable across every path. */
50
50
  portable: string[];
51
+ /** The tokens the model's context holds (lib/models/contextWindow.ts); absent for a provider the table does not know. */
52
+ contextWindow?: number;
51
53
  }
52
54
  export declare function describeCapabilities(model: string, tools?: readonly string[], opts?: {
53
55
  callerKey?: boolean;
@@ -21,6 +21,7 @@ import { PROVIDERS } from './providerMap.js';
21
21
  import { gatewaySearch, planTransport } from './gateway.js';
22
22
  import { nativeSearchOn, PLATFORMS_FOR, platformFromEnv } from './endpoints.js';
23
23
  import { claudeGeneration, claudeUrlImagesOn } from './claudeModels.js';
24
+ import { contextWindowOf } from './contextWindow.js';
24
25
  /** The platform a direct path uses (ADR 0023); a misconfigured one reads as direct here, the doctor reports it. */
25
26
  function directPlatform(provider) {
26
27
  try {
@@ -79,6 +80,7 @@ export function describeCapabilities(model, tools = [], opts = {}) {
79
80
  native,
80
81
  dropped,
81
82
  portable,
83
+ ...(contextWindowOf(model) !== undefined ? { contextWindow: contextWindowOf(model) } : {}),
82
84
  };
83
85
  }
84
86
  /**
@@ -896,6 +896,19 @@ export class ChatCompletionsAdapter {
896
896
  return;
897
897
  }
898
898
  });
899
+ // This step's notes, after the history: joined to a last user message's
900
+ // text, or a user message of their own after tool results, so the cached
901
+ // prefix before them holds. Never a trailing system message: some
902
+ // OpenAI-compatible servers take system first only.
903
+ if (request.turnNotes) {
904
+ const last = messages[messages.length - 1];
905
+ if (last?.role === 'user' && typeof last.content === 'string')
906
+ last.content = `${last.content}\n\n${request.turnNotes}`;
907
+ else if (last?.role === 'user' && Array.isArray(last.content))
908
+ last.content.push({ type: 'text', text: request.turnNotes });
909
+ else
910
+ messages.push({ role: 'user', content: request.turnNotes });
911
+ }
899
912
  if (system.length > 0)
900
913
  messages.unshift({ role: 'system', content: system.join('\n\n') });
901
914
  return messages;