You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
update is destructive: PATCH replaces config and tools wholesale #14
PATCH /agents/{id} does not merge object fields. Probed on 2026-08-26 with a throwaway draft agent (created, probed, deleted — no existing agent touched), because the OpenAPI spec is reportedly out of sync and the docs don't state the semantics.
Replaced wholesale — anything the payload omits is destroyed:
Field
Probe result
config
4 keys → 1; lost enableAlgoliaMcp, feedback, max_iterations
tools array
2 tools → 1; lost algolia_display_results
Preserved even when omitted: index-level enhancedDescription, searchControls, searchParameters; tool-level allowUnlistedIndices, mode.
Why this makes update destructive
cmd_update builds its payload as:
"tools": [tool] — a single algolia_search_index, since build_tool() cannot express any other tool type
config.get("config") or current.get("config") — the local config block replaces the server's
So algolia-agent update against an agent with a second tool, or with a fuller config than the local file, silently deletes the difference.
This is not hypothetical. Of 14 agents on the account, 7 carry algolia_display_results and all are published. (Corrected from an earlier count of 8-of-9, which was based on a truncated agent list — see #15.) A dry-run against one shows:
tools:
- algolia_display_results
config:
- enableAlgoliaMcp: True (will be removed)
- entryPoints: {...} (will be removed)
- feedback: {"enabled": true} (will be removed)
- max_iterations: 25 (will be removed)
- max_tokens: 0 (will be removed)
- thread_depth: {"max_messages": 0} (will be removed)
Scope
#13 makes this loss visible in --dry-run. It does not prevent it. Preventing it needs a behavior decision:
Merge instead of replace. Start from current and overlay only what the config file specifies, for both config and tools. Preserves unmodeled server state by default; means the CLI can never intentionally remove a tool or config key.
Preserve unrecognized, replace recognized. Carry forward tool types and config keys the CLI has no model for; fully own the ones it does.
Require explicit opt-in to destroy. Keep replace semantics but refuse to send a payload that drops anything unless --allow-remove (or similar) is passed.
Option 3 is probably the smallest safe step, and composes with the dry-run output already in #13.
Related
predefinedSearchParameters (added in #5) appears on zero live agents at tool level, while every index entry carries a field named searchParameters. Possible wrong name/level, i.e. a silent no-op. Worth a separate look.
max_tokens, max_iterations and thread_depth.max_messages are the agent's runtime limits. memory is a config key not present on any previously inspected agent, so this list should not be assumed complete.
A dry-run of the reference config against that agent:
config:
- enableAlgoliaMcp: True (will be removed)
- entryPoints: {...} (will be removed)
- feedback: {"enabled": true} (will be removed)
- max_iterations: 15 (will be removed)
- max_tokens: 3000 (will be removed)
- memory: {"enabled": true} (will be removed)
- thread_depth: {"max_messages": 10} (will be removed)
All seven, including every limit.
Why this raises the priority
The CLI has no model for any config key — it forwards the local file's block verbatim and nothing else. Combined with PATCH's replace semantics, the practical situation is:
these settings can only be created in the dashboard
the CLI's only possible effect on them is deletion
Any user who sets limits in the dashboard and later runs algolia-agent update silently loses them. That argues for option 1 or 2 (merge / preserve-unrecognized) over option 3 (--allow-remove), since a flag still defaults an unaware user into data loss on their first update.
An interim mitigation independent of the option chosen: have examples/tcg/agent-config.json carry the full config block instead of only suggestions, so the reference config is not itself a footgun.
build_tool() emits exactly ['indices', 'name', 'type'], so none of the three appear in an update payload. The dry-run in #13 does not flag mode or allowUnlistedIndices either, because it compares only fields present in the outgoing payload — correct for suppressing API-expansion noise, wrong for fields with replace semantics.
Combined with the dashboard shipping ahead of both the CLI and the OpenAPI spec (which the team reports is out of sync), the pattern generalizes: each new dashboard option arrives as a field the CLI cannot express and will destroy, and stays invisible until someone patches build_tool().enhancedDescription, config.memory, and config.widgetType all appeared the same way.
Consequence for the options above
Option 3 (--allow-remove) should be dropped. A confirmation flag only guards against losses the tool can enumerate, and the defining property of this problem is that the next loss is one nobody has modelled yet. It also defaults an unaware user into data loss on their first update.
Recommend option 1 or 2 — preserve by default. Build the payload by starting from the GET response and overlaying only what the config file specifies, for tools, tool-level fields, and config alike. Properties:
a new dashboard option survives an update from a CLI that has never heard of it
no ongoing maintenance obligation to chase the dashboard
the cost is that removing something requires an explicit mechanism, which is the safer direction for the failure to lie
Still open
Whether omitted tool-level fields are preserved or reset to defaults. The earlier probe could not distinguish these: it used mode: "static" / allowUnlistedIndices: false, which are the defaults, so "unchanged after omission" was ambiguous. Now that non-default values exist, a throwaway-agent probe (create with mode: "dynamic", PATCH omitting it, read back, delete) would settle it.
If they revert, update silently turns dynamic mode off — and that belongs in this issue's summary.
Correction: index-level searchControls is destroyed, not preserved
An earlier comment in this issue and in #13 stated that index-level searchControls survives omission from the payload. That was wrong. The first probe checked only whether the searchControlskey was present after the PATCH — the key survived carrying a null value, and key presence was misread as preservation.
A second probe using non-default values settles it. Created a throwaway draft with mode: "dynamic", allowUnlistedIndices: true, searchControls on the primary index only, and an algolia_display_results tool; then PATCHed exactly what build_tool() emits (['indices', 'name', 'type']).
Field
Before
After
mode
"dynamic"
"static"
reverted to default
allowUnlistedIndices
true
false
reverted to default
primary searchControls
set
null
wiped
algolia_display_results
present
gone
deleted
config (sent in full)
5 keys
5 keys
preserved
index enhancedDescription
set
set
regenerated by platform
tool description
set
set
regenerated by platform
So the destroyed set is larger than this issue originally recorded, and it includes the search-tool configuration itself.
Known tool-field defaults:mode = "static", allowUnlistedIndices = false. A field already at its default loses nothing by being omitted, which matters for distinguishing real losses from noise.
This strengthens the case for preserve-by-default over a confirmation flag: the destroyed set grew twice during investigation, and both times the tooling had been silent about the addition.
Supersedes the three options in the issue body. Probing settled the design questions that made those options hard to choose between.
Findings that shape the design
Probe 3 (throwaway draft agent, created and deleted):
Question
Result
Is the GET response a valid PATCH body, verbatim?
Yes, and the agent round-trips to identical state
Read-only fields (id, createdAt, updatedAt, lastUsedAt) in the body
Accepted and ignored, not rejected
description, systemPrompt, templateType
All writable — not read-only, just unexpressible in the config model
enhancedDescription
Ignored / regenerated — effectively read-only
Two consequences:
No defaults registry is needed. Omitting a field already at its default is a no-op (verified: mode: "static" omitted stays "static"). Loss only occurs when a non-default value is omitted. So writing back everything you read is sufficient for correctness, and the server remains the single authority on defaults — nothing to hand-maintain, nothing to go stale.
The config model does not need to be a designed schema. It can be the agent JSON. Any field the dashboard ships in future is covered with no code change.
This also removes the need for the merge layer, the null-means-remove convention, and the deep-vs-shallow merge decision. Removal becomes "delete the key from the file."
provisioning many agents from one spec — the TCG workflow
native (new)
the agent JSON as returned by GET
faithfully managing one agent
Detected by the presence of a tools array. Both remain supported; the friendly format stays exactly as it is, so nothing breaks and templated provisioning keeps working.
Writes the GET response minus cosmetic fields (id, createdAt, updatedAt, lastUsedAt, enhancedDescription). Those are excluded for tidiness, not correctness — the API ignores them either way, which means a mistake in that list fails loudly (phantom diff) rather than silently.
Guard on legacy configs
update must refuse to send a payload that would drop non-null server-side state, pointing at snapshot as the remedy, with --force to override. _diff() already computes exactly this after #13 — the removal lines it now emits ((will be removed), → none (will be removed), → default (not sent)) are the signal. The guard must gate update itself, not only --dry-run, since the destructive path is the one without the flag.
Phases
1 — Guard. Refuse destructive updates unless --force. Small, independent, stops the bleeding immediately regardless of what follows. Ships on its own.
2 — snapshot command. Write the native format. Acceptance criterion, and the reason this design is self-verifying:
algolia-agent snapshot <id> -o snap.json
algolia-agent update <id> --config snap.json --dry-run # must print "No changes detected."
Any field the writer misses shows up immediately as a phantom diff. Run it against 47d6a15c (dynamic mode, per-index searchControls, display-results tool) and one two-tool TCG agent.
3 — Native format in update / create. Send the native payload as-is; skip build_tool(), skip provider-name resolution (providerId is already in the file). The friendly path is untouched.
4 — Retire the guard's reason for existing. With snapshot available, the destructive case is only reachable from a hand-written partial config, which is then an explicit choice plus --force.
predefinedSearchParameters — phase 2 resolves this incidentally: a snapshot of any real agent will show whether the field exists on the server at all. It appears on zero of 14 agents at tool level, while index entries carry searchParameters.
Risks
Drift. Snapshot, someone edits in the dashboard, then update reverts them. Correct for a declarative model but surprising; the honest dry-run from fix(diff): eliminate dry-run false negatives and false positives #13 makes it visible before it happens. Worth a line in the README.
Verbosity. A full snapshot carries all 8 searchControls parameters, 9 config keys and 5 display-results params. Verbose and correct beats terse and lossy; a --minimal variant can come later if anyone wants it.
Two formats to maintain. Justified because they serve genuinely different jobs, and the friendly path needs no changes.
create accepts a native config, making a snapshot a complete clone source
Phase 4 needed no code — with snapshot available, the destructive path requires a hand-written partial config plus --force.
Correction to this issue's premise
The three options originally listed here — merge, preserve-unrecognised, or a confirmation flag — were all superseded once probing established that the API accepts its own GET response as a PATCH body. That removed the need for a defaults registry, a merge layer, and a null-means-remove convention in one go: nothing has to be reconstructed, so nothing can be left out, and removal is deleting a key from the file. The server stays the only authority on its own defaults.
Credit where due: the snapshot approach came from review, not from this issue's analysis. It is a better design than what was proposed here, primarily because it is self-verifying — snapshot → update --dry-run → "No changes detected" proves the config model covers everything the service stores. Verified clean for all 12 agents on the account.
That oracle earned its place immediately: the first sweep failed on 3 of 14 agents and found a real bug — native configs were being template-rendered, which broke every agent carrying literal {{...}} in its stored prompt. Agent Studio's own templates ship such placeholders, so that was a whole class of agent, not an edge case. No amount of code reading would have surfaced it.
Scope of the guard — narrower than earlier comments implied
Worth stating precisely, because an earlier comment and #20's description were looser than the truth:
The guard blocks deletion of state the config model cannot express: a dropped tool, a reverted mode/allowUnlistedIndices, wiped per-index searchControls, removed config keys.
It does not block a partial config from overwriting fields it does specify. A config naming only index and instructions will still replace those values; --dry-run reports it, nothing refuses it.
That distinction is deliberate — silently dropping a tool you never mentioned is different from changing a value you did write — but "update is now safe against partial configs" would be an overstatement. Confirmed the hard way: a partial config applied to a live agent replaced its instructions and index description without triggering the guard, correctly, because those were changes rather than removals. Restored from a snapshot taken moments earlier, which is itself the clearest argument for snapshot existing.
Established about the API along the way
None of this is in the docs; all of it was probed with throwaway agents, created and deleted.
PATCH replacesconfig, the tools array, and per-index searchControls; omitted tool fields revert to their defaults (mode → "static", allowUnlistedIndices → false)
Preserved when omitted: description, systemPrompt, templateType; enhancedDescription and the tool-level description are regenerated by the platform
Read-only fields (id, createdAt, updatedAt, lastUsedAt) are accepted and ignored, not rejected — so the snapshot exclusion list is tidiness, not correctness
templateType is an unvalidated provenance label, not a selector: any string is accepted, setting it has no side effects, and there is no /templates endpoint
description, systemPrompt and templateType are all writable
predefinedSearchParameters still appears on zero agents at tool level while index entries carry searchParameters. Snapshotting any real agent is now the cheap way to confirm it is dead code.
merge semantics, and the query/custom search controls, remain unobserved.
Activity
chuckmeyer commentedon Aug 26, 2026
The destroyed
configkeys are the agent's guardrailsInspected a dashboard-created agent (
47d6a15c) with limits set to real values rather than defaults:{ "suggestions": {"enabled": true}, "feedback": {"enabled": true}, "memory": {"enabled": true}, "entryPoints": {"searchBar": {"aiModeEnabled": true, "promptSuggestions": {"sourceIndex": "tcg_cards_msft-build-2026"}}}, "max_tokens": 3000, "max_iterations": 15, "thread_depth": {"max_messages": 10}, "enableAlgoliaMcp": true }max_tokens,max_iterationsandthread_depth.max_messagesare the agent's runtime limits.memoryis a config key not present on any previously inspected agent, so this list should not be assumed complete.A dry-run of the reference config against that agent:
All seven, including every limit.
Why this raises the priority
The CLI has no model for any
configkey — it forwards the local file's block verbatim and nothing else. Combined with PATCH's replace semantics, the practical situation is:Any user who sets limits in the dashboard and later runs
algolia-agent updatesilently loses them. That argues for option 1 or 2 (merge / preserve-unrecognized) over option 3 (--allow-remove), since a flag still defaults an unaware user into data loss on their first update.An interim mitigation independent of the option chosen: have
examples/tcg/agent-config.jsoncarry the fullconfigblock instead of onlysuggestions, so the reference config is not itself a footgun.chuckmeyer commentedon Aug 26, 2026
This is ongoing schema drift, not a fixed set of gaps
Three settings were configured on a test agent (
47d6a15c) and are reported to be new dashboard options:tools[].mode: "dynamic""static"tools[].allowUnlistedIndices: truefalsealgolia_display_resultstool, non-default params (isTerminal: false,maxGroups: 3,maxResultsPerGroup: 6,minGroups: 1,minResultsPerGroup: 3)build_tool()emits exactly['indices', 'name', 'type'], so none of the three appear in an update payload. The dry-run in #13 does not flagmodeorallowUnlistedIndiceseither, because it compares only fields present in the outgoing payload — correct for suppressing API-expansion noise, wrong for fields with replace semantics.Combined with the dashboard shipping ahead of both the CLI and the OpenAPI spec (which the team reports is out of sync), the pattern generalizes: each new dashboard option arrives as a field the CLI cannot express and will destroy, and stays invisible until someone patches
build_tool().enhancedDescription,config.memory, andconfig.widgetTypeall appeared the same way.Consequence for the options above
Option 3 (
--allow-remove) should be dropped. A confirmation flag only guards against losses the tool can enumerate, and the defining property of this problem is that the next loss is one nobody has modelled yet. It also defaults an unaware user into data loss on their first update.Recommend option 1 or 2 — preserve by default. Build the payload by starting from the
GETresponse and overlaying only what the config file specifies, fortools, tool-level fields, andconfigalike. Properties:Still open
Whether omitted tool-level fields are preserved or reset to defaults. The earlier probe could not distinguish these: it used
mode: "static"/allowUnlistedIndices: false, which are the defaults, so "unchanged after omission" was ambiguous. Now that non-default values exist, a throwaway-agent probe (create withmode: "dynamic", PATCH omitting it, read back, delete) would settle it.If they revert,
updatesilently turns dynamic mode off — and that belongs in this issue's summary.chuckmeyer commentedon Aug 26, 2026
Correction: index-level
searchControlsis destroyed, not preservedAn earlier comment in this issue and in #13 stated that index-level
searchControlssurvives omission from the payload. That was wrong. The first probe checked only whether thesearchControlskey was present after the PATCH — the key survived carrying anullvalue, and key presence was misread as preservation.A second probe using non-default values settles it. Created a throwaway draft with
mode: "dynamic",allowUnlistedIndices: true,searchControlson the primary index only, and analgolia_display_resultstool; then PATCHed exactly whatbuild_tool()emits (['indices', 'name', 'type']).mode"dynamic""static"allowUnlistedIndicestruefalsesearchControlsnullalgolia_display_resultsconfig(sent in full)enhancedDescriptiondescriptionSo the destroyed set is larger than this issue originally recorded, and it includes the search-tool configuration itself.
Two useful side results:
searchControlsis accepted on create. The replica stayednullwhile the primary kept its controls, so the fix proposed in config model cannot express per-index searchControls (or the full searchControls schema) #16 is viable against the real API.mode="static",allowUnlistedIndices=false. A field already at its default loses nothing by being omitted, which matters for distinguishing real losses from noise.Effect on #13
Two false negatives, both in the destructive direction, are fixed in
ffbf9ee:new_has_scgate skippedsearchControlscomparison whenever the payload had nosearchControlskey — precisely when the update wipes themmode/allowUnlistedIndiceswas silentThe live dry-run against a dynamic-mode agent now reports all of it:
This strengthens the case for preserve-by-default over a confirmation flag: the destroyed set grew twice during investigation, and both times the tooling had been silent about the addition.
chuckmeyer commentedon Aug 27, 2026
Plan: snapshot-based configuration
Supersedes the three options in the issue body. Probing settled the design questions that made those options hard to choose between.
Findings that shape the design
Probe 3 (throwaway draft agent, created and deleted):
id,createdAt,updatedAt,lastUsedAt) in the bodydescription,systemPrompt,templateTypeenhancedDescriptionTwo consequences:
mode: "static"omitted stays"static"). Loss only occurs when a non-default value is omitted. So writing back everything you read is sufficient for correctness, and the server remains the single authority on defaults — nothing to hand-maintain, nothing to go stale.This also removes the need for the merge layer, the
null-means-remove convention, and the deep-vs-shallow merge decision. Removal becomes "delete the key from the file."Design
Two config formats
index,replicas,providername,instructionsfile path,{{vars}}Detected by the presence of a
toolsarray. Both remain supported; the friendly format stays exactly as it is, so nothing breaks and templated provisioning keeps working.New command
Writes the GET response minus cosmetic fields (
id,createdAt,updatedAt,lastUsedAt,enhancedDescription). Those are excluded for tidiness, not correctness — the API ignores them either way, which means a mistake in that list fails loudly (phantom diff) rather than silently.Guard on legacy configs
updatemust refuse to send a payload that would drop non-null server-side state, pointing atsnapshotas the remedy, with--forceto override._diff()already computes exactly this after #13 — the removal lines it now emits ((will be removed),→ none (will be removed),→ default (not sent)) are the signal. The guard must gateupdateitself, not only--dry-run, since the destructive path is the one without the flag.Phases
1 — Guard. Refuse destructive updates unless
--force. Small, independent, stops the bleeding immediately regardless of what follows. Ships on its own.2 —
snapshotcommand. Write the native format. Acceptance criterion, and the reason this design is self-verifying:Any field the writer misses shows up immediately as a phantom diff. Run it against
47d6a15c(dynamic mode, per-index searchControls, display-results tool) and one two-tool TCG agent.3 — Native format in
update/create. Send the native payload as-is; skipbuild_tool(), skip provider-name resolution (providerIdis already in the file). The friendly path is untouched.4 — Retire the guard's reason for existing. With snapshot available, the destructive case is only reachable from a hand-written partial config, which is then an explicit choice plus
--force.Out of scope, tracked elsewhere
searchControlsin the friendly format. The native format gets it for free, so config model cannot express per-index searchControls (or the full searchControls schema) #16 shrinks to "friendly format parity" and the README schema docs.predefinedSearchParameters— phase 2 resolves this incidentally: a snapshot of any real agent will show whether the field exists on the server at all. It appears on zero of 14 agents at tool level, while index entries carrysearchParameters.Risks
updatereverts them. Correct for a declarative model but surprising; the honest dry-run from fix(diff): eliminate dry-run false negatives and false positives #13 makes it visible before it happens. Worth a line in the README.searchControlsparameters, 9configkeys and 5 display-results params. Verbose and correct beats terse and lossy; a--minimalvariant can come later if anyone wants it.feat(update): refuse updates that would delete existing configuration (…
feat(snapshot): write a full config from an agent's current state (#19)
feat(create): accept a native config, making snapshots a clone source (…
chuckmeyer commentedon Aug 27, 2026
Closing: phases 1–3 shipped
updaterefuses to delete configuration the config model cannot express, unless--forcesnapshot <agent_id>writes the API's own representation;updatesends a native config back verbatimcreateaccepts a native config, making a snapshot a complete clone sourcePhase 4 needed no code — with
snapshotavailable, the destructive path requires a hand-written partial config plus--force.Correction to this issue's premise
The three options originally listed here — merge, preserve-unrecognised, or a confirmation flag — were all superseded once probing established that the API accepts its own GET response as a PATCH body. That removed the need for a defaults registry, a merge layer, and a
null-means-remove convention in one go: nothing has to be reconstructed, so nothing can be left out, and removal is deleting a key from the file. The server stays the only authority on its own defaults.Credit where due: the snapshot approach came from review, not from this issue's analysis. It is a better design than what was proposed here, primarily because it is self-verifying —
snapshot→update --dry-run→ "No changes detected" proves the config model covers everything the service stores. Verified clean for all 12 agents on the account.That oracle earned its place immediately: the first sweep failed on 3 of 14 agents and found a real bug — native configs were being template-rendered, which broke every agent carrying literal
{{...}}in its stored prompt. Agent Studio's own templates ship such placeholders, so that was a whole class of agent, not an edge case. No amount of code reading would have surfaced it.Scope of the guard — narrower than earlier comments implied
Worth stating precisely, because an earlier comment and #20's description were looser than the truth:
mode/allowUnlistedIndices, wiped per-indexsearchControls, removedconfigkeys.indexandinstructionswill still replace those values;--dry-runreports it, nothing refuses it.That distinction is deliberate — silently dropping a tool you never mentioned is different from changing a value you did write — but "update is now safe against partial configs" would be an overstatement. Confirmed the hard way: a partial config applied to a live agent replaced its instructions and index description without triggering the guard, correctly, because those were changes rather than removals. Restored from a snapshot taken moments earlier, which is itself the clearest argument for
snapshotexisting.Established about the API along the way
None of this is in the docs; all of it was probed with throwaway agents, created and deleted.
config, thetoolsarray, and per-indexsearchControls; omitted tool fields revert to their defaults (mode→"static",allowUnlistedIndices→false)description,systemPrompt,templateType;enhancedDescriptionand the tool-leveldescriptionare regenerated by the platformid,createdAt,updatedAt,lastUsedAt) are accepted and ignored, not rejected — so the snapshot exclusion list is tidiness, not correctnesstemplateTypeis an unvalidated provenance label, not a selector: any string is accepted, setting it has no side effects, and there is no/templatesendpointdescription,systemPromptandtemplateTypeare all writableFollow-ups
searchControlsschema documentation (docs: complete the searchControls parameter table #21). The native format already delivers per-indexsearchControls.predefinedSearchParametersstill appears on zero agents at tool level while index entries carrysearchParameters. Snapshotting any real agent is now the cheap way to confirm it is dead code.mergesemantics, and thequery/customsearch controls, remain unobserved.