From cdcec788213551e42033314c37373a08d87d9d73 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Fri, 14 Nov 2025 16:42:29 -0800 Subject: [PATCH 1/2] feat(runner/llm-builtin): expose schema on result self-link and use in llm tools also makes a bunch of parts that were async sync now, which might fix some races --- packages/runner/src/builtins/llm-dialog.ts | 60 +++++++--------------- packages/runner/src/runner.ts | 9 +++- 2 files changed, 27 insertions(+), 42 deletions(-) diff --git a/packages/runner/src/builtins/llm-dialog.ts b/packages/runner/src/builtins/llm-dialog.ts index ed0912d93..ec6819f64 100644 --- a/packages/runner/src/builtins/llm-dialog.ts +++ b/packages/runner/src/builtins/llm-dialog.ts @@ -83,38 +83,17 @@ function normalizeInputSchema(schemaLike: unknown): JSONSchema { * - Prefer a non-empty recipe.resultSchema if recipe is loaded * - Otherwise derive a simple object schema from the current value */ -async function getCharmResultSchemaAsync( - runtime: IRuntime, - charm: Cell, -): Promise { - try { - const source = charm.getSourceCell(); - const recipeId = source?.get()?.[TYPE]; - if (recipeId) { - await runtime.recipeManager.loadRecipe(recipeId, charm.space); - } - return ( - getLoadedRecipeResultSchema(runtime, charm) ?? - buildMinimalSchemaFromValue(charm) - ); - } catch (_e) { - return buildMinimalSchemaFromValue(charm); - } -} - -function getLoadedRecipeResultSchema( - runtime: IRuntime | undefined, - charm: Cell, +function getCellSchema( + cell: Cell, ): JSONSchema | undefined { - const source = charm.getSourceCell(); - const recipeId = source?.get()?.[TYPE]; - const recipe = recipeId - ? runtime?.recipeManager.recipeById(recipeId) - : undefined; - if (recipe?.resultSchema !== undefined) { - return recipe.resultSchema; - } - return undefined; + return cell.schema ?? + // If cell has a source cell, any resultRef has a schema attached + cell.getSourceCell<{ resultRef: Cell }>({ + type: "object", + properties: { resultRef: { asCell: true } }, + })?.get()?.resultRef?.schema ?? + // Otherwise, derive a simple object schema from the current value + buildMinimalSchemaFromValue(cell); } function buildMinimalSchemaFromValue(charm: Cell): JSONSchema | undefined { @@ -828,11 +807,11 @@ function buildToolCatalog( * from the attachments list. This is appended to the system prompt so the LLM * has immediate context about available charms without needing to call schema() first. */ -async function buildAttachmentsSchemaDocumentation( +function buildAttachmentsSchemaDocumentation( runtime: IRuntime, space: MemorySpace, attachments: Cell, -): Promise { +): string { const currentAttachments = attachments.get() || []; if (currentAttachments.length === 0) { return ""; @@ -855,7 +834,7 @@ async function buildAttachmentsSchemaDocumentation( } // Get schema for the cell - const schema = await getCharmResultSchemaAsync(runtime, cell); + const schema = getCellSchema(cell); if (schema) { const schemaJson = JSON.stringify(schema, null, 2); schemaEntries.push( @@ -1371,11 +1350,10 @@ function handleListAttachments( /** * Handles the schema tool call. */ -async function handleSchema( - runtime: IRuntime, +function handleSchema( resolved: ResolvedToolCall & { type: "schema" }, -): Promise<{ type: string; value: any }> { - const schema = await getCharmResultSchemaAsync(runtime, resolved.charm) ?? +): { type: string; value: any } { + const schema = getCellSchema(resolved.charm) ?? {}; const value = JSON.parse(JSON.stringify(schema ?? {})); return { type: "json", value }; @@ -1557,7 +1535,7 @@ async function invokeToolCall( } if (resolved.type === "schema") { - return await handleSchema(runtime, resolved); + return handleSchema(resolved); } if (resolved.type === "read") { @@ -1798,7 +1776,7 @@ export function llmDialog( }; } -async function startRequest( +function startRequest( tx: IExtendedStorageTransaction, runtime: IRuntime, space: MemorySpace, @@ -1820,7 +1798,7 @@ async function startRequest( const toolCatalog = buildToolCatalog(runtime, toolsCell); // Build charm schemas documentation from attachments and append to system prompt - const charmSchemasDocs = await buildAttachmentsSchemaDocumentation( + const charmSchemasDocs = buildAttachmentsSchemaDocumentation( runtime, space, attachments, diff --git a/packages/runner/src/runner.ts b/packages/runner/src/runner.ts index 10666a6bf..e63d788a9 100644 --- a/packages/runner/src/runner.ts +++ b/packages/runner/src/runner.ts @@ -302,7 +302,14 @@ export class Runner implements IRunner { processCell.withTx(tx).setRaw({ ...processCell.getRaw({ meta: ignoreReadForScheduling }), [TYPE]: recipeId || "unknown", - resultRef: resultCell.getAsLink({ base: processCell }), + resultRef: (recipe.resultSchema !== undefined + ? resultCell.asSchema(recipe.resultSchema).getAsLink({ + base: processCell, + includeSchema: true, + }) + : resultCell.getAsLink({ + base: processCell, + })), internal, ...(recipeId !== undefined) ? { spell: getSpellLink(recipeId) } : {}, }); From 5364e8d5238a13d36077dfaafc930e4782c99f7b Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Fri, 14 Nov 2025 16:50:56 -0800 Subject: [PATCH 2/2] allow schema tool for all cells + clean up redundancy --- packages/runner/src/builtins/llm-dialog.ts | 59 +++++++--------------- 1 file changed, 18 insertions(+), 41 deletions(-) diff --git a/packages/runner/src/builtins/llm-dialog.ts b/packages/runner/src/builtins/llm-dialog.ts index ec6819f64..bff55399d 100644 --- a/packages/runner/src/builtins/llm-dialog.ts +++ b/packages/runner/src/builtins/llm-dialog.ts @@ -868,7 +868,7 @@ type ResolvedToolCall = | { type: "listAttachments"; call: LLMToolCall } | { type: "addAttachment"; call: LLMToolCall; path: string; name: string } | { type: "removeAttachment"; call: LLMToolCall; path: string } - | { type: "schema"; call: LLMToolCall; charm: Cell } + | { type: "schema"; call: LLMToolCall; cellRef: Cell } | { type: "read"; call: LLMToolCall; cellRef: Cell } | { type: "navigateTo"; call: LLMToolCall; cellRef: Cell } | { @@ -954,42 +954,31 @@ function resolveToolCall( }; } + const target = extractStringField( + toolCallPart.input, + "path", + "/of:bafyabc123/path", + ); + + const link = parseLLMFriendlyLink(target, space); + const cellRef = runtime.getCellFromLink(link); + if (name === SCHEMA_TOOL_NAME) { const charmName = extractStringField( toolCallPart.input, "path", "/of:bafyabc123/path", ); - const charm = catalog.charmMap.get(charmName); - if (!charm) { - throw new Error( - `Unknown charm "${charmName}". Use listAttachments() for options.`, - ); - } return { type: "schema", - charm, + cellRef, call: { id, name, input: { charm: charmName } }, }; } - const target = extractStringField( - toolCallPart.input, - "path", - "/of:bafyabc123/path", - ); - - const link = parseLLMFriendlyLink(target, space); - if (name === READ_TOOL_NAME) { // Get cell reference from the link - works for any valid handle - const cellRef = runtime.getCellFromLink(link); - if (!cellRef) { - throw new Error( - `Could not resolve handle "${id}" to a cell. The handle may not exist in this space.`, - ); - } - if (isStream(cellRef)) { + if (isStream(cellRef.resolveAsCell())) { throw new Error(`Path resolves to a handler; use run("${target}").`); } @@ -1001,14 +990,6 @@ function resolveToolCall( } if (name === NAVIGATE_TO_TOOL_NAME) { - // Get cell reference from the link - works for any valid handle - const cellRef = runtime.getCellFromLink(link); - if (!cellRef) { - throw new Error( - `Could not resolve handle "${id}" to a cell. The handle may not exist in this space.`, - ); - } - return { type: "navigateTo", cellRef, @@ -1016,17 +997,14 @@ function resolveToolCall( }; } - // For run(), resolve the cell and check if it's a handler or pattern - const cellRef: Cell = runtime.getCellFromLink(link); - // Get optional charm metadata for validation (only used for handlers) const charmEntry = catalog.handleMap.get(link.id); const charm = charmEntry?.charm; - if (isStream(cellRef)) { + if (isStream(cellRef.resolveAsCell())) { return { type: "run", - handler: cellRef as any, + handler: cellRef as unknown as Stream, charm, call: { id, @@ -1036,13 +1014,13 @@ function resolveToolCall( }; } - const pattern = (cellRef as Cell).key("pattern") + const pattern = cellRef.key("pattern") .getRaw() as unknown as Readonly | undefined; if (pattern) { return { type: "run", pattern, - extraParams: (cellRef as Cell).key("extraParams").get() ?? {}, + extraParams: cellRef.key("extraParams").get() ?? {}, charm, call: { id, @@ -1353,9 +1331,8 @@ function handleListAttachments( function handleSchema( resolved: ResolvedToolCall & { type: "schema" }, ): { type: string; value: any } { - const schema = getCellSchema(resolved.charm) ?? - {}; - const value = JSON.parse(JSON.stringify(schema ?? {})); + const schema = getCellSchema(resolved.cellRef) ?? {}; + const value = JSON.parse(JSON.stringify(schema)); return { type: "json", value }; }