Skip to content
archmax.ai

Changelog

Newest first. Each section says what changed and links to the guide that describes the behaviour as it is today; the guides themselves describe only the present. Every release also has GitHub release notes listing its pull requests.

  • A script reaches a tool by its own name. The bridge keys each tool by its camelCased name, so await tools.read_file({ … }) in an archmax_eval or archmax_run script, the spelling a model writes, was undefined and died with TypeError: not a function. A tool’s own name now resolves to the same function as its camelCased key, governed and evented the same way; the listing still names only the camelCased keys, and a hook’s tools is unchanged. See the code interpreter.
  • Breaking: a hook reaches a governed mount only where its workflow grants it. A hook runs on runtime authority, and the mount rule used to skip that authority entirely. One mount table serves a session and every child it dispatches, so a caller’s before or after script could read a mount only a child grants, and write it where the host served it writable. A hook is now held to its workflow’s grants, taken across every state rather than the active one. It reads a governed mount that allow_always or any state’s allow names, and is otherwise refused with mount.not-allowed. It writes one only where a grant of the workflow asks access: read_write, and an access: read in allow_always narrows any mount for it, so the refusal is mount.read-only. A state’s own allow, forbid and access: read still do not bind a hook. A file value is checked on the same authority, so a seed or return naming such a path is refused too. Migrate by granting the mount in the workflow: mounts.allow_always: [name], or { mount: name, access: read_write } where a hook writes. See mount governance.
  • A workflow’s own forbid_always binds its own hooks. mounts.forbid_always and skills.forbid_always reached a hook only as a caller’s inherited denial, so a top-level workflow’s hooks could read what its own lists denied. They now refuse the call with mount.forbidden or skill.forbidden, as the docs already said.
  • Breaking: a sub-workflow’s unset returns reach its caller empty. A child that finishes without setting some of its declared returns still completes. Its caller’s returns now carries every declared name, each unset one as null, beside the note naming them. Before, an unset name was left out. A caller that tested for a key’s presence now tests for null. See returns.
  • Breaking: a seed of a return lands unlocked. A name in both requires and returns arrived locked, so a child could never write its result. A seed of a return now lands unlocked, whether it is a call argument, a host’s variables or a delivery naming one. A later turn’s seed leaves a value the session already holds in place. The agent may still lock a return with lock: true once it is final. A guard that reads a seeded return now reads a value the session can change until it is locked.
  • The prompt no longer reads as asking for returns up front. Every state shows the trigger’s returns. It now adds that each is set once its final value is known, that moving to another state does not need it, and that one is locked only when it is final. archmax_advance never checked the returns. A state’s own requires: is still the only gate on leaving it. See a return is owed when the session finishes.
  • Disposing a session can no longer end the host’s process. A host that called agent.dispose(sessionId) while a script’s tools.* call was still pending freed the script’s QuickJS context under the call. A host does exactly that in a finally around a cancelled send. When the call settled, @langchain/quickjs wrote its result into the freed context and threw where nothing could catch it. The unhandled rejection ended the Node process, and every turn running in it. A turn that ended while a timed-out script still awaited a call did the same. Disposal now abandons the calls still pending and frees the context once every call made from it has settled. dispose stays synchronous. See cancelling a turn.
  • A cancelled turn stops its scripts. The signal a host passes to send reached a script’s tools but not the script, so a cancelled script ran on until timeoutMs, and a retry in its catch could start a sub-workflow under the cancelled turn. The signal now reaches every archmax_eval, archmax_run and hook script of the turn, in the session and in its child sessions. The evaluation ends with an error at once. A tools.* call still pending fails with the turn was aborted, and any call the script makes after that is refused before it reaches the tool. A custom sandboxRuntime receives the signal as the third argument of eval. See the code interpreter.
  • A host tool knows which session its call is made in. ToolContext carries sessionId: inside a sub-workflow’s child it is the child’s own id, for a model’s call and a script’s alike. A session’s files are stored under <sessionId>/ in the session store, and that address is now a documented contract, so a host that reads or writes them through its own store handle keys them by ToolContext.sessionId. Keyed by the session the host sent the turn to, a child’s files landed in its caller’s folder, where the child’s own read_file and ls never looked. The field is new and optional to read. See host tools.
  • Breaking: a child session’s id is its dispatch number. A child is <parent>~<n>, n counting the parent session’s dispatches from 1 across all its turns, and a grandchild is <parent>~<n>~<m>. The count is held in the parent’s checkpointed state. Before, the <state>:<workflow>:<ordinal> ordinal restarted with every agent, so a host building an agent per turn could hand a second dispatch the child an earlier one parked on, its thread and its folder. The model’s delegation calls in one batch are numbered in tool-call order, so a replayed node reaches the same children. dispatchId is the child’s id below the root session (1, 1~1) instead of a per-process subwf-<n>. subRunIdentity is gone; dispatchIdOf and rootSessionIdOf join the spec subpath. sessionIdRejection refuses a session id holding ~ in any shape but a child’s. A session parked on a child before the upgrade still resumes it, under the identity its park recorded. See what crosses, and what does not.
  • Breaking: a workflow writes a governed mount only where its own grants ask to. A bare grant no longer takes the host’s write posture: a governed mount is writable in a state when the host serves it writable, some grant of the same workflow says access: read_write, and no applicable grant says access: read. A mount table serves a session and every child it dispatches, so a route served writable for one workflow let every other workflow in the tree write it through a bare grant. Now each workflow writes only what its own grants ask for. A spec that wrote a writable governed mount through bare grants alone adds access: read_write to one of them. An ungoverned mount is unchanged. See read or read/write.
  • XML and JSON read as text whatever the store labels them. read_file refused a file its backend reported as application/xml, as a +xml or +json type, or as JSON with a charset parameter: it answered “is a binary file” and returned nothing. An object store labels a file by its registered type, so an Office document unpacked into the scratchpad could not be read, and so could not be edited as text. These types now read as text, for the model’s read_file and a script’s tools.readFile. Bytes and NUL-holding content are still refused. The defect dates from 0.4.0. See the workspace.
  • A file signature type. A requires or returns entry may declare type: file. Its value is a path from the workspace root, such as scratchpad/attachments/invoice.pdf or skills/billing/assets/template.pdf. signatureJsonSchema maps it to { type: "string", format: "file" }, so a host can render a file picker, and signatureValueIssues checks the path’s shape. Wherever a file value crosses a boundary (the start of a session, archmax_set_variables, completion, a delegation call), the runtime also checks that a file is there and that the session may read it. See the signature.

  • Files cross a sub-workflow boundary as copies. A file argument is copied into the child’s own scratchpad/attachments/, and the child is seeded with that path. A file return is copied back into the caller’s scratchpad/attachments/ under a name nothing there holds: a second report.pdf becomes report-1.pdf, including when concurrent calls return at once. See passing files.

  • A child’s file space is its own, and the docs now say so. A child session has always resolved its paths inside its own session, but the sub-workflows guide said a child’s files came back to the caller, and the authoring skill said a child shared its parent’s session and run zone. Both now describe the separate session, and a test pins it. See what crosses, and what does not.

  • Hosts and the CLI hand a session files. TurnInput.files maps a variable to { name, content }. The runtime places the bytes in the session’s scratchpad/attachments/ and seeds the variable with the path. archmax run takes a repeatable --file <variable>=<path> built on it. See sessions.

  • No reset just to stay put. The platform prompt told the agent when to call archmax_reset but never when not to, so it reset on follow-ups the current state could serve. In a workflow whose entry state is terminal, such as a one-state chat, “a terminal state never serves a follow-up” sent it back to the state it was already in on every new message. The prompt now says that a state that can serve the message answers it there, and that a reset never helps while the session is still in the state the conversation began in. It also lists a terminal entry state among the cases that take no reset, which is where the per-turn state block already stayed silent. archmax_reset’s description says the same in one clause. See waiting is not finishing.

  • The title follows the conversation, not its latest message. The agent renamed sessions on follow-ups because the prompt asked for an update “when the task turns into something the old title no longer describes”, and the model judged that against the last message. The prompt now asks for a rename only when the conversation as a whole has moved to a different subject. A follow-up, a detail, a correction or a side question on the same subject keeps the title, and a rename names what the whole conversation is about. The title sentence in archmax_set_variables’ description sets the same bar. A sub-workflow’s child still reads neither. See two reserved names.

  • Prompt and description text only: the runtime accepts and refuses exactly what it did. A workspace that overrides the platform prompt with .platform/system/GRAPH_STATE.md keeps its own text.

  • grep‘s max_count reaches the store. Deep Agents passes a search’s cap to the backend as a fourth argument, and the workspace’s wrappers took three, so neither the agent’s max_count nor Deep Agents’ default of 1,000 matches ever applied. The store returned every match, and only Deep Agents’ 80,000-character limit on the answer cut it short. The session zone, every mount and every mountSubtree mount now forward it, and a search addressed at a searchable: false mount is capped even when its store ignores the cap. For hosts: a custom store’s grep now receives maxCount. See session storage.

  • No more “could not prune” warning. The harness cut Deep Agents’ task guidance out of the system prompt, but Deep Agents 1.13 never adds that guidance. The pruning found nothing and warned on every governed assembly. It is gone. The prompt is unchanged with one exception: the pruning also collapsed runs of three or more line breaks, so authored prose holding such a run now reaches the model as written. The prompt-shaping event still names the withheld tools.

  • Deep Agents 1.13.4 or later. The read_file description the harness rewrites exists only in 1.13, so the dependency floor is now ^1.13.4. The harness also stopped passing generalPurposeAgent: false, which is not a createDeepAgent parameter and never had an effect. As before, a plain agent is offered Deep Agents’ task tool with its general-purpose subagent, and a governed agent never is. See Agent, in detail.

  • What Deep Agents adds per model is now documented and pinned by tests.

    • A model with a Deep Agents harness profile gets that profile’s prompt suffix, and createDeepAgent has no per-agent opt-out. A Codex model reached through initChatModel is told to “bias to action”, against every human state. The env-configured ChatOpenAI is not affected: Deep Agents reads no model id from it.
    • ChatAnthropic and ChatBedrockConverse are cached by Deep Agents’ own middleware even with promptCache off. On ChatAnthropic, Deep Agents also places a breakpoint on the volatile block.

    See what the model reads and prompt caching.

  • A store without the protocol’s optional methods works again. Deep Agents makes downloadFiles, uploadFiles and delete optional and checks for them, and 0.4.0’s workspace defined all three and threw where a store had none. Deep Agents’ skills loader reads each SKILL.md through downloadFiles when the backend has one, so a plain agent over such a store found no skill. Now downloadFiles is always present and reads a store without it through readRaw (exact bytes, or permission_denied for text whose decoding replaced bytes). uploadFiles is present when the session store has it, and a mount without it refuses that file. delete answers an error. Nothing throws for want of a method. See raw bytes and deletion.
  • remove_file is the only way to delete a file. Because 0.4.0’s workspace could delete, Deep Agents registered its own delete tool, which removes a folder and everything under it. A plain agent was shown it beside remove_file. Deep Agents is now handed the workspace without delete, so that tool is neither shown nor able to delete.
  • The scratchpad opens only the always-on tools. scratchpad/ and the offload areas were open in every state to any tool that declared paths, ahead of the state’s grant, so declaring a granted host tool’s paths made it callable everywhere on scratchpad paths. They are now open only to the essential tools: the file tools, the file operations and your essentialTools. Any other tool needs its state’s grant there as anywhere, so declaring paths only narrows. For hosts: a non-essential host tool that ran on scratchpad paths without a grant now needs one. See file operations and path arguments.
  • A path argument may hold a list of paths. Each path of a list is governed with the argument’s access, and a paths: guard tests each one: a grant needs every path to match, a denial any. Before, a list was not governed by the path rules, and a guard read it as one comma-joined string, so paths: [attachments/**] admitted ["attachments/a.txt", "contracts/b.docx"]. A declared argument holding anything else (a number, an object, a list containing one) is refused (tool.path-argument). See host tools.
  • The binary type table is exported. BINARY_MIME_TYPES and binaryMimeTypeOf on @archmax-ai/harness/spec are Deep Agents’ extensions whose write carries base64, so a host store honouring the convention uses the same table.
  • An out-of-turn tool context can be compared and logged. Its throwing workspace property is non-enumerable, so a deep-equality check or a serializer skips it; reading it still says why.
  • File operations: copy_file, move_file, remove_file. Three new always-on tools, in governed and plain agents alike, that work on one file without its content entering the conversation. copy_file({ source, destination, overwrite? }) copies the bytes, text or binary (a .png, a .docx), creating the destination’s folders. move_file takes the same arguments and writes the destination before removing the source. remove_file({ file_path }) deletes a file. An existing destination is refused unless overwrite: true. Each answers with one line (Copied …, Moved …, Removed …, or Error: …), and scripts reach them as tools.copyFile and the rest. See file operations.
  • Path rules read each tool’s declared path arguments. The kernel used to apply its zone, mount, skill and inherited-denial rules to a fixed list of file tools and their one file_path/path argument. Every tool now declares its path arguments and how a call uses each (read, list, search, write, remove, execute), and every path rule reads the declaration. A call naming several paths is refused when any one is, and the refusal names the argument. A removal is refused wherever a write is. A paths: guard matches the tool’s declared path arguments, so { tool: grep, paths: [...] } now guards grep‘s path (it used to match no call). A paths: grant needs every path to match and a paths: denial needs any, so forbid: [{ tool: "*", paths: [secrets/**] }] refuses a copy out of secrets/ and into it. The built-in tools’ verdicts are otherwise unchanged. See machine-spec.
  • Host tools are governed by their paths and handed the turn’s workspace. An AgentToolDescriptor may declare paths, or a host passes toolPaths to createAgent, and its path arguments are then governed like the built-ins’. Before, a host tool reading a file was bound by no path rule. Its handler gets a second argument, a ToolContext whose workspace is the turn’s workspace: the host’s mounts and the session zone, bound to the session, the instance read_file uses. The workspace now carries downloadFiles, uploadFiles and delete through every layer, and a read-only mount refuses an upload and a delete as it refuses a write. See host tools. For hosts: a host tool named copy_file, move_file or remove_file is now refused at assembly (ReservedToolNameError), so drop your own. Declaring a built-in tool’s paths throws ToolPathsError. The three schemas add about 2,250 characters to every model call.
  • A sub-workflow child goes straight to its work. A one-state child spent two of its model calls on bookkeeping: it named its session, because the platform prompt’s step 1 said it was not optional, and it read its inputs with archmax_get_variables, because its opening line told it to. A child’s title is never returned and describes a session nothing lists, and its caller already held every input. A child’s prompt now leaves out the “Name the run first” step and the paragraph on title, and its archmax_set_variables description no longer asks for one. Its opening message lists its inputs’ values: strings JSON-quoted, numbers and booleans as written, up to 200 characters each and 1,000 in all. Any other input is named, with archmax_get_variables as the way to read it. A top-level session’s prompt and its cacheable prefix are byte-identical. For hosts: a child emits no title-set unless its caller passed a title, and the opening note’s text changes (its kind and shape do not). A workspace that overrides the platform prompt (.platform/system/GRAPH_STATE.md) should wrap its own title step in <!-- top-level-only --> / <!-- /top-level-only --> lines to get the same saving for its children. See what the child’s model reads.
  • A child short of its returns comes back with what it set and a note, and a caller’s successful retry recovers a failed delegation call. A sub-workflow child that finished without setting every declared return used to fail its caller outright, after a full run, although its prompt said it “does not complete until every one is set”; the caller had to run it again. Now it completes, with no second attempt and no extra model call, and the call’s returns carry the ones it did set plus note: "Not all return variables were set by the sub-workflow: 'description' was not set.". The calling agent decides what to do about the gap. A typed return holding a value of another type still fails the call (invalid-return), and a top-level session finishing short of its returns is still rejected. The rendered signature tells each which applies. On the calling side, a failed delegation call no longer commits a rejection that nothing clears. Each failed call is recorded with its arguments and stays the calling state’s failure until a later successful call of the same workflow from the same state recovers it. One success recovers one failed call: the one whose arguments it repeats, else the oldest. A corrected retry therefore recovers, and a caller that retries and then finishes no longer routes through on_error, but two parallel calls that both failed need two successes. Another workflow’s success, a sibling call in the same tool batch and a rejection with another cause recover nothing. The route names every outstanding failure with the arguments of its call. Two delegation calls failing in one tool batch no longer crash the turn with InvalidUpdateError. For hosts and authors: note is now reserved, so a trigger whose returns declares note fails to load: rename it. A routed delegation failure’s reason now ends with (called with {…}) naming the call’s arguments. A mocked sub-workflow that omits a declared return answers with the note too, instead of failing. See what returns enforces and failure is always closed.
  • Reads are text only. read_file on an image, audio, video, PDF or PowerPoint file used to hand the model the file as base64 in a multimodal block. An OpenAI-compatible tool message cannot carry that block, so the request failed or the model read base64. The bytes also landed in the session’s checkpoints, in a script’s tools.readFile result, and in tool-result previews. A binary file with an unknown extension (.zip, .docx, .bin) came back as decoded garbage. Both now return a notice instead: Error: '<path>' is a binary file (<type>, <size>) and was not read; read_file returns text files only. A file is binary by its type (as Deep Agents’ read_file decides it) or by a NUL byte in its content. The read_file description the model is handed says so, in governed and plain agents alike. See the workspace. For hosts: a model with vision still sees the images it reads, once the host turns on images (createAgent({ images: true })). The model’s read_file of a PNG, JPEG, GIF or WebP then answers with one line, and the image is added to each later model request as a user message right after that batch of tool results. The image is read from the workspace for each request, so its bytes never enter the history, checkpoints, events or scripts. Earlier images stay attached (keep, default all), and one over maxBytes (default 10 MB) or deleted since is named in text instead. Without the option, and for PDFs, audio and video either way, a multimodal model no longer receives the file through read_file. See images.
  • The agent ends a failed task with archmax_raise instead of a reply. On GPT-6 Luna, an agent whose work could not be completed explained the failure in a reply, which ends the session as a success: the e2e case for an order-store outage raised in 0 of 4 runs on 0.3.1’s prompt. “When the work fails” is its own section of the platform prompt again; it bounds recovery to a retry or two and says to make the call in the same message as the explanation, and the tool description says that a reply which only explains the failure reports it as a success. The same case now raises in 9 of 10 runs, and no case that should complete raised. The prompt stays a fifth below 0.3.0’s (1,787 tokens). See ending a session as a failure.
  • The reference workspace asks for the follow-up call in the same message. refund-request and clarify (and the clarify examples in the sessions guide and the authoring skill) now say to call archmax_advance or archmax_wait in the same message as the text for the person. With the wording “…, then call…”, GPT-6 Luna wrote the text and stopped.
  • The agent e2e reruns a failed case once. On a live model a case can miss a step it passes nine runs in ten; a failed case now runs once more on its own, and CI fails only when it fails again.
  • A sub-workflow child’s streamed text is no longer reported as its caller’s. A child runs inside its caller’s tool call and inherited the caller’s callbacks, the handler of the caller’s own stream among them, so every token a child’s model produced was also emitted as an agent-text-delta of the calling state, with no subWorkflowDispatchId. Two children answering at once interleaved word by word into the caller’s reply, and a caller’s turn that failed while a child streamed finalized the child’s text as the caller’s partial agent-text. A child is now invoked without its caller’s stream handlers: its text streams once, from its own session, tagged with the dispatch; the caller’s deltas are its own. Every other callback a host hands down, a tracer included, still sees the child’s runs. For hosts: the untagged duplicate of a child’s text is gone and the event shapes are unchanged; a filter that dropped untagged deltas while a dispatch was open can be deleted. See sub-workflow observability.
  • The platform prompt says that text alone ends the turn. GPT-6 Luna wrote the message a state asked for and stopped, without the archmax_advance or archmax_wait its instructions put after that message: a refund never reached its review, a clarifying question never parked. The prompt now says that a message without a tool call ends the turn and that the text and the call belong in the same message. A workspace that overrides the platform prompt (.platform/system/GRAPH_STATE.md) should add the rule.
  • The platform prompt is a fifth shorter (2,232 → 1,771 tokens, o200k_base), with the same rules: repeated contrasts, generic tool-use explanation and rationale the model does not act on are gone, and waiting, deciding and failing are one list.
  • The agent can end a session as a failure: archmax_raise({ code, reason }). A new control tool, offered in every state (terminal ones included) and never declared, for work that cannot be completed: a system the work depends on keeps failing, the thing asked about does not exist. code is the agent’s own short token (orders-unavailable), reason says what failed. The call must stand alone in its message, and it ends the session at once with the new status failed: no further model call, no on_error route, no hook, no returns check, and a rejection still pending from earlier in the turn is replaced. A session that ends without it is a success, so the tool is for failure only; the tool description and the platform prompt tell the model to recover first and never to use it in place of archmax_wait, archmax_reset or a human state. Forbid it with tools.forbid_always: [archmax_raise] (or a state’s tools.forbid). A child session that raises fails its caller’s delegation call with the kind raised, naming the code and reason. The CLI reports ✖ failed with the code and exits 1; cases gain the structural raised assertion, and succeeded: true fails on a raise. For hosts: Outcome.kind and WorkflowStatus gain failed, which is finished; a switch over either that must be exhaustive needs the new member, and code that read “not rejected” as success must now check for failed too. Every finished outcome the agent ended carries exit (SessionExit): { success: true } when completed, { success: false, code, reason } when failed; SessionSummary carries the same record for a failed session, and the raised event (state, code, reason, callId) precedes the closing state-leave. A workspace that overrides the platform prompt (.platform/system/GRAPH_STATE.md) should add the new “When the work fails” section. See ending a session as a failure, how a session ended and on_error.
  • A script lists only its state’s tools. In archmax_eval and archmax_run, tools used to list every tool the assembly registered, whatever the state allowed: Object.keys(tools) in a state with two tools printed all 120 of a connected MCP server’s. The calls were governed, the names were not. tools now lists exactly the tools the model’s own tool list carries in the state the script’s calls are governed against, re-scoped at the start of every evaluation, so after an advance the same REPL lists the next state’s tools. It stays one object, so a helper or a reference kept from an earlier call follows along. A tool the state does not offer is unlisted but still resolves by name, and calling it is refused with governance’s reason, as before. Lifecycle hooks are unchanged: they run on runtime authority and list every tool. For hosts and authors: nothing to change; a script that enumerated tools now sees fewer names, and every call is decided as before. See the code interpreter.
  • A parked session’s handoff works on providers that refuse tool history without tools. The reply-only model call a park spends (the handoff into a human state or an archmax_wait, a decision routed into another human state, a delegated child’s decision, a reply to a parked session) used to send an empty tool list beside a transcript full of tool calls, and every runtime note is one. Amazon Bedrock refuses that request (through LiteLLM: Bedrock doesn't support tool calling without tools= param specified), so every park failed there. The call is now handed the parked state’s tool list, like any other call in that state. It still cannot act: the kernel’s tool.reply-only rule refuses every tool call it makes, and a model that calls one instead of writing its message leaves the park without that message and without a second model call. For hosts: the reply-only request now carries tool definitions; the public API and the events are unchanged. See a parked session can still talk.
  • A failing tool is answered to the model, not thrown out of the turn. When a tool the agent calls throws (a dropped remote connection, a flaky API, a filesystem error), the call is answered with an error-status tool message carrying the error’s message. The session stays in its state, the next model call reads the failure, and the answers to sibling calls of the same step are kept. A tool failure is not a turn failure and never routes through on_error. Behaviour change for hosts: a turn that used to reject from agent.workflow.send on a tool failure now continues. Read tool failures from the tool-result event, which still settles error with the message as its preview. Parks and cancelled runs propagate as before, and a script’s tools.* call still throws to the script. See tool governance and on_error.
  • Typed trigger signatures. A requires/returns entry may be { name, type?, description? } beside the bare name, which stays valid and untyped. type is one of string, integer, number, boolean, date, date-time, object or array. The runtime holds a typed entry at the turn boundary, at archmax_set_variables, at completion and across a delegation, which gains the refusal kind invalid-param and the failure kind invalid-return. The delegation tool’s schema carries each parameter’s type and description, and an argument that is exactly one ${{…}} reference keeps the referenced value’s type. A trigger declaration may carry a caller-facing description, which leads the delegation tool’s description and never reaches the session’s own model. @archmax-ai/harness/spec (and the root) export SIGNATURE_TYPES, normalizeSignature, signatureForTrigger, signatureJsonSchema and signatureValueIssues, so a host builds MCP tool schemas, start forms and request validation from the rule the runtime enforces. Additive: every existing spec loads and behaves as before. For hosts: a spec that uses an object entry does not load on an earlier version, so upgrade every host that reads a workspace to this release before its specs use typed entries, and read signature lists through normalizeSignature rather than as string[]. See typing a signature, the signature reference and building a host schema.
  • No warning for a model the runtime places no cache marker for. Assembly used to warn that “prompt caching is inactive” on every turn for any model that is not Claude or Bedrock Nova, including OpenAI, Gemini and OpenAI-compatible proxy aliases, which cache a stable prefix automatically. That warning is gone. The prompt-shaping event still names the strategy (unsupported), cacheReadTokens on model-usage events shows whether the provider caches, and the warning for a state whose model wants a different native caching mechanism stays. See prompt caching.
  • A rejected session fails the CLI command. archmax run, decide and deliver used to print ✔ answer and exit 0 for a session that ended rejected, such as a start refused for a missing input. They now print ✖ rejected and the reason on stderr and exit 1, as the CLI reference always promised. Outcome and DecideOutcome gain rejected, the reason, so a host reads why without reaching into the checkpoint. See the CLI reference.

The first release of @archmax-ai/harness: a governed layer over LangChain Deep Agents.

  • One workflow.yaml per workflow is the enforced state machine: states, transitions, per-state allow/forbid governance for tools, skills and mounts, before/after hooks, budgets, error routing and triggers. See the workflow machine guide and the machine spec reference.
  • Human states and waits park a session durably; decide, reply and deliver resume it. See sessions.
  • Hooks and the sandbox: hook scripts, archmax_eval and archmax_run share one QuickJS sandbox with a governed tool bridge. See the code interpreter guide.
  • Cases (archmax test) and rubric grading. See testing and grading rubrics.
  • Sub-workflows, skills and triggers: see sub-workflows, skills and triggers.
  • The archmax CLI (run, test, validate, sessions, decide, reply, deliver). See the CLI reference.
  • The library API: createAgent and the sandbox, testing, cli, spec and messages subpaths. See the public API reference.