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.
- package/.env.example +13 -2
- package/CHANGELOG.md +92 -0
- package/DOCUMENTATION.md +23 -1
- package/config/agents/syndicate.schema.json +66 -44
- package/config/agents/syndicateSchema.yaml +11 -6
- package/dist/lib/a2a/app.d.ts +13 -1
- package/dist/lib/a2a/app.js +7 -3
- package/dist/lib/a2a/executor.d.ts +14 -4
- package/dist/lib/a2a/executor.js +61 -8
- package/dist/lib/compile.d.ts +4 -4
- package/dist/lib/compileNative.js +4 -1
- package/dist/lib/envSyndicate.d.ts +35 -0
- package/dist/lib/envSyndicate.js +70 -0
- package/dist/lib/index.d.ts +2 -0
- package/dist/lib/index.js +2 -0
- package/dist/lib/loadSyndicate.d.ts +19 -7
- package/dist/lib/loadSyndicate.js +14 -5
- package/dist/lib/models/attachments.d.ts +42 -0
- package/dist/lib/models/attachments.js +80 -0
- package/dist/lib/models/capabilities.d.ts +2 -0
- package/dist/lib/models/capabilities.js +2 -0
- package/dist/lib/models/chatCompletionsAdapter.js +19 -1
- package/dist/lib/models/claudeAdapter.js +25 -0
- package/dist/lib/models/contextWindow.d.ts +32 -0
- package/dist/lib/models/contextWindow.js +73 -0
- package/dist/lib/models/contract.d.ts +15 -1
- package/dist/lib/models/errorResponse.d.ts +4 -0
- package/dist/lib/models/errorResponse.js +25 -0
- package/dist/lib/models/geminiAdapter.js +9 -0
- package/dist/lib/models/genaiMapping.js +10 -1
- package/dist/lib/models/gptAdapter.d.ts +1 -1
- package/dist/lib/models/gptAdapter.js +3 -0
- package/dist/lib/models/jevAdapter.js +2 -0
- package/dist/lib/observability/tracer.js +13 -2
- package/dist/lib/registry.d.ts +23 -9
- package/dist/lib/registry.js +57 -3
- package/dist/lib/runtime/native/agentLoop.js +25 -3
- package/dist/lib/runtime/native/compaction.d.ts +91 -42
- package/dist/lib/runtime/native/compaction.js +188 -86
- package/dist/lib/runtime/native/history.d.ts +52 -1
- package/dist/lib/runtime/native/history.js +80 -6
- package/dist/lib/runtime/native/recall.d.ts +9 -0
- package/dist/lib/runtime/native/recall.js +42 -0
- package/dist/lib/runtime/native/request.d.ts +8 -2
- package/dist/lib/runtime/native/request.js +30 -9
- package/dist/lib/storage/postgres/index.d.ts +3 -0
- package/dist/lib/storage/postgres/index.js +2 -0
- package/dist/lib/syndicateSchema.d.ts +16 -12
- package/dist/lib/syndicateSchema.js +10 -12
- package/dist/lib/tools/memoryTools.js +3 -0
- package/dist/lib/tools/tool.d.ts +9 -2
- package/dist/lib/tools/toolContract.d.ts +1 -1
- package/dist/lib/tools/toolContract.js +4 -2
- package/dist/lib/tools/webExtractTool.js +4 -0
- package/dist/scripts/a2a_server.js +33 -6
- package/dist/scripts/registry.d.ts +7 -4
- package/dist/scripts/registry.js +17 -5
- 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
|
-
#
|
|
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
|
|
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": "
|
|
1017
|
-
"
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
"
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
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
|
-
|
|
1032
|
-
"description": "The model that writes the summary. Default: the agent's own.",
|
|
1047
|
+
{
|
|
1033
1048
|
"type": "string",
|
|
1034
|
-
"
|
|
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": "
|
|
2133
|
-
"
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
"
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
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
|
-
|
|
2148
|
-
"description": "The model that writes the summary. Default: the agent's own.",
|
|
2174
|
+
{
|
|
2149
2175
|
"type": "string",
|
|
2150
|
-
"
|
|
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]
|
|
372
|
-
#
|
|
373
|
-
#
|
|
374
|
-
#
|
|
375
|
-
#
|
|
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
|
-
#
|
|
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
|
package/dist/lib/a2a/app.d.ts
CHANGED
|
@@ -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
|
package/dist/lib/a2a/app.js
CHANGED
|
@@ -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
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
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). */
|
package/dist/lib/a2a/executor.js
CHANGED
|
@@ -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
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
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'
|
|
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
|
-
|
|
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) {
|
package/dist/lib/compile.d.ts
CHANGED
|
@@ -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 {
|
|
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?:
|
|
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
|
|
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);
|