Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .agents/skills/floatboat-workbench/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ These topics are also available without installing dependencies through `node to
| Browser connection, theme and Host contributions | [ui](references/ui.md) |
| Business tables, migrations and transaction boundaries | [database](references/database.md) |
| Workflow/Combo declarations and portable file resources | [resources](references/resources.md) |
| Model text, structured output, streaming, conversations, tool calls, reasoning, image input, speech, image generation, Jev decisions, embeddings, reranking or model list | [llm](references/llm.md) |
| Model text, structured output, streaming, conversations, tool calls, reasoning, image input, speech, image generation, Jev decisions, reranking or model list | [llm](references/llm.md) |
| Agent tasks with tools and an explicit execution directory | [agent](references/agent.md) |
| Streaming speech recognition | [asr](references/asr.md) |
| Starting or observing a declared Workflow | [workflows](references/workflows.md) |
Expand Down
36 changes: 33 additions & 3 deletions .agents/skills/floatboat-workbench/references/agent.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Agent tasks

Declare `agent` in manifest.capabilities. On the server, use `app.host.agent.start({ prompt, workingDirectory, idempotencyKey, model?, timeoutMs? })` for reasoning plus tool execution. workingDirectory is an explicit real absolute execution directory; it is not implicitly the workspace cwd. A declared resource directory can provide that path. The Host fixes workspace/session ownership; callers do not select sessionId or workspaceId.
Declare `agent` in manifest.capabilities. On the server, use `app.host.agent.start({ prompt, workingDirectory, idempotencyKey, model?, timeoutMs?, instructions? })` for reasoning plus tool execution. `instructions` (up to 30000 characters) is appended to the Agent system prompt: use it for tool usage notes and business rules. It is not a message, is not shown in Host chat and never appears in `conversations.messages`, so do not prepend such text to `prompt` or the first user turn. Omitting `model` uses the same default as llm calls (`gemini-3.8-flash`, or the first available platform model when that one is not allowed); a local model runs only when named explicitly. workingDirectory is an explicit real absolute execution directory; it is not implicitly the workspace cwd. A declared resource directory can provide that path. The Host fixes workspace/session ownership; callers do not select sessionId or workspaceId.

Persist the business operation ID before start and reuse it as idempotencyKey. Save the returned runId, use get/list/findByIdempotencyKey to recover state, events({runId,afterSequence}) for progress, and cancel({runId}) to cancel. Same-key changed input is a conflict. On timeout or disconnection, query the original operation before retrying.

Expand All @@ -14,11 +14,13 @@ The SDK resumes interrupted Agent event subscriptions from the last delivered cu

Persist the last applied sequence with business state. If retry attempts are exhausted, read agent.get, show its partial text while running and reopen events using the saved cursor. For an expired cursor, reconcile the current snapshot and poll get for partial text and terminal status; do not reuse the expired cursor or leave the UI permanently loading. Task execution timeoutMs is separate from the subscription timeout.

Current Floatboat persists assistant text and tool calls/results during execution and displays them in the Host chat session. SDK events intentionally expose sanitized progress, not raw tool arguments/results or internal reasoning. The workbench owns its business history and loading/error states; Host persistence does not automatically render them in your page.
Current Floatboat persists assistant text and tool calls/results during execution and displays them in the Host chat session. By default SDK events carry sanitized progress: text, tool names with success/failure, status, and one `usage` event per turn with `promptTokens`/`cachedPromptTokens`/`completionTokens` for every model call plus the total. The usage is not split into system prompt, tool definitions and history. Internal reasoning is never exposed. The workbench owns its business history and loading/error states; Host persistence does not automatically render them in your page.

When the page really needs tool records (for example a context inspector), declare both `agent` and `agent-tool-details` in manifest.capabilities. Then `tool-started` also carries `input` (JSON text, at most 4000 characters) and `tool-finished` carries `output` (at most 8000 characters) and `error`; `inputTruncated`/`outputTruncated` are true when text was cut. These values can contain workspace files and command output: show them only where needed and never log or send them elsewhere. The declaration is visible in review.

## Multi-turn conversations

For a chat UI, create one conversation and persist its conversationId in business SQLite. Use a new idempotencyKey for each user send and reuse that key only when retrying that same send. A new conversation starts a separate context; agent.start remains an independent task.
For a chat UI, create one conversation and persist its conversationId in business SQLite. Pass `instructions` to `conversations.create` when every turn needs the same background; it is fixed for the life of the conversation and changing it under the same idempotencyKey is a conflict. Use a new idempotencyKey for each user send and reuse that key only when retrying that same send. A new conversation starts a separate context; agent.start remains an independent task.

```ts
const conversation = await app.host.agent.conversations.create({
Expand All @@ -39,3 +41,31 @@ for await (const event of app.host.agent.events(run)) {
Host restores prior assistant and tool history before the next turn; do not concatenate old messages into prompt. Use conversations.get/list and conversations.messages({conversationId,offset,limit}) after refresh; nextOffset is null at the end. Messages expose only user/assistant text; complete tool details remain in Host chat. The directory is fixed at create; a different execution directory requires a new conversation. These APIs use the existing agent capability and workbench execution permission.

While one turn is running, another new send returns AGENT_CONVERSATION_BUSY. Cancel with agent.cancel({runId}), wait for its terminal status, then send the next turn. Cancelling a subscription does not cancel the run. Account changes or Host shutdown interrupt the current run without erasing the owning account's conversation. Verify a follow-up referring to a prior fact and a tool result, refresh recovery, cancellation followed by another send, and the busy state. These APIs require a Floatboat build that advertises agent/conversationCreate and agent/conversationSend through capabilities.list; older clients must be upgraded.

## Agent tools

Let the Agent call this workbench's own business functions with native tools instead of reading notes and calling the app API through bash or HTTP:

```ts
app.agentTool(
{
name: 'library_add',
description: 'Save a finished design into the library. Use after the user approves a draft.',
parameters: {
type: 'object',
properties: { title: { type: 'string' }, html: { type: 'string' } },
required: ['title', 'html'],
additionalProperties: false,
},
},
async ({ input, runId, conversationId, signal }) => saveToLibrary(input, { runId, conversationId, signal }),
);
```

`name` starts with a lowercase letter, uses single underscores between lowercase letters/digits, and has at most 40 characters. `parameters` is a JSON Schema whose top level is `type: 'object'`. Register tools at startup, before `listen()`; invalid or duplicate definitions throw.

Tools are offered only to Agent runs that this workbench starts (`agent.start` and conversation turns), and the model sees them as `mcp__aoe-host__app_<name>`. The Host validates arguments against `parameters` first; invalid calls never reach the handler. A string return value goes to the model unchanged, other values are serialized as JSON, and an error with a `code` property reaches the model as that code and message. `signal` aborts when the run is cancelled. In events, `tool-started.appTool` names the workbench tool. Tools are available when the Floatboat Host runs the app (after open), not in standalone `pnpm dev` preview.

The build records `agent.tools` and `agent.instructions` in `manifest.requires.operations` when the code uses them, so older Floatboat clients ask the user to upgrade.

Skills that ship with a ported plugin belong in a bundled Combo dependency (`manifest.dependencies` with `kind: 'combo'`); the Host loads declared Combos before every Agent run.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Read and update `app/ENGINEERING.md` for the actual entrypoints, module responsi

Use strict TypeScript and unknown plus validation for external JSON. The compiler rejects explicit any, type-check suppression and server/Node imports from UI. Edit source and the declarative manifest/build configuration; `app/dist/`, `app/sdk/` and `.generated/` are generated. Application Vite/PostCSS configs, plugins and package scripts are not the Host build interface.

For an additional pure-JS runtime dependency, choose an exact compatible version and update both the root and `app/` package/lock files using pnpm (`pnpm add --save-exact package@version`, then the corresponding command from `app/`). The root toolchain supplies TypeScript, Vite and esbuild. Native modules are outside this application profile. Checking verifies the lock against installed versions; it does not repair dependencies through network access.
For an additional pure-JS runtime dependency, choose an exact compatible version and update both the root and `app/` package/lock files using pnpm (`pnpm add --save-exact package@version`, then the corresponding command from `app/`). The root toolchain supplies TypeScript, Vite and esbuild. Native modules are outside this application profile. Checking verifies the lock against installed versions; it does not repair dependencies through network access. Code cannot be downloaded or loaded by package name at runtime: the build rejects dynamic `import()`/`require` with non-literal paths and unbundled external dependencies, so review can see all code. When porting a plugin-based app, install the plugins you need as dependencies before building and import them with fixed paths.

## Metadata

Expand Down
23 changes: 13 additions & 10 deletions .agents/skills/floatboat-workbench/references/llm.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Declare `llm` in manifest.capabilities. Call the SDK on the server; the browser
## Text, structured output and streaming

- `app.host.llm.generateText({ prompt, ...options })` returns text, model, requestId, finishReason, optional usage and an `assistant` message you can append to a conversation.
- `generateObject({ prompt, schema })` validates output against JSON Schema. Treat the returned object as unknown until checked against the business type.
- `generateObject({ prompt, schema })` validates output against JSON Schema. The Host first extracts JSON from a code fence or surrounding prose; text that still is not JSON fails with `LLM_OBJECT_INVALID` and a schema mismatch with `LLM_OBJECT_SCHEMA_MISMATCH` (both executed, so retry deliberately). Treat the returned object as unknown until checked against the business type.
- `streamText({ prompt })` returns an async iterable with delta, tool-call, result and done events. Preserve incomplete/interrupted state if the stream ends without the required terminal result.
- With `prompt`, all three accept `images` (up to 8, each at most 10 MiB): `{ resource: { resourceId, relativePath } }` for a file in a declared directory resource, `{ url: 'https://…' }` that the Host downloads, or a small `{ dataUrl: 'data:image/png;base64,…' }` (the whole request is limited to 2 MiB). Only PNG, JPEG, WebP and GIF are accepted. A model or protocol without image input returns `LLM_IMAGE_INPUT_UNSUPPORTED` before any charge; check `inputs` from `listModels` first.

Expand All @@ -30,37 +30,40 @@ for (let round = 0; round < 5; round++) {

## Model catalog

`listModels({ kind? })` returns `models` (`id`, `kinds` of `text`/`speech`/`image`/`decision`/`embedding`/`rerank`, `inputs` of `text`/`image`, `source`), per-kind `defaults`, and `media`. Text models follow the same release, membership and local-model rules as the Floatboat chat model picker. Speech, image, decision, embedding and rerank models come from the platform catalog; when `media` is `unavailable` that part may be incomplete, so keep an explicit retry instead of treating it as "no models". Choose models from this result rather than hard-coding names.
`listModels({ kind? })` returns `models` (`id`, `kinds` of `text`/`speech`/`image`/`decision`/`rerank`, `inputs` of `text`/`image`, `source`), per-kind `defaults`, and `media`. Text models follow the same release, membership and local-model rules as the Floatboat chat model picker. `defaults.text` is the text default (`gemini-3.8-flash` when allowed); local models are listed with `source: 'local'` but are never a default. Speech, image, decision and rerank models come from a list Floatboat configures remotely (the first entry of each kind is its default), so `media` is always `available`; the platform may change these lists without an app update. Choose models from this result rather than hard-coding names.

## Speech and images

Both write results into a manifest resource with `kind: 'directory'` and `access: 'read-write'`; pass a logical `output: { resourceId, relativePath }`, never an absolute path. Results return the resource reference, byte length and format/MIME only.

- `generateSpeech({ text, voice?, format?, speed?, instructions?, model?, output })` synthesizes up to 4096 characters. `format` is `mp3` (default), `wav`, `opus`, `aac`, `flac` or `pcm`.
- `generateSpeech({ text, voice?, format?, speed?, instructions?, model?, output })` synthesizes up to 4096 characters. `voice` is a provider voice ID (an ElevenLabs voice ID, or a Chirp name such as `cmn-CN-Chirp3-HD-Kore`); leave it empty for the model's default voice and do not pass OpenAI names like `alloy`. Platform speech models generally support `mp3` (default), `wav`, `opus` and `pcm` (24 kHz, 16-bit mono), and most reject `instructions`.
- `generateImage({ prompt, size?, quality?, count?, images?, model?, output })` creates 1–4 images. With `images` it edits from those references. For `count > 1`, files get `-1`, `-2` before the extension. The default request deadline is 330 seconds; show progress instead of blocking the page.

Omitting `model` uses the catalog default for that kind; if the catalog is unavailable the call returns `LLM_MODEL_REQUIRED`.
Omitting `model` uses the catalog default for that kind; a kind with no models yet (rerank today) has no default, so the call returns `LLM_MODEL_REQUIRED` until you pass an explicit `model`.

## Decisions (Jev)

`decide({ state, questions, model? })` asks the platform decision model (default `jev-latest`) structured questions about `state` (string, object or array). `questions` holds 1–32 entries keyed by `[A-Za-z0-9_-]{1,64}`, each `{ type: 'noul' | 'choice' | 'score', instructions, criteria? }`; `choice` and `score` require `criteria`. It returns the raw `answers`. Use it for yes/no, option and scoring judgements; it is cheaper and steadier than prompting a text model, and does not generate prose.
`decide({ state, questions, model? })` asks the platform decision model (default `jev-latest`) structured questions about `state` (string, object or array). `questions` holds 1–32 entries keyed by `[A-Za-z0-9_-]{1,64}`, each `{ type: 'noul' | 'choice' | 'score', instructions, criteria? }`. For `choice`, `criteria` is an object mapping each option name to its description (1–255 entries); for `score`, it is an ordered array of levels scored from 0 (1–10 entries); `noul` optionally takes `{ true, false }` descriptions. Other shapes return `LLM_DECISION_CRITERIA_INVALID` before any charge. It returns the raw `answers`. Use it for yes/no, option and scoring judgements; it is cheaper and steadier than prompting a text model, and does not generate prose.

```ts
const result = await app.host.llm.decide({
state: { message: ticket.body },
questions: { urgent: { type: 'noul', instructions: 'Does the customer need a reply within an hour?' } },
questions: {
urgent: { type: 'noul', instructions: 'Does the customer need a reply within an hour?' },
topic: { type: 'choice', instructions: 'What is the message about?', criteria: { billing: 'Invoices or charges', bug: 'Something is broken' } },
tone: { type: 'score', instructions: 'How upset is the customer?', criteria: ['Calm', 'Annoyed', 'Angry'] },
},
});
```

## Retrieval: embeddings and reranking
## Retrieval: reranking

- `embed({ model, input, dimensions? })` requires an explicit model (pick one from `listModels({ kind: 'embedding' })`) and takes up to 256 texts. Vectors from different models are not comparable, so store the model ID next to each vector.
- `rerank({ query, documents, topN?, model? })` returns `{ index, relevanceScore }` sorted by score, with `index` pointing into `documents` (up to 1000).
- The Host does not store vectors. Keep them in your app's SQLite as BLOBs and compare them with `sdk/vectors.mts`: `vectorToBlob`, `vectorFromBlob`, `cosineSimilarity` and `topK`. A full scan of 10,000 vectors at 1536 dimensions takes about 20 ms; use `topK` for recall, then `rerank` the shortlist.
- For a small candidate set, rerank all documents directly and show the matching originals as sources.

## Required Host operations

`pnpm workbench:check` and Floatboat builds scan the server code and write Host operations newer than the baseline (for example `llm.embed`, or `llm.messages` / `llm.tools` / `llm.reasoning` / `llm.images` when text calls use those fields) to `manifest.requires.operations`. Older Floatboat clients then ask the user to update instead of failing at runtime. Commit the field with your code; the build only adds entries, so remove one by hand only when the code no longer uses it.
`pnpm workbench:check` and Floatboat builds scan the server code and write Host operations newer than the baseline (for example `llm.rerank`, or `llm.messages` / `llm.tools` / `llm.reasoning` / `llm.images` when text calls use those fields) to `manifest.requires.operations`. Older Floatboat clients then ask the user to update instead of failing at runtime. Commit the field with your code; the build only adds entries, so remove one by hand only when the code no longer uses it.

## Errors and billing

Expand Down
Loading
Loading