Skip to content

Commit 4b25ab8

Browse files
committed
feat: standardize agent environments and asset lineage
1 parent 4c9768f commit 4b25ab8

396 files changed

Lines changed: 286742 additions & 625926 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/api-cf/src/agents/backends/canvas.test.ts‎

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -174,25 +174,33 @@ describe("Canvas class", () => {
174174
expect(node!.position).toEqual({ x: 0, y: 0 });
175175
});
176176

177-
it("creates image_gen node with generative proposal and assetId", () => {
177+
it("creates an image generation action without assigning output identity", () => {
178178
const canvas = makeCanvas();
179-
const result = canvas.createNode("img1", "image_gen", { label: "Img" }, null, null, "asset-123");
179+
const result = canvas.createNode(
180+
"img1",
181+
"image_gen",
182+
{ label: "Img", assetId: "legacy-data-asset" },
183+
null,
184+
null,
185+
"legacy-argument-asset",
186+
);
180187

181188
expect(result.proposal!.type).toBe(ProposalType.Generative);
182189
expect(result.proposal!.nodeType).toBe(RF_NODE_TYPE.ActionBadge);
183-
expect(result.asset_id).toBe("asset-123");
190+
expect(result.asset_id).toBeNull();
191+
expect(result.proposal).not.toHaveProperty("assetId");
192+
expect(result.proposal!.nodeData).not.toHaveProperty("assetId");
184193

185194
const node = canvas.readNode("img1");
186-
expect(node!.data.assetId).toBe("asset-123");
195+
expect(node!.data).not.toHaveProperty("assetId");
187196
});
188197

189-
it("auto-generates assetId for image_gen when not provided", () => {
198+
it("does not preallocate an assetId for image generation actions", () => {
190199
const canvas = makeCanvas();
191200
const result = canvas.createNode("img1", "image_gen", { label: "Img" });
192201

193-
expect(result.asset_id).toBeTruthy();
194-
expect(typeof result.asset_id).toBe("string");
195-
expect(result.asset_id!.length).toBe(8);
202+
expect(result.asset_id).toBeNull();
203+
expect(canvas.readNode("img1")!.data).not.toHaveProperty("assetId");
196204
});
197205

198206
it("creates video_gen node with generative proposal", () => {
@@ -201,7 +209,8 @@ describe("Canvas class", () => {
201209

202210
expect(result.proposal!.type).toBe(ProposalType.Generative);
203211
expect(result.proposal!.nodeType).toBe(RF_NODE_TYPE.ActionBadge);
204-
expect(result.asset_id).toBeTruthy();
212+
expect(result.asset_id).toBeNull();
213+
expect(canvas.readNode("vid1")!.data).not.toHaveProperty("assetId");
205214
});
206215

207216
it("includes upstreamNodeIds in proposal", () => {

‎apps/api-cf/src/agents/tools/canvas.test.ts‎

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,8 @@ describe("Canvas tools", () => {
215215
// ─── create_generation_node ───
216216

217217
describe("create_generation_node", () => {
218-
it("creates generation node with assetId", async () => {
219-
const result = await tools.create_generation_node.execute!(
218+
it("creates an action first and assigns asset identity only to its output child", async () => {
219+
const created = await tools.create_generation_node.execute!(
220220
{
221221
node_type: "image_gen",
222222
label: "AI Cat",
@@ -225,8 +225,35 @@ describe("Canvas tools", () => {
225225
{ toolCallId: "1", messages: [] }
226226
);
227227

228-
expect(result).toContain("Created generation node gen-id-1");
229-
expect(result).toContain("assetId gen-id-2");
228+
expect(created).toBe("Created generation node gen-id-1");
229+
const action = doc.getMap("nodes").get("gen-id-1") as any;
230+
expect(action.data).not.toHaveProperty("assetId");
231+
232+
const started = await tools.run_generation_node.execute!(
233+
{ node_id: "gen-id-1" },
234+
{ toolCallId: "2", messages: [] },
235+
);
236+
expect(started).toBe(
237+
"Started generation: created pending image node gen-id-2",
238+
);
239+
240+
const output = doc.getMap("nodes").get("gen-id-2") as any;
241+
expect(output.type).toBe("image");
242+
expect(output.data).not.toHaveProperty("assetId");
243+
doc.getMap("nodes").set("gen-id-2", {
244+
...output,
245+
data: {
246+
...output.data,
247+
status: "completed",
248+
assetId: "asset-ai-cat",
249+
},
250+
});
251+
expect((doc.getMap("nodes").get("gen-id-1") as any).data).not.toHaveProperty(
252+
"assetId",
253+
);
254+
expect(
255+
(doc.getMap("nodes").get("gen-id-2") as any).data.assetId,
256+
).toBe("asset-ai-cat");
230257
});
231258

232259
it("creates video_gen node", async () => {

‎apps/api-cf/src/agents/tools/canvas.ts‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ export function createCanvasTools(
226226
});
227227

228228
const createGenerationNode = tool({
229-
description: "Create a new image, video, audio, or text generation node on the canvas. Pass the generation prompt directly. Returns nodeId and assetId.",
229+
description: "Create a new image, video, audio, or text generation node on the canvas. Pass the generation prompt directly. Returns the action nodeId; run it to create an output node.",
230230
inputSchema: z.object({
231231
node_type: z.enum(GENERATION_NODE_TYPES).describe("Generation node type: image_gen, video_gen, audio_gen, or text_gen"),
232232
label: z.string().describe("Display label"),
@@ -241,7 +241,6 @@ export function createCanvasTools(
241241
try {
242242
const resolvedParent = parent_id ?? getWorkspaceGroupId() ?? null;
243243
const nodeId = generateId();
244-
const assetId = generateId();
245244

246245
// Resolve model defaults from MODEL_CARDS
247246
const kind =
@@ -257,7 +256,7 @@ export function createCanvasTools(
257256
: MODEL_CARDS.find(c => c.kind === kind);
258257
const modelId = modelCard?.id || model_name || "";
259258

260-
log.info("create_generation_node creating node", { nodeId, assetId, modelId, resolvedParent });
259+
log.info("create_generation_node creating node", { nodeId, modelId, resolvedParent });
261260

262261
const data: Record<string, unknown> = {
263262
label,
@@ -277,12 +276,10 @@ export function createCanvasTools(
277276
};
278277
stampActor(data);
279278

280-
const result = canvas.createNode(nodeId, node_type, data, position, resolvedParent, assetId);
279+
const result = canvas.createNode(nodeId, node_type, data, position, resolvedParent);
281280
log.info("create_generation_node result", { node_id: result.node_id, asset_id: result.asset_id, error: result.error });
282281
if (result.error) return `Error: ${result.error}`;
283-
const response = result.asset_id
284-
? `Created generation node ${result.node_id} with assetId ${result.asset_id}`
285-
: `Created generation node ${result.node_id}`;
282+
const response = `Created generation node ${result.node_id}`;
286283
log.info("create_generation_node returning", { response });
287284
return response;
288285
} catch (e) {

‎apps/docs/guide/architecture.md‎

Lines changed: 75 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,42 @@ windowing, and local runtime wiring belong to Desktop controllers. Until the ext
158158
claim that every product view already lives in `@clash/gui`. Web must share Desktop's GUI, not
159159
Desktop business logic.
160160

161-
## Asset system
161+
## Asset + Generator system
162+
163+
The native semantic model has two first-class concepts: Assets are durable
164+
referenced facts, while a Generator is versioned Project state with one or more
165+
named Actions that materialize new Assets. Actions are methods of a Generator
166+
Definition, not independently mutable Project entities. Media uses the
167+
ProjectAsset/Resource authority below; typed structured content uses immutable
168+
[Document Asset revisions](/guide/document-assets). See
169+
[Asset + Generator Model](/guide/asset-generator-model) for the native
170+
Definition, revision, Action, Run, output, and migration contract.
171+
172+
The Generator v2 schemas, Project Loro authority, Local Run bridge, plugin ABI,
173+
and first-party Codex ImageGen artifact are delivered. The Local Host HTTP
174+
surface now lists/resolves Definitions, creates/reads Project Generators,
175+
advances versioned heads by observed-head CAS, submits/reads Runs, and reads
176+
Output Commits. A materialized COW revision rejects head advancement with an
177+
explicit fork hint; the existing create route records `forkedFrom` lineage.
178+
There is no separate fork route or Project Generator collection listing, and
179+
CLI/MCP/GUI adapters are not yet wired. Canvas, Timeline, Director Stage, and
180+
inline edit remain on their existing product models. ASR additionally has a
181+
native first-party Generator path, while its existing Timeline and synchronous
182+
endpoint consumers remain on the legacy model.
183+
184+
Typed Document contracts, the built-in registry, Project authority, Host body
185+
service, and Local list/create/read/history/CAS/attachment routes are also
186+
delivered. CLI/MCP/native file projection, standard-consumer wiring, and legacy
187+
metadata migration are not. Native ASR publication is delivered, but Timeline
188+
and other legacy ASR consumers are not migrated.
189+
190+
ASR migration is split deliberately. The strict `speech.transcribe` Broker/SDK
191+
contract, reserved broker enforcement, `clash.asr` bundled package,
192+
Generator/executor, server/runtime injection, and native Run-to-Document path
193+
are delivered. Legacy route replacement, Timeline wiring, and consumer
194+
migration are not delivered.
195+
196+
## Media Asset system
162197

163198
The canonical Local Host authority, resolver, and consumer-CAS implementation
164199
described in this section is current. Every post-cutover Local publication that
@@ -248,29 +283,32 @@ The future Cloud adapter will record stable Resource-to-OSS bindings in the
248283
cloud Resource Registry; it must not introduce a second Project sync envelope
249284
or put OSS keys in Loro. Its target behavior allows local-origin structure to
250285
synchronize before silent OSS upload finishes, while other Hosts reject
251-
byte-dependent work until the Resource is ready. A cloud-origin ActionRun and
252-
placeholder may likewise synchronize early, but its output ProjectAsset and
253-
binding may appear only after verified OSS staging. Other devices will then
254-
download and verify ready Resources asynchronously.
255-
256-
The target execution rule follows the initiating surface rather than a device
257-
that later observes synchronized state: Web will use the cloud task runtime,
258-
while Desktop, CLI, and MCP use the designated local-api Host. Only the Local
259-
adapter of this unified protocol is implemented today. It persists the shared
260-
Durable Run Engine and step graph through SQLite plus local CAS. The future Cloud adapter must reuse
261-
that graph with Workflow state plus OSS staging. In both adapters, an ambiguous
262-
interrupted submit may be attempted again as an explicit availability trade-off;
263-
once a Provider task token is checkpointed, recovery only polls that task, and
264-
publication remains idempotent. Attempt journals and Provider tokens stay
265-
owner-private and Project Loro never holds a transaction open across an
266-
external request. The current Local projection carries Canvas node outcome and
267-
`ActionAssetBinding` lineage, not a standalone `ActionRun`. The future unified
268-
Project entity may carry only the five coarse ActionRun states; it must never
269-
carry private submit/poll phases or become the execution journal.
286+
byte-dependent work until the Resource is ready. A future Web-submitted native
287+
Run and product projection may likewise synchronize early while its cloud owner
288+
remains a private Task fact, but its output ProjectAsset and Output Commit may
289+
appear only after verified OSS staging. Other devices will then download and
290+
verify ready Resources asynchronously.
291+
292+
Execution follows the initiating surface rather than a device that later
293+
observes synchronized state: Web will use the future cloud task runtime, while
294+
Desktop, CLI, and MCP use the designated local-api Host. Only the Local adapter
295+
of the shared Durable Run Engine is implemented today. It persists its private
296+
six-phase Task through SQLite plus local CAS; the future Cloud adapter must
297+
reuse that graph with Workflow state plus OSS staging. Attempt journals and
298+
Provider tokens stay owner-private, and Project Loro never holds a transaction
299+
open across an external request.
300+
301+
Native Generator execution does have a standalone Project Loro Action Run with
302+
the four public states `pending`, `running`, `succeeded`, and `failed`. It pins
303+
one Generator Revision, Action, semantic executor, invocation, and output
304+
contract. Legacy Canvas, Timeline, Director, and Provider paths still project
305+
through their existing node and `ActionAssetBinding` models even when they use
306+
the same private Durable Run Engine; they have not thereby become native
307+
Generator Runs.
270308

271309
See [Durable Run Protocol](/guide/durable-run-protocol) for the shared step
272-
graph, checkpoint and idempotency rules, collaboration projection, owner-only
273-
recovery, and the future Cloud adapter ports.
310+
graph, checkpoint and idempotency rules, the native-Generator/legacy-product
311+
split, owner-only recovery, and the future Cloud adapter ports.
274312

275313
Canonical Asset deletion is explicit and split into two lifecycles. The current
276314
Local Host atomically checks Action bindings and changes the ProjectAsset to
@@ -325,7 +363,8 @@ UI/CLI submits task to local-api
325363
→ host resolves route (card × provider implementation)
326364
→ host binds the selected Provider account to the invocation
327365
→ Durable Run Engine claims and checkpoints one submit or poll step
328-
→ plugin host invokes the plugin's provider-executor once over stdio
366+
→ plugin host invokes the provider-executor endpoint once
367+
(bundled first-party module or supervised process/stdio package)
329368
→ plugin reads account-scoped state through context.store
330369
→ plugin performs exactly one Provider operation with its own fetch/Axios/client
331370
(one submit, or one status poll plus completed-result conversion)
@@ -336,15 +375,19 @@ UI/CLI submits task to local-api
336375
```
337376

338377
The manifest declares only what the plugin contributes. Network and filesystem
339-
access are ordinary process capabilities; provider traffic recording and replay
340-
are test-runner instrumentation, not branches in plugin business code. Provider
341-
plugins never own retry loops, total task lifetime, restart recovery, or Project
342-
publication; those belong to the Host's durable step graph.
378+
access are ordinary runtime capabilities of the trusted plugin code; the
379+
process realm is fault isolation, not a security sandbox. Provider traffic
380+
recording and replay are test-runner instrumentation, not branches in plugin
381+
business code. Provider plugins never own retry loops, total task lifetime,
382+
restart recovery, or Project publication; those belong to the Host's durable
383+
step graph.
343384

344385
## Kinds
345386

346-
`ModelKind = 'image' | 'video' | 'audio' | 'text' | 'asr'`. ASR is a
347-
first-class kind: the five local ASR cards sit in the same registry and route
348-
through the same composition as image/video/audio cards, with `providerId:
349-
"local"` implementations that run on-device (see
350-
[Local ASR](/guide/local-asr)).
387+
`ModelKind = 'image' | 'video' | 'audio' | 'text'`. Transcription cards are
388+
text-output cards identified by an exact audio input contract and an `asr_model`
389+
runtime parameter; `asr` is a workflow/capability, not a fifth output kind. The
390+
five local cards sit in the same registry and run on-device (see
391+
[Local ASR](/guide/local-asr)). The native `clash.asr` Generator publishes a
392+
`media.transcript@1` Document Asset; the existing endpoint and Timeline flow
393+
have not yet migrated to it.

0 commit comments

Comments
 (0)