Architecture
Lifetime, run type, Deployments versus Jobs, the wake inbox, and intelligence fetch.
Binaries
| Binary | Path | Role |
|---|---|---|
| API | cmd/api | HTTP controller + outbox publisher; writes the foundry audit row from each agent/skill outbox event; flags -migrate, -openapi |
| harness | cmd/harnessV1 | Always-on: rereads the prompt via broker every 15s and runs tools the model requests; also polls inbox |
| MCP gateway | cmd/mcp | Serves the Foundry tools to the Flow Intelligence Desktop App and other MCP clients; calls the API and Broker with the person's token |
| agentListener | cmd/agentListener | Logs agent.* events |
| auditListener | cmd/auditListener | Also writes that audit row from the bus; a redelivery inserts nothing |
foundry-api is the long-lived controller. The web UI talks directly to it; the Flow Intelligence Desktop App uses the Foundry MCP gateway, which forwards calls to it over HTTP. The harness does not run in the browser.
Who can use an agent
An agent's owner is owner_fid, the person it acts as. agent_member holds editors and
viewers on the agent at the top of each parent chain, so a swarm member or coding agent
follows the agent that started it. foundry-api checks the caller's role on every agent route
after the Cedar: The authorization engine; each service embeds a Cedar policy and evaluates every mutating request against the actor's principal record. check. An agent that calls with the service token and
X-Flow-Agent-Id gets its owner's role. The roles, refusals, and admin reads are on
API.
Coding harness
cmd/harnessCodingV1 is a second harness image, foundry-harness-coding. It clones one
repository over SSH, edits files, and can commit, push, and open a pull request. It does not
run the Azure CLI. The coding pod mounts Kubernetes secret foundry-harness-ssh and picks the GitHub or Azure DevOps key from the remote. Creating that secret is on Operations.
The image carries the toolchain this repository checks with: Go 1.26, Node 26.5.1, pnpm 11.0.8 and 11.1.3, Python 3, make, gcc, git, jq, ripgrep, gopls v0.22.0, and OpenJDK 21. sqlc and gotestsum are go tool commands. golangci-lint is v2.13.2, the version CI installs. OpenJDK 21 is the JDK Debian trixie ships for a Kotlin change under deepsky. The Azure CLI is not in the image.
A commit uses the agent mailbox as user.name and user.email ([email protected] in prod, [email protected] otherwise). open_pull_request puts an Agent heading and that same address at the top of the description. git push uses the same SSH command as clone. open_pull_request calls the GitHub or Azure DevOps HTTP API for the remote host. The token is GITHUB_TOKEN or AZURE_DEVOPS_TOKEN, copied from the foundry-api process into the coding pod when it is set. ask_question writes a question log line and waits. A person answers from the agent Logs tab, which posts POST /v1/agents/{id}/answer. The CLI asks on the terminal instead. Tasks are list_tasks, add_task, and update_task. code_intel runs gopls for a definition or for diagnostics.
finish with completed needs a passing check after the last edit, unless every changed file is .md, .mdx, or .txt. The checks that count are go test, go vet, go build, gofmt, golangci-lint, a make target such as test, lint, lint-new, check, or check-static, an npm, pnpm, or yarn script named test, lint, check, verify, typecheck, or build, npx tsc, eslint, vitest, jest, or playwright, node --test, python -m pytest, gradlew or gradle with a task named test, lint, check, build, or ktlint, and ktlint. The program name is gradlew, and the wrapper is gradlew or deepsky/gradlew in the clone. A foreground command stops after 10 minutes. read_file returns at most 2,000 lines at a time and refuses binary files. glob accepts ** for any number of directories and skips .git and node_modules. When the repository scope limits the agent to some paths, list_dir, grep, and explore on the repository root show only those paths rather than refusing. The pod copies COREPACK_HOME to $HOME/.cache/corepack when the image's copy is read-only, so a repository that pins another pnpm version can download it.
A run ends when the model calls finish, or when the wall clock runs out. Two backstops sit behind that: a step allowance of 60 model turns per attempt at a phase, and a wall clock of 30 minutes for the whole task, planning and building on the same clock. Using up the step allowance does not end the run. While the clock has time left, the phase runs again, and the attempt that carries on is given what the run already knows — the files it touched, the decisions it recorded, the checks that failed — rather than starting from the task alone. Five turns before an allowance runs out the model is told to stop exploring, commit, open the pull request, and call finish. The wall clock is the stop that ends the work unfinished. The harness expires its ephemeral agent when a run finishes or when the clock runs out, never because one attempt ran out of turns, so a change that is half made keeps the pod it was being made in.
The pod also registers the Agent Harness V1 platform tools, but only the ones the agent is granted plus the harness's always-granted tools, each with the description and schema from GET /v1/tools. If the catalog cannot be read, the pod writes a warn log line and runs without platform tools. list_queue_errors is always granted here. It reads the intelligence dead-letter queue so the run can open a pull request for the cause. requeue_queue_jobs and expire_queue_job are not offered to a coding run, because its fix is not deployed when it finishes; a person grants them to an Agent Harness V1 agent instead. See Scheduled work.
Every inbox task plans first. save_coding_plan uploads a markdown document with source coding_plan through documents, into the agent-storage container on the FOUNDRY_FILE_STORAGE_ACCOUNT account. The document carries a reference with type foundry_agent, the agent id, and category coding-plan, and the plan log line records its location as documents:<document id>. GET /v1/agents/{id} includes plan_document_id for the newest of those locations, so the swarm card can open the plan after later log lines have moved it off the first page. GET /v1/agents/{id}/plans/{documentId} returns a read URL with a SAS for that document, and returns not found unless the document references that agent as a coding plan. The intelligence app uses it to show and download the plan. A fenced mermaid block in that plan, and in the instructions preview, is drawn as a chart. The repository scope field approval is person or self. person is the default: the agent saves the plan and waits. A reply of "build" starts the work. Any other reply revises the plan. self builds immediately and answers its own questions with the recommended action, and a completed finish that changed files needs the pull request write-up. An ephemeral agent always approves its own plan, whatever the field says. A plan that is still waiting lives in the pod's memory. A restart drops it. The document remains, and the next task starts a new plan.
The pod checks out flow/<slug> before the model runs. It clones base_ref when that is set, and otherwise the repository default branch (default_ref, or the remote HEAD when default_ref is empty). The pull request targets that default branch. git_push refuses to push the target branch, and run_command cannot run git push. merge_pull_request is present and refused while allow_merge is false. The repository scope API does not set that flag.
An always-on agent granted spawn_coding_agent starts one of these ephemeral agents with POST /v1/agents/{id}/coding-agents. The task, the SSH remote, and an optional start branch come from the call. The spawning agent also picks the harness with harness_id, which has to be a harness that declares the repo_scope feature, since a coding agent clones a repository and edits it. Omitting harness_id uses Coding Harness V1. A harness without repo_scope is refused with 422, and an unknown id with 404. See Building and managing agents.
Each task is saved to a workspace conversation owned by the agent's owner_fid. The harness sends X-Flow-On-Behalf-Of with that principal, so the thread appears in that person's conversations. The next task loads the recent user and assistant turns back into the model. The local CLI does the same when WORKSPACE_URL and FLOWCODE_ACTOR are set. FLOWCODE_ACTOR is that person's principal.
The coding pod calls broker-api with the shared service token and sends the agent mailbox on X-Flow-On-Behalf-Of. That is the usage person Foundry registered when the agent was created. The model is the one stored on the agent. auto is openai / gpt-5-mini.
The agent detail shows a context gauge for a coding harness. The fill is an estimate, about four characters to a token. The window it is measured against is the model's own max_context_tokens from the broker catalog, read once at the start of a run. Set FLOW_CONTEXT_TOKENS to pin a different window; that value wins over the catalog. When neither is available the window is 128000. The same line is written to the agent log when the percent changes.
The harness sends reasoning_effort only when the catalog says the model accepts it. FLOW_REASONING_EFFORT names the level to ask for, and a model whose catalog row lists no levels gets the field dropped with one info line on the agent log saying so. Anthropic models are the common case: Claude takes a thinking block rather than reasoning_effort. If the catalog cannot be reached the harness sends what the environment asked for rather than silently changing how the model is called.
A provider that answers 200 with no completion is retried twice, after 500ms and 1500ms, and the retry drops reasoning_effort because the usual cause is the whole output budget going to reasoning tokens. A run that still gets nothing fails with the provider's own response body on the error, so the agent log says why instead of only "no choices".
Coding pods carry the label foundry-role: coding, which gives them public TCP 22 and 443 for git, proxy.golang.org, and the npm registry. Other harness pods stay on DNS and the in-cluster APIs. See Persistent versus ephemeral for both network policies.
Before any per-agent additions it allows git, rg, jq, go, gofmt, golangci-lint, gopls, node, npm, npx, corepack, pnpm, python, python3, make, gcc, java, and the
inspection commands ls, cat, grep, pwd, echo, head, tail, less, more,
wc, which, whereis, dir, env, printenv, whoami, date, time, uptime, uname,
df, du, ps, top, htop, man, and info. That is the Cursor client's inspection set.
The harness has no shell, so the builtins from that set are not commands: cd, type, alias,
history, and help. The repository, path prefix, and extra allowed commands
are set per agent in the Foundry UI when the harness feature repo_scope is present.
gradlew runs the wrapper at gradlew or deepsky/gradlew in the clone.
JAVA_HOME is /usr/lib/jvm/default-java.
A coding agent that is persistent and on-demand is rejected. foundry-api has no toolchain, so
that shape would have nowhere to run the work. Ephemeral agents are Jobs. Persistent always-on
agents are one-replica Deployments. The coding pod asks for 1 CPU and 2Gi and is limited to 4
CPU and 8Gi, with GOMAXPROCS, GOMEMLIMIT, and a Node heap cap set from those limits.
cmd/flowcode is the same engine as a local program. It does not talk to foundry-api. See
flowcode.
Persistent versus ephemeral
A persistent always-on start creates Deployment foundry-agent-{agentId}, replicas 1.
AlreadyExists is idempotent. Restart bumps generation and recycles that Deployment.
A persistent on-demand agent has no harness pod. Start only sets desired_state=running.
Callers POST /v1/agents/on-demand/{slug} with a Cloudflare JWT and a one-time secret
minted in the intelligence-app Call tab.
An ephemeral agent is lifetime=ephemeral. Only another agent creates one, by spawning a swarm
member or calling spawn_coding_agent; a person creating one by hand is refused with 422. The
persistent agent that spawned it is what later spins it down. Every ephemeral agent gets its own
agent row (parent_agent_id, swarm_id, ttl_at) and runs as a Job
foundry-agent-{agentId}-{generation} (backoffLimit: 0, ttlSecondsAfterFinished,
activeDeadlineSeconds).
status is active, expired, or archive, separate from desired_state. Stop and start are
reversible: POST /v1/agents/{id}/stop sets desired_state=completed and leaves status=active,
and POST /v1/agents/{id}/start brings the same agent back. Start bumps generation so the new
Job gets a name the finished one is not still holding. Start also deletes every earlier Job for the
agent, pods included, and stop deletes all of them. Every generation polls the same inbox, so an
older pod left running would take the next wake on whatever image it started with.
Expiry is explicit. When a coding run finishes, or an on-demand ephemeral wake is handled, the
harness calls POST /v1/agents/{id}/expire, which sets status=expired and
desired_state=completed. Dismissing a swarm does the same, as does the reconcile sweep once
ttl_at has passed — the sweep takes any ephemeral agent still active, including a stopped
one, so resting does not postpone the TTL. POST /v1/agents/{id}/start still refuses an expired
agent. POST /v1/agents/{id}/restart brings an expired ephemeral agent back: status returns to
active, generation bumps, ttl_at moves four hours ahead, and a new Job starts. That process
reads the instructions on the row and runs them from the start.
POST /v1/agents/{id}/archive sets status=archive and stops the harness. An archived agent
cannot be started or restarted. It drops off the agent list and search, but stays in
GET /v1/agents/{id}/children so the parent's Ephemeral tab still shows it.
Locally the same harness binary is a child process (FOUNDRY_RUNTIME=local). Cluster uses
FOUNDRY_RUNTIME=cluster or an in-cluster kubeconfig. Each agent stores harness_id. A new
harness is a new harness row; existing agents stay on Agent Harness V1 until someone changes them.
model is auto (broker routing) or a broker wire_id. Foundry stores the string. Skills are
a catalog (skill, skill_version, agent_skill). The bytes live in documents;
foundry stores document_id and an optional path inside an archive. A null pin on
agent_skill means latest. Agent Harness V1 reads each attached skill's SKILL.md
and puts that text in the system prompt, beside the agent's instructions. A markdown
document is that file. A zip or tar yields SKILL.md, or the version path when one is set.
tools is a grant list drawn from the tool catalog. Each harness names the
tools it can run (harness_tool) and whether it allows skills (allows_skills).
Extra harness features such as a later soul.md land in harness_feature.
Agent Harness V1 permits four families: the communications catalog, the
intelligence layer (search, packs, workspace conversations, outputs, files), the
agent's own memory and research checkpoint (agent), and the
Foundry tools for building agents
(foundry). SMS and email keys stay in communications
(ADR 0045).
An empty list grants nothing beyond the always-granted tools. A grant that the
pinned harness does not permit is refused. The harness opens a workspace
conversation on boot and appends a turn for every prompt, model reply, and tool
call. That conversation, and any other conversation the agent opens, is stored
with the agent's contributes_to_knowledge setting. The setting defaults to
false, so the knowledge layer does not read it. The Foundry workspace loads
Project and Conversation from workspace-api with the same Cloudflare access token
the rest of the console uses.
slug is unique and user-settable. The create form proposes it from the name:
spaces and other punctuation become hyphens. The mailbox is ${slug}@ai.flow.life
in prod and ${slug}@dev-ai.flow.life everywhere else. Communications looks that
address up on inbound mail.
On create, rename, or swarm spawn, Foundry registers that mailbox on broker as a
usage person (POST /v1/people): first name agent, last name the agent name.
Broker usage later attributes work to that email.
Copy from zoomlauncher: controller Deployment, deterministic names, narrow RBAC. Do not copy Zoom webhooks, the two-container loopback, or the C++ SDK image.
The harness image is Debian 13 (debian:trixie-slim), pushed to each environment ACR as
foundry-harness. foundry-api injects FOUNDRY_HARNESS_IMAGE from Helm harnessImage. The
API distroless image is not used for agent pods.
foundry-api's chart sets serviceAccountName in default. The Role for Deployments,
ReplicaSets, and Jobs is in namespace foundry-agents. Listener and gateway releases leave agentRuntime
unset and keep the basicapi Deployment. Harness pods run in foundry-agents as ServiceAccount
foundry-harness with no Role and automountServiceAccountToken: false.
Every harness PodSpec runs as uid 65532, drops all capabilities, uses a read-only root
filesystem with emptyDir on /tmp and /work, and does not use host network or PID.
FOUNDRY_AGENT_NODE_SELECTOR and FOUNDRY_AGENT_TOLERATION pin pods to the agent node
pool (agentpool=agents1 / workload=agents1 in Helm values). Until that pool exists, a
create can succeed and the pod stay Pending.
A NetworkPolicy in foundry-agents selecting app: foundry-harness denies all ingress. Egress is
kube-dns plus TCP 8080 to foundry-api, intelligence-api, communications-api,
broker-api, workspace-api, and documents-api in default.
A second NetworkPolicy, foundry-harness-coding, selects only pods that also carry
foundry-role: coding. Network policies add together, so coding pods keep the rules above and also
get TCP 22 and 443 to any IPv4 address outside 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, and
169.254.0.0/16. A network policy cannot name a host, so this is how git, proxy.golang.org, and
the npm registry are reached. It does not limit which public hosts a coding pod can reach. The
excluded ranges keep the VNet, other pods, and the instance metadata endpoint (169.254.169.254)
off the list, so a coding pod reaches nothing inside the cluster that other harness pods cannot.
Network policies only take effect when the AKS cluster runs a network policy engine. The users-app README records that dev runs Azure CNI with network policy turned off. Where that is true, neither policy is enforced.
If kube refuses create or delete, the API returns that error (503) and Start reverts
desired_state to stopped. The UI must not show running when nothing started.
Wake loop
The process in the pod is /harness. In dev and production it writes JSON
lines to stdout (one object per event) and the same events to
POST /v1/agents/{id}/logs. On start it writes harness started. Heartbeats print immediately, then every
30 seconds. Always-on work is a think loop: every 15 seconds the harness reloads
the agent, sends the instructions and granted tools to broker (/v1/relay/chat/completions
or /v1/relay/messages), and runs the tool calls the model returns.
A cycle is multi-step. The harness runs the requested tools, sends each result back to the model as a tool message, and asks again, up to the agent's max turns. Max turns defaults to 10 and cannot be set lower. A higher value is kept. That is what lets an agent read what a lookup returned and then decide what to do about it, rather than calling one tool blind and stopping. Results larger than 6 KB are truncated with a note saying so, so one wide search cannot fill the context. A step that returns no tool calls ends the cycle; hitting the limit logs a warning and leaves the rest for the next cycle.
An agent carries three kinds of memory, and they answer different questions.
The working set is the last eight cycles — what it looked up, what each lookup returned, and what it concluded. It lives in the process and goes into the user turn so an agent does not re-run the same search or repeat a message it already sent. A restart clears it. It is not in the system prompt, so those bytes stay stable enough for the provider to cache.
The same user turn says when this start began, the agent's start number
(generation), and how many evaluations have run since that start. Generation
is the start. A restart bumps it and recycles the process. Editing instructions,
the model, the harness, skills, or tools also bumps it, and the next evaluation
treats that as a new start while the process keeps running. For up to three
evaluations of that start, until the model calls a tool other than the research
checkpoint, the turn says work assigned to each start is due even if memories
show it was done after an earlier start. A scheduled research read does not
complete that work. Durable memories survive a restart, so without this an
instruction such as "open a pull request every time you start" looks finished.
Durable memories are rows in agent_memory, stored on the agent. A memory is
one row. Thoughts are the assembled block the next cycle reads: the curated
core set plus a small recent window of rows consolidation has not
reviewed yet. Everything else is archive, reachable with recall and not
in the prompt. The same Thoughts renderer lives in
services/foundry/internal/harness/memory so the API and any harness binary
produce the same text. V1 chooses when to load, remember, recall, and
consolidate; the store and the renderer are shared.
remember writes to archive. A person adding a memory from the Memories tab
writes to core: that is how you correct an agent without editing its
instructions. Human-written rows are never demoted or superseded by
consolidation. Only a person can delete one row or purge the collection.
Consolidation is a separate broker call inside the cycle, not part of the tool
loop. It runs when at least eight rows are unreviewed or thirty minutes have
passed since the last pass. It uses agent.memory_model (auto means the
agent's own think model). The pass returns the new core set and which ids are
superseded; foundry applies that and stamps consolidated_at, which is what
retires a row from the recent window. Core is capped at ten items or 1,500
characters.
remember and recall are always granted on Agent Harness V1. They do not
appear in the per-agent tool picker. A later harness can omit them.
The system prompt is ordered stable-first (policy, instructions, thoughts) and
uses absolute timestamps. Anthropic calls set a cache_control breakpoint on
that block. Cache hits land on broker_usage_records.cache_read_tokens and
cache_savings_micros.
None of this replaces the workspace conversation, which stays the record of what the agent actually did.
A think call waits up to 5 minutes for the first broker response headers; inbox and
foundry reads use 15 seconds. Broker itself waits up to 10 minutes on the model
provider. auto uses gpt-5-mini.
Logs name the arguments, not just the tool: step 1: search limit=20 q=open billing problems followed by step 1: search returned 12 results. Phone numbers
are redacted. A cycle that decides to do nothing logs step 1: done, nothing further plus a short redacted reason.
The instructions are the agent's standing configuration, so they go in the model's system prompt alongside thoughts. The user turn carries the tick — the current time, a nudge to decide whether anything is due, and the in-process cycle log — which is also what keeps a user message present for Anthropic.
The agent's workspace conversation gets the model reply and each tool call as
turns. Instructions are written as role=system, turn_kind=system, and only
when they differ from the previous cycle. The conversation stays out of the
knowledge layer while the agent's setting is off. The turn kinds still matter if it is later marked as
contributing: intelligence skips lookup, system, and output turns when
extracting claims, so a prompt logged as a spoken utterance would be read as
something a person said. Attached skill text is written the same way, as a system
turn, and only when it differs from the previous cycle. Inbox wakes still drain in
parallel. Failures name the service, the action, the HTTP status, and the other
service's error text (for example broker relay failed: HTTP 502: …).
Those lines go to stdout and kind=error. A tool failure is reported back to the
model as that call's result rather than ending the cycle, so the agent can try a
different approach.
The harness waits on GET /v1/agents/{id}/inbox.
| Reason | Who posts | Who accepts |
|---|---|---|
comms | communications after inbound | any running process |
manual | UI or POST /run | any running process |
dream | scheduler POST /wake | always_on only |
harvest | scheduler POST /wake | always_on only |
On wake the harness loads the assignment, GETs knowledge-sections and insights from
intelligence, and may fetch GET /v1/messages/{id} from communications. It writes a run
record (ids, tokens, reason — no prompt text). Intelligence stays the source of truth.
A stopped persistent agent still records the wake. A completed ephemeral agent is gone; do not resurrect it. Link threads to the persistent parent.
For transcript-based momentum briefings, see Transcript research. Its source-page reviews and continuation mode are separate from insight-led discovery.
Daily brief pipeline
The daily brief job runs inside foundry-api, on the scheduler route, or from cmd/reportrunner. It does not use a harness pod. It reads as the recipient.
| Stage | Reads | Code |
|---|---|---|
| Roster | Intelligence GET /v1/people and GET /v1/people/resolve | internal/brief/roster.go |
| Role | Workspace GET /v1/context/person?mode=brief, with the service token and X-Flow-On-Behalf-Of | internal/brief/context.go |
| Meetings | Workspace Zoom transcripts from yesterday | internal/brief/narrative.go, internal/brief/meetings.go |
| Writing | The broker, with no tools | internal/reportwrite/writer.go |
A meeting list or a transcript that cannot be read does not store a brief. The meetings that were read are stored on the daily_brief coverage, not in the text. Feedback is a separate brief_feedback row. The private text of a person's context stays in Workspace. See Daily briefs and docs/decisions/0101-a-brief-is-yesterdays-meetings.md.
Continuous insight research
Agent Harness V1 supports opt-in continuous research through the save_research_checkpoint
tool grant. Select it together with list_insights, search, and get_insight in
that agent's Tools field. list_discovery_packets reads the same review queue as the
Discovery screen, including saved leads. It can be limited by lens, inquiry, entity text,
and an exclusive created_after or updated_after instant. get_discovery_packet reads
one investigation with its supporting and opposing passages. The list returns the latest
review mark, the source record ids, and when the investigation was stored. It does not
read published insights. The momentum briefing uses culture and overlap cards as leads
and still cites the transcripts. Deploy migrations 0035 and 0036 so the
list tool, its reporting-period filters, and the detail tool are in the catalog.
pulse and Workspace output tools remain optional. This
mode does not grant evidence access implicitly or change ordinary scheduled agents
into researchers. Deploy migration 0020 and the API before the new harness.
The API stores one versioned checkpoint per agent outside memory consolidation.
It contains the current investigation, a bounded summary, the listing page, and
an explicit next read (list_insights, search, or get_insight). At the start of
each research evaluation the harness loads and executes that read before asking
the model to reason. The returned evidence is marked as source content. Historical
cycle summaries are labeled as previous evaluations, with a separate unique ID for
the current evaluation.
The model calls save_research_checkpoint with the current version, investigation,
summary, complete flag, page, next_tool and next_args. Completion atomically schedules
list_insights for the supplied next unread page. The API rejects stale versions
and backward page movement; keyword search remains available for historical work.
Ordinary remember calls cannot replace this execution checkpoint. The initial
checkpoint selects page 1 with three records. Enabling this mode does not import old
free-form checkpoints: the model must explicitly reconcile useful old research with
the new authoritative state.
Checkpoint POSTs require an idempotency key and write an outbox event containing only the agent ID and version. Conflicting writers receive 409 and must reload. The runtime uses the agent's existing authorization for reads. Checkpoint concurrency protects writes; it does not prevent two overlapping pods from performing the same read.
The harness stops before inference when catalog loading fails or a selected tool
lacks a schema or executor. Research additionally checks its required grants and
INTELLIGENCE_URL. Failures enter a five-minute process-local retry gate; a changed
agent configuration bypasses the wait. Unchanged blockers do not produce repeated
conversation messages. The user's stopped state takes precedence. A pod restart
clears the gate, but not the database checkpoint.
Three successful research evaluations without a changed saved checkpoint produce a stalled warning and the same retry delay. This detects lack of continuation, not whether a finding is economically useful. Research progress still requires evidence review and business evaluation. Numeric page progress is not a snapshot guarantee when the underlying corpus changes.
The loop permits the agent's max turns, potentially multiple tool calls per step, plus
the scheduled read. Max turns defaults to 10 and cannot be set lower. A higher value
is kept. Each evaluation states the running agent's id, slug, and
attached Workspace conversation id, so the model does not have to discover them
with get_agent. Oversized tool results are returned as a valid JSON envelope
with truncated: true and an incomplete preview, rather than a broken JSON fragment.
The model must narrow the query; truncation does not establish coverage. An agent
record whose instructions would push workspace_conversation_id past that cut
omits the instructions and returns the identity fields complete. A repeated
identical call is marked as unchanged. If tool calls use every step without
save_research_checkpoint, the harness asks once more for that checkpoint and
accepts no other tool on that turn. An evaluation that calls no tools is not
given that extra turn. save_note writes an evidence note through POST /v1/notes.
The markdown is the note body, so the evidence page can render it immediately.
The tool leaves contributes_to_knowledge false unless the caller sets it, so a
skill draft is not extracted as company knowledge. When project_id is omitted
and the agent has a project, the note is filed there. With no project it stays
unfiled, and only its author and platform admins can read it. Agents that
already have save_output on Agent Harness V1 receive save_note from the
migration.
Retrieval identifiers
Agent Harness V1 validates identifiers for get_insight and get_conversation before
making an upstream request. Missing, abbreviated, malformed, and zero UUIDs return
an actionable tool-argument error, rather than an apparent source-access failure.
The conversation reader validates the resolved default conversation ID as well.
Agents are instructed to retain full identifiers and field names in checkpoints.
UUID validation cannot establish resource type or authorization: an evidence
record_id must not be substituted for a workspace conversation_id merely
because both are UUIDs. Valid identifiers still require successful upstream reads.
Reading original Slack and interview evidence
Migration 0021_evidence_read_tools.sql registers two optional Agent Harness V1 tools.
Grant get_evidence and get_evidence_utterances to each agent explicitly and
configure WORKSPACE_URL. These use the public Workspace evidence routes with
normal on-behalf-of authorization; they do not use internal derivation endpoints.
search adds source_retrieval instructions to hits with a valid record_id.
The instructions name the tools and full arguments without replacing the original
hit identifiers. get_evidence reads /v1/evidence/{record_id}, exposing original
source, kind, title, date, participants and note body where present.
get_evidence_utterances reads /v1/meetings/{record_id}/utterances for meetings,
threads and documents. It defaults to three lines and allows up to twenty;
since_seq resumes after the last read sequence. The response preserves the
upstream data and cursor metadata, including speaker attribution and redactions.
The existing tool-result text budget still applies. A truncation marker means
incomplete evidence even if the upstream page says hasMore=false. Reduce the
page size; if one utterance still exceeds the budget, disclose the gap rather
than claiming complete coverage. Search hints may themselves be truncated and
are not an authorization grant. A Slack external reference is not necessarily a
clickable URL. No new Slack ingestion, source filtering, or automatic case queue
is introduced by these adapters. Agents must distinguish original sources from
agent-generated conversations; prompt instructions are not a server-side filter.
flowcode
The local coding agent. Same engine as the Foundry coding harness, without foundry-api.
Building and managing agents
The Foundry tools that create, edit, start, stop, and delete agents, the tool that starts an ephemeral coding agent, the tools that handle one Jira issue including a read-only resident investigation, and how a person grants them from the Flow Intelligence Desktop App.