melchizedek-agents 1.13.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 (58) hide show
  1. package/.env.example +13 -2
  2. package/CHANGELOG.md +92 -0
  3. package/DOCUMENTATION.md +23 -1
  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 +13 -1
  7. package/dist/lib/a2a/app.js +7 -3
  8. package/dist/lib/a2a/executor.d.ts +14 -4
  9. package/dist/lib/a2a/executor.js +61 -8
  10. package/dist/lib/compile.d.ts +4 -4
  11. package/dist/lib/compileNative.js +4 -1
  12. package/dist/lib/envSyndicate.d.ts +35 -0
  13. package/dist/lib/envSyndicate.js +70 -0
  14. package/dist/lib/index.d.ts +2 -0
  15. package/dist/lib/index.js +2 -0
  16. package/dist/lib/loadSyndicate.d.ts +19 -7
  17. package/dist/lib/loadSyndicate.js +14 -5
  18. package/dist/lib/models/attachments.d.ts +42 -0
  19. package/dist/lib/models/attachments.js +80 -0
  20. package/dist/lib/models/capabilities.d.ts +2 -0
  21. package/dist/lib/models/capabilities.js +2 -0
  22. package/dist/lib/models/chatCompletionsAdapter.js +19 -1
  23. package/dist/lib/models/claudeAdapter.js +25 -0
  24. package/dist/lib/models/contextWindow.d.ts +32 -0
  25. package/dist/lib/models/contextWindow.js +73 -0
  26. package/dist/lib/models/contract.d.ts +15 -1
  27. package/dist/lib/models/errorResponse.d.ts +4 -0
  28. package/dist/lib/models/errorResponse.js +25 -0
  29. package/dist/lib/models/geminiAdapter.js +9 -0
  30. package/dist/lib/models/genaiMapping.js +10 -1
  31. package/dist/lib/models/gptAdapter.d.ts +1 -1
  32. package/dist/lib/models/gptAdapter.js +3 -0
  33. package/dist/lib/models/jevAdapter.js +2 -0
  34. package/dist/lib/observability/tracer.js +13 -2
  35. package/dist/lib/registry.d.ts +23 -9
  36. package/dist/lib/registry.js +57 -3
  37. package/dist/lib/runtime/native/agentLoop.js +25 -3
  38. package/dist/lib/runtime/native/compaction.d.ts +91 -42
  39. package/dist/lib/runtime/native/compaction.js +188 -86
  40. package/dist/lib/runtime/native/history.d.ts +52 -1
  41. package/dist/lib/runtime/native/history.js +80 -6
  42. package/dist/lib/runtime/native/recall.d.ts +9 -0
  43. package/dist/lib/runtime/native/recall.js +42 -0
  44. package/dist/lib/runtime/native/request.d.ts +8 -2
  45. package/dist/lib/runtime/native/request.js +30 -9
  46. package/dist/lib/storage/postgres/index.d.ts +3 -0
  47. package/dist/lib/storage/postgres/index.js +2 -0
  48. package/dist/lib/syndicateSchema.d.ts +16 -12
  49. package/dist/lib/syndicateSchema.js +10 -12
  50. package/dist/lib/tools/memoryTools.js +3 -0
  51. package/dist/lib/tools/tool.d.ts +9 -2
  52. package/dist/lib/tools/toolContract.d.ts +1 -1
  53. package/dist/lib/tools/toolContract.js +4 -2
  54. package/dist/lib/tools/webExtractTool.js +4 -0
  55. package/dist/scripts/a2a_server.js +33 -6
  56. package/dist/scripts/registry.d.ts +7 -4
  57. package/dist/scripts/registry.js +17 -5
  58. 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
@@ -295,7 +303,10 @@ PUBLIC_URL=
295
303
  # OpenAPI tools: the exact environment variables an `auth:` may name. Unset, any
296
304
  # variable but the framework's own settings (this file's) is allowed.
297
305
  # OPENAPI_CREDENTIAL_ENVS=WEATHER_API_KEY,CRM_TOKEN
298
- # A2A_BODY_LIMIT=1mb
306
+ # Inline files in one message, decoded bytes; 0 refuses file parts. A file by URL is always refused.
307
+ # A2A_MAX_FILE_BYTES=10485760
308
+ # Set by default to hold A2A_MAX_FILE_BYTES as base64.
309
+ # A2A_BODY_LIMIT=15mb
299
310
  # How long SIGTERM waits for running tasks before canceling them.
300
311
  # A2A_SHUTDOWN_GRACE_MS=25000
301
312
  # Of that, how long /readyz fails while requests are still served, so a load
package/CHANGELOG.md CHANGED
@@ -4,6 +4,98 @@ 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
+
81
+ ## 1.14.0 — 2026-11-01
82
+
83
+ **Files in a message.** The A2A server takes inline file parts (`raw`, or 0.3's
84
+ `file.bytes`): images and documents the agent's model reads go to it as blobs,
85
+ text types as a text part, and anything else ends `rejected` naming what it
86
+ does read. `A2A_MAX_FILE_BYTES` (default 10 MiB) caps one message's files, and
87
+ 0 restores the old refusal; `A2A_BODY_LIMIT` now defaults to what holds that
88
+ limit as base64. A file by URL is still refused. The agent card's
89
+ `defaultInputModes` lists the types.
90
+
91
+ **`attachmentSupport(model)`** (from the barrel) says which file types a
92
+ model's path takes and how large, for a caller that converts a file first.
93
+
94
+ **Claude reads PDFs.** A `application/pdf` blob is a `document` block; it was
95
+ dropped. The chat-completions adapters no longer label a non-image blob as an
96
+ image, which failed the whole request; it is left out with a warning. A failed
97
+ call's recorded request keeps a file's size, not its bytes.
98
+
7
99
  ## 1.13.0 — 2026-10-24
8
100
 
9
101
  **A Prompt Writer template.** `templates/prompt_writer.yaml` writes
package/DOCUMENTATION.md CHANGED
@@ -1056,7 +1056,7 @@ running". The lock is in-process by default and a Postgres advisory lock
1056
1056
  with `DATABASE_URL`, so it holds across instances. `tasks/cancel` stops a running task, including the model call
1057
1057
  in flight, and the task ends `canceled`. Final states: `completed`,
1058
1058
  `failed` (the message names the stage and the provider's reason),
1059
- `canceled`, `rejected` (a file part, an empty message, or the server at
1059
+ `canceled`, `rejected` (a file its model does not read, a file by URL, an empty message, or the server at
1060
1060
  capacity). Message parts may be `text` or `data` (sent to the model as
1061
1061
  JSON); `file` parts are refused.
1062
1062
 
@@ -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.
@@ -309,8 +317,10 @@ export interface A2AAppOptions {
309
317
  };
310
318
  /** Express `trust proxy` setting (hop count, boolean, or subnet list). */
311
319
  trustProxy?: number | boolean | string;
312
- /** JSON body size limit, e.g. "1mb". */
320
+ /** JSON body size limit, e.g. "1mb". An inline file is base64 in this body: leave room for `maxFileBytes`. */
313
321
  bodyLimit?: string;
322
+ /** Largest total of inline files in one message, decoded bytes. Undefined or 0 refuses file parts. */
323
+ maxFileBytes?: number;
314
324
  /**
315
325
  * Allowlist of agent ids reachable at /:agentId/*. Undefined = any id with
316
326
  * a file in the deployment's own agents directory. Ids listed here may also
@@ -386,6 +396,8 @@ export declare function compileAgentCard(config: SyndicateYamlConfig, opts: {
386
396
  identity?: IdentityScheme;
387
397
  version: string;
388
398
  byok?: boolean;
399
+ /** The server takes file parts: the card names what this agent's model reads. */
400
+ files?: boolean;
389
401
  }): AgentCard;
390
402
  /**
391
403
  * The app name a syndicate's sessions and memory are stored under: its
@@ -73,6 +73,7 @@ import { setCredentialHosts } from '../tools/credentialHosts.js';
73
73
  import { syndicateCredentialHostProblems } from '../tools/credentialUses.js';
74
74
  import { syndicateOAuthHostProblems } from '../tools/oauthTools.js';
75
75
  import { credentialValue } from '../models/credentialSource.js';
76
+ import { attachmentSupport } from '../models/attachments.js';
76
77
  const SURFACE_HEADERS = [
77
78
  ['x-surface', 'name'],
78
79
  ['x-surface-guild', 'guild'],
@@ -183,6 +184,7 @@ const scopeUserBuilder = async () => {
183
184
  */
184
185
  export function compileAgentCard(config, opts) {
185
186
  const base = `${opts.baseUrl.replace(/\/$/, '')}${opts.routePrefix ?? ''}`;
187
+ const fileSupport = opts.files ? attachmentSupport(config.orchestrator?.model) : undefined;
186
188
  const securitySchemes = {};
187
189
  if (opts.byok) {
188
190
  securitySchemes.apiKey = {
@@ -242,7 +244,7 @@ export function compileAgentCard(config, opts) {
242
244
  },
243
245
  securitySchemes,
244
246
  securityRequirements: Object.keys(requirement).length ? [{ schemes: requirement }] : [],
245
- defaultInputModes: ['text/plain', 'application/json'],
247
+ defaultInputModes: ['text/plain', 'application/json', ...(fileSupport ? [...fileSupport.images, ...fileSupport.documents, ...fileSupport.media] : [])],
246
248
  defaultOutputModes: ['text/plain'],
247
249
  skills: (config.subagents ?? []).map((sub) => ({
248
250
  id: sub.name,
@@ -318,7 +320,7 @@ export async function createA2AApp(options) {
318
320
  };
319
321
  // ── The default syndicate ──────────────────────────────────────────────────
320
322
  const config = checkOAuthHosts(options.defaultSyndicate.startsWith('registry:')
321
- ? 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 } : {}) })
322
324
  : loadSyndicate(options.defaultSyndicate, { bindings: bindings() }), options.defaultSyndicate);
323
325
  // ── Persistence ────────────────────────────────────────────────────────────
324
326
  let durableSessions;
@@ -541,6 +543,7 @@ export async function createA2AApp(options) {
541
543
  memoryService: services.memoryService,
542
544
  compileFor,
543
545
  taskTimeoutMs: options.taskTimeoutMs,
546
+ maxFileBytes: options.maxFileBytes,
544
547
  streamText: options.streamText,
545
548
  turnLock,
546
549
  turnLockWaitMs: options.turnLockWaitMs,
@@ -556,6 +559,7 @@ export async function createA2AApp(options) {
556
559
  });
557
560
  const version = executor.configHashFor().slice(0, 12);
558
561
  const cardFor = (base) => compileAgentCard(cfg, {
562
+ files: !!options.maxFileBytes,
559
563
  baseUrl: base,
560
564
  routePrefix,
561
565
  bearer: !!options.serverSecret,
@@ -972,7 +976,7 @@ export async function createA2AApp(options) {
972
976
  const registryId = agentId.startsWith('registry:') ? agentId.slice('registry:'.length) : registryIds.has(agentId) ? agentId : undefined;
973
977
  if (registryId) {
974
978
  try {
975
- const loaded = await loadSyndicateFromRegistry(registryId, { bindings: bindings() });
979
+ const loaded = await loadSyndicateFromRegistry(registryId, { bindings: bindings(), ...(options.storage?.registry ? { registry: options.storage.registry } : {}) });
976
980
  return { config: checkOAuthHosts(loaded, `registry:${registryId}`), source: `registry:${registryId}` };
977
981
  }
978
982
  catch (err) {
@@ -26,6 +26,7 @@ import type { TaskRecord } from '../observability/metrics.ts';
26
26
  import type { AuditSink } from '../observability/audit.ts';
27
27
  import type { Policy } from './policy.ts';
28
28
  import type { Metadata } from '../observability/metadata.ts';
29
+ import type { AttachmentSupport } from '../models/attachments.ts';
29
30
  /** Per-request caller context, set by the server's identity middleware. */
30
31
  export interface A2AContext {
31
32
  /** The caller's provider key in BYOK mode (or from resolveRequest); '' otherwise. */
@@ -108,17 +109,24 @@ export declare function describeTurnError(error: {
108
109
  }): string;
109
110
  /** The failed task's message for a turn result. */
110
111
  export declare function describeFailedTurn(result: SyndicateTurnResult): string;
112
+ /** What this server takes as a file part: the bytes it allows and what the agent's model reads. */
113
+ export interface FilePartPolicy {
114
+ /** Largest total of files in one message, decoded bytes. */
115
+ maxBytes: number;
116
+ support: AttachmentSupport;
117
+ }
111
118
  /**
112
119
  * A2A message parts → model parts. Text passes through; a `data` part
113
120
  * becomes its JSON as text (callers inject structured payloads this way).
114
- * File parts (`url` or `raw` content) are refused with a clear error rather
115
- * than silently blanked: a file URL fetched server-side is an SSRF surface,
116
- * and inline bytes need a size policy this server does not yet have.
121
+ * An inline file (`raw` content, or 0.3's `file.bytes`) becomes a blob part
122
+ * the agent's model reads, or text when it is text, within `files`; anything
123
+ * else is refused with a clear error rather than silently blanked. A file by
124
+ * URL is always refused: fetching one server-side is an SSRF surface.
117
125
  *
118
126
  * Accepts A2A 1.0 parts (`content.$case`) and, for callers that hand in
119
127
  * plain objects, the 0.3 shapes (`kind: 'text' | 'data' | 'file'`).
120
128
  */
121
- export declare function a2aPartsToMessage(rawParts: unknown[]): {
129
+ export declare function a2aPartsToMessage(rawParts: unknown[], files?: FilePartPolicy): {
122
130
  parts: MessagePart[];
123
131
  refused?: string;
124
132
  };
@@ -132,6 +140,8 @@ export interface ExecutorOptions {
132
140
  compileFor: (ctx: A2AContext) => CompileOptions;
133
141
  /** Wall-clock budget per task in ms; 0 or undefined = none. */
134
142
  taskTimeoutMs?: number;
143
+ /** Largest total of inline files in one message, decoded bytes; 0 or undefined refuses file parts. */
144
+ maxFileBytes?: number;
135
145
  /** Shared across every executor on the server. */
136
146
  limiter: TaskLimiter;
137
147
  /** The agent id this executor serves ('' for the default syndicate). */
@@ -27,6 +27,7 @@ import { isWorkflowSyndicate } from '../workflow.js';
27
27
  import { describeInput } from '../runtime/syndicateTurn.js';
28
28
  import { describeConsent } from '../runtime/credentials.js';
29
29
  import { MAX_META_KEYS, metaAttributes, requestMetadata } from '../observability/metadata.js';
30
+ import { attachmentSupport, takesBlob } from '../models/attachments.js';
30
31
  export const requestContextStorage = new AsyncLocalStorage();
31
32
  /** The app name every A2A session and memory silo is stored under. */
32
33
  export const A2A_APP_NAME = 'melchizedek-a2a';
@@ -143,19 +144,62 @@ export function describeFailedTurn(result) {
143
144
  }
144
145
  return `Error: [${error.code}] The agent run failed — ${describeTurnError(error)}`;
145
146
  }
147
+ /** Types read as text whatever the model: they go to it as a text part. */
148
+ const TEXT_FILE = /^(text\/[\w.+-]+|application\/(json|xml|yaml|x-yaml|x-ndjson|csv))$/;
149
+ /** A file's name as the model reads it: printable and short, since the caller chose it. */
150
+ const fileLabel = (name) => (typeof name === 'string' ? name : '').replace(/[^\p{L}\p{N} ._()-]/gu, '_').slice(0, 120) || 'file';
151
+ /** One inline file as model parts, or why it is refused. */
152
+ function filePart(bytes, declared, name, policy) {
153
+ const type = String(declared ?? '').split(';')[0].trim().toLowerCase();
154
+ const shown = type.replace(/[^\x20-\x7e]/g, '?').slice(0, 64) || 'a file with no type';
155
+ if (TEXT_FILE.test(type)) {
156
+ let text;
157
+ try {
158
+ text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
159
+ }
160
+ catch {
161
+ return `The file part declared as ${shown} is not UTF-8 text.`;
162
+ }
163
+ return { text: `<attached_file name="${fileLabel(name)}" type="${shown}">\n${text}\n</attached_file>` };
164
+ }
165
+ if (!takesBlob(policy.support, type))
166
+ return `This agent's model does not read ${shown}. It reads ${[...policy.support.images, ...policy.support.documents, ...policy.support.media].join(', ')} and text.`;
167
+ const limit = policy.support.images.includes(type) ? policy.support.maxImageBytes : policy.support.maxFileBytes;
168
+ if (bytes.length > limit)
169
+ return `A ${shown} file may be at most ${limit} bytes for this agent's model; this one is ${bytes.length}.`;
170
+ return { inlineData: { mimeType: type, data: bytes.toString('base64') } };
171
+ }
146
172
  /**
147
173
  * A2A message parts → model parts. Text passes through; a `data` part
148
174
  * becomes its JSON as text (callers inject structured payloads this way).
149
- * File parts (`url` or `raw` content) are refused with a clear error rather
150
- * than silently blanked: a file URL fetched server-side is an SSRF surface,
151
- * and inline bytes need a size policy this server does not yet have.
175
+ * An inline file (`raw` content, or 0.3's `file.bytes`) becomes a blob part
176
+ * the agent's model reads, or text when it is text, within `files`; anything
177
+ * else is refused with a clear error rather than silently blanked. A file by
178
+ * URL is always refused: fetching one server-side is an SSRF surface.
152
179
  *
153
180
  * Accepts A2A 1.0 parts (`content.$case`) and, for callers that hand in
154
181
  * plain objects, the 0.3 shapes (`kind: 'text' | 'data' | 'file'`).
155
182
  */
156
- export function a2aPartsToMessage(rawParts) {
183
+ export function a2aPartsToMessage(rawParts, files) {
157
184
  const parts = [];
158
- const refusal = 'File parts are not supported by this server; send text or data parts.';
185
+ const refusal = files ? 'A file part given by URL is not fetched by this server; send its bytes.' : 'File parts are not supported by this server; send text or data parts.';
186
+ let total = 0;
187
+ /** Adds one inline file; a string is why the message is refused. */
188
+ const addFile = (value, declared, name) => {
189
+ if (!files)
190
+ return refusal;
191
+ const bytes = typeof value === 'string' ? Buffer.from(value, 'base64') : value instanceof Uint8Array ? Buffer.from(value) : null;
192
+ if (!bytes || bytes.length === 0)
193
+ return 'A file part has no bytes.';
194
+ total += bytes.length;
195
+ if (total > files.maxBytes)
196
+ return `The files in one message may total at most ${files.maxBytes} bytes on this server.`;
197
+ const part = filePart(bytes, declared, name, files);
198
+ if (typeof part === 'string')
199
+ return part;
200
+ parts.push(part);
201
+ return undefined;
202
+ };
159
203
  for (const raw of rawParts) {
160
204
  if (typeof raw === 'string') {
161
205
  parts.push({ text: raw });
@@ -168,8 +212,13 @@ export function a2aPartsToMessage(rawParts) {
168
212
  parts.push({ text: content.value });
169
213
  else if (content.$case === 'data')
170
214
  parts.push({ text: JSON.stringify(content.value) });
171
- else if (content.$case === 'url' || content.$case === 'raw')
215
+ else if (content.$case === 'url')
172
216
  return { parts, refused: refusal };
217
+ else if (content.$case === 'raw') {
218
+ const refused = addFile(content.value, p.mediaType, p.filename);
219
+ if (refused)
220
+ return { parts, refused };
221
+ }
173
222
  continue;
174
223
  }
175
224
  const kind = p.kind ?? (p.text !== undefined ? 'text' : p.data !== undefined ? 'data' : p.file !== undefined ? 'file' : undefined);
@@ -181,7 +230,11 @@ export function a2aPartsToMessage(rawParts) {
181
230
  parts.push({ text: JSON.stringify(p.data) });
182
231
  }
183
232
  else if (kind === 'file') {
184
- return { parts, refused: refusal };
233
+ if (p.file?.bytes === undefined)
234
+ return { parts, refused: refusal };
235
+ const refused = addFile(p.file.bytes, p.file.mimeType, p.file.name);
236
+ if (refused)
237
+ return { parts, refused };
185
238
  }
186
239
  }
187
240
  return { parts };
@@ -606,7 +659,7 @@ export class SyndicateExecutor {
606
659
  throw new Error('No authentication context available.');
607
660
  ctxForReport = ctx;
608
661
  const rawParts = message?.parts ?? message?.content ?? [];
609
- const mapped = a2aPartsToMessage(rawParts);
662
+ const mapped = a2aPartsToMessage(rawParts, this.opts.maxFileBytes ? { maxBytes: this.opts.maxFileBytes, support: attachmentSupport(this.opts.config.orchestrator?.model, { callerKey: !!ctx.apiKey }) } : undefined);
610
663
  const refused = mapped.refused;
611
664
  let parts = mapped.parts;
612
665
  if (refused) {
@@ -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);