Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 2 additions & 10 deletions apps/hub/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
} from "@intx/hub-api";

import {
agentDefinitionSourceTree,
buildAgentDefinitionWorkflow,
createAgentDefinitionRoutes,
createDefinitionAssetHistory,
Expand Down Expand Up @@ -2568,13 +2569,6 @@ export async function createHub(config: HubConfig) {
listModels: listMyraModels,
};

// Mirrors `@corbits/agent-directory`'s own private
// `AGENT_DEFINITION_ASSET_PATH` constant (not exported — the route
// module keeps it internal), kept in lockstep by convention since
// this is the same asset-tree contract `ensureWorkflowDefinitionForAsset`
// reads back from.
const PLANNER_AGENT_DEFINITION_ASSET_PATH = "workflow.json";

/**
* Wraps the same sequence `@corbits/agent-directory`'s `POST /`
* handler runs (`buildAgentDefinitionWorkflow` → `reindexPinnedSkills`
Expand Down Expand Up @@ -2656,9 +2650,7 @@ export async function createHub(config: HubConfig) {
ref: DEFAULT_ASSET_REF,
principal: { kind: "hub" },
tree: {
files: {
[PLANNER_AGENT_DEFINITION_ASSET_PATH]: workflowJson,
},
files: agentDefinitionSourceTree({ handle, workflowJson }),
message: `Define agent ${input.name}`,
},
});
Expand Down
19 changes: 17 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

160 changes: 152 additions & 8 deletions docs/revendor-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -770,12 +770,156 @@ folded-run-<runId>`), definition loaded from that closure, run grants
`scripts/e2e/cl-6324-launch-proof.ts` keeps the assertion as written
rather than weakening it to something the current shape happens to
satisfy.
5. **STILL OPEN — the agent-directory authoring lineage still writes the
retired envelope.** `createAgentDefinitionCore` and the read/modify/
write routes in `routes.ts`, `workflow-capability-routes.ts`, and
`workflow-skill-pin-routes.ts` all populate a `workflow`-kind asset
5. **CLOSED — the agent-directory authoring lineage now writes the source
form.** `createAgentDefinitionCore` and the read/modify/write routes in
`routes.ts`, `workflow-capability-routes.ts`, and
`workflow-skill-pin-routes.ts` used to populate a `workflow`-kind asset
with a bare `workflow.json`, which `workflowKindHandler.validatePush`
now refuses. That lineage is authoring, not launching — a projection
is a read-only artefact and cannot be written back through — so it
needs its own cutover to the codebase form, and none of the four
proofs exercise it.
now refuses. That lineage is authoring, not launching — a projection is
a read-only artefact and cannot be written back through — so it got its
own cutover. See the section below.

## CL-6324: the agent-directory authoring cutover

The renderer that had been living in `@workbench/hub-client`'s
`workflow-push.ts`, and a second copy of it in `@corbits/agent-runtime`'s
`source-tree.ts`, moved into a new dependency-free package,
`@corbits/workflow-source`. Every authoring path in the repo now writes
its asset tree through that one `renderWorkflowSourceTree` — the seed
pusher, the per-run agent-runtime package, and the agent-directory
lineage — so there is a single producer of the bytes a workflow-kind
asset carries.

`@corbits/agent-directory`'s `definition-asset.ts` is the lineage's own
seam onto it: `agentDefinitionSourceTree` renders a definition's tree
under a `@workbench-agent/<handle>` package name, and
`readAgentDefinitionWorkflowJson` reads the definition back out of the
entry module. Both replace the private `AGENT_DEFINITION_ASSET_PATH =
"workflow.json"` constant each of the four writers used to declare for
itself; `apps/hub`'s planner deploy and `@corbits/evals`' world snapshot
route through the same two functions rather than re-deriving the path.
The `workflow.json` write path is gone, not kept beside the new one.

Reading the definition back is a strict slice of the exact bytes the
renderer emits (the `export default ` prefix and `;\n` suffix), never an
eval and never a pattern search. Anything else — in practice, an asset
last written before this cutover, whose tree still holds a bare
`workflow.json` — throws `RetiredWorkflowEnvelopeError`, defined in
`@corbits/workflow-source` and re-exported from `@corbits/agent-directory`.
It carries re-author-and-re-deploy guidance and is mapped to a 409 in
every route module that can reach it: `routes.ts`,
`workflow-capability-routes.ts`, and `workflow-skill-pin-routes.ts` each
answer it from their own `app.onError`, so a stale asset never reads as a
server fault.

One behavioural change rode along: `POST /:definitionId/restore` now
reports the definition it just wrote, parsed from the entry module it
restored, instead of re-reading the asset immediately afterward.

## CL-6324: what the four proofs found on a real stack

`scripts/e2e/cl-6324-launch-proof.ts` is the harness: one scratch
database, a real signup, a real local Ollama, nothing mocked. It runs as

```
E2E_PROVIDER=ollama OLLAMA_BASE_URL=http://localhost:11434 \
E2E_OLLAMA_MODEL=<a model the instance serves> \
DATABASE_URL=postgres://localhost:5432/<scratch> \
bun run scripts/e2e/cl-6324-launch-proof.ts
```

### Proof 2, in the shape each deploy actually takes

The earlier revision asserted a workflow-host `RunStarted` for a folded
`step`-mode run and hung on it forever. That was an assertion about the
section shape aimed at the step shape's run. The two are now asserted
separately, each against the artefact it really produces.

**Step mode.** One unbounded step services every inbound mail, so the run
starts no child run per message and its own workflow event log stays
empty — the harness asserts exactly that, as a falsifiable statement
rather than a footnote. Its per-message bracket is the
`message.run.started` / `message.run.ended` AGENT event pair, which
travels the sidecar's `agent.event` channel and is never committed to the
workflow-run repo. The durable, HTTP-observable evidence that the bracket
opened AND closed is `@corbits/insights`' `turn_latency` row: the tracker
opens it on `message.run.started` and commits it on `message.run.ended`,
and `GET /api/tenants/:tenantId/insights/latency` reports it as a sample
count. That, plus the reply row itself, is what the proof asserts.

**Section mode — and CL-6329's first live validation.** `mode: "section"`
had existed as a config argument since the agent-runtime cutover and
nothing had ever deployed or run one. The proof now does, for real: it
renders a section-mode `AgentRuntimeConfig` into its own source package,
pushes it as a `workflow`-kind asset, deploys it by source-ref through
`POST /workflows/deployments`, and drives it with real mail. Every
message is an `onTrigger` occurrence with its own child run and its own
event log, which is where `RunStarted` genuinely lives:

```
turn__0 events: RunStarted, StepStarted, StepCompleted, RunCompleted
parent run events: RunStarted, StepStarted, ChildSpawned,
ChildCompleted, SignalAwaited
```

The parent's `SignalAwaited` after `ChildCompleted` is the section
re-arming for the next message. This is the shape the milestone's
`RunStarted` assertion was always about, and it works.

### Proof 4 FAILS, in both shapes, for the same reason

Kill the sidecar mid-turn, restart it, and boot restore does its half of
the job: the scan finds every deployment record, replays each pin, and
re-materializes the closure — the restarted stack reports the run's own
`liveness: "ok"` 9.2s after the restart. But the run inside it comes back
**terminal**, and every later message is refused:

- the folded chat run: `workflow-host·supervisor: rejecting inbound mail
'<...>': workflow run 'run_...' is terminal` — the hub accepts the room
message, nothing ever answers it, and the turn times out with no reply
and no notice;
- the section deployment: `POST /workflows/<id>/mail` answers `409
workflow_run_terminal`, "is terminal and cannot receive more mail".

So a mid-turn sidecar death is permanent for the run. `onBodyFailure:
"continue"` does not rescue the section here, and could not: the failure
is on the TOP-LEVEL run, not on a body occurrence, so there is no failed
child edge for the policy to act on. Nothing in the chat layer recovers
either, because its wake choke point only redeploys an address that is
NOT routable — and this address is routable, just dead.

This is the milestone's real remaining gap, and it is a platform-level
one rather than anything the app-side conversion introduced: restoring a
deployment is not the same as resurrecting its run, and at this pin
nothing does the second half.

### Timings, from a clean boot

| Phase | Wall clock |
| ---------------------------------------------------- | ---------- |
| section-mode deploy (push + probe + freeze + deploy) | 4.3s |
| section occurrence to its own `RunStarted` | 1.0s |
| sidecar restart + boot restore to `liveness: "ok"` | 9.2s |

Two phases read as ~0s and should not be mistaken for speed: a chat mint
is DB-only (the deploy happens later, on the first traffic), and the join
greeting is canned copy `@corbits/chat` posts in the agent's voice, not a
model turn. The first REAL token is proof 3's reply.

### Two environment traps the harness now closes explicitly

1. **The onboarding seed pins the curated model; the catalog is seeded
from the live instance.** `modelSourceFor` takes its model from
`CATALOG_SEEDS.ollama`, so every default workflow deploys pinned at
that name, while `seedCatalog` fills the tenant catalog from the
instance's own `/api/tags`. On an instance that never pulled the
curated model the two never meet, and every chat turn dies as
`InferenceResolutionError: No launchable inference source for model
"<curated>"` — surfaced to the reader as "I can't reach a model right
now". The harness names its model through `E2E_OLLAMA_MODEL` instead
of inheriting the curated one.
2. **A live Ollama connect seeds embedding models as offerings with no
capability metadata** (CL-6351), so default-model resolution breaks
the tie alphabetically and `all-minilm` can win the bench default. The
harness narrows the bench catalog to its one pinned model through the
catalog API before it asserts anything about a turn.
1 change: 1 addition & 0 deletions packages/agent-directory/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@corbits/chat": "workspace:*",
"@corbits/skills": "workspace:*",
"@corbits/workflow-catalog": "workspace:*",
"@corbits/workflow-source": "workspace:*",
"@intx/agent": "workspace:*",
"@intx/db": "workspace:*",
"@intx/hub-api": "workspace:*",
Expand Down
14 changes: 4 additions & 10 deletions packages/agent-directory/src/agent-workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
} from "@corbits/skills";
import { type } from "arktype";

import { agentDefinitionSourceTree } from "./definition-asset";
import type { DefinitionSkillsStore } from "./skills-store";

export const AGENT_DEFINITION_STEP_ID = "agent";
Expand Down Expand Up @@ -367,13 +368,6 @@ export function serializeAgentDefinitionWorkflow(
return JSON.stringify(definition);
}

/** Where a definition's serialized `WorkflowDefinition` lives in its
* asset tree — the same path every route that reads/writes a
* definition's `workflow.json` declares privately for itself
* (`./routes.ts`, `./workflow-capability-routes.ts`); this copy is the
* one `createAgentDefinitionCore` itself writes to. */
const AGENT_DEFINITION_ASSET_PATH = "workflow.json";

export type CreateAgentDefinitionCoreDeps = {
readonly db: DB["db"];
readonly assetService: AssetService;
Expand Down Expand Up @@ -448,7 +442,7 @@ export class DuplicateAgentHandleError extends Error {

/**
* The full create-agent-definition sequence: resolve the tenant's mail
* domain, build and pin the definition's `workflow.json`, materialize
* domain, build and pin the definition's serialized workflow, materialize
* it as a `workflow`-kind asset, persist its pinned skills, and
* project it onto a first-class `workflow_definition` row. Factored out
* of `./routes.ts`'s `POST /` handler so `./workflow-create-routes.ts`
Expand Down Expand Up @@ -509,7 +503,7 @@ export async function createAgentDefinitionCore(
cause.reason === "duplicate_asset"
) {
// A previous attempt may have created the asset row but failed
// before populateAsset wrote workflow.json — an empty shell that
// before populateAsset wrote its source tree — an empty shell that
// blocks retries with a misleading conflict. Recover: look up the
// existing asset and reuse it only if it has no definition yet.
const existing = await deps.db.query.asset.findFirst({
Expand Down Expand Up @@ -542,7 +536,7 @@ export async function createAgentDefinitionCore(
ref: DEFAULT_ASSET_REF,
principal: { kind: "hub" },
tree: {
files: { [AGENT_DEFINITION_ASSET_PATH]: workflowJson },
files: agentDefinitionSourceTree({ handle: input.handle, workflowJson }),
message: `Define agent ${input.name}`,
},
});
Expand Down
Loading
Loading