Aller au contenu

The AI assistant and AI help

The built-in ai plugin owns the connection settings (API address, key, model, instructions, history size — Settings › AI assistant), the conversation engine, the sessions and the tool registry. It talks to any OpenAI-compatible endpoint through the ai platform service (src/services/ai-openai.ts), so nothing is hard-coded and the key stays on the machine. It contributes no panel of its own: it is a factory of assistants.

The service delegates model access to @scfloworg/ai, the neutral package copied from scflow-core (ADR-0010): the streamed transport, the model profiles (which parameters a model takes: reasoning models such as o3 or gpt-5 get no temperature and take max_completion_tokens, so there is no temperature setting), one retry without a parameter the model refuses, errors normalized as AiError with a kind (auth, rate-limit, context-length, unsupported-parameter, model-not-found, network, provider) that the chat explains to the user, and a usage report per chat (ctx.getServices().ai.onUsage?.(listener): model, tokens, duration, outcome). The conversation types of ctx.getServices().ai (AiMessage, AiChatRequest, AiChunk, AiToolSpec, AiUsage) are those of the package and come inlined in the plugin API: no plugin needs a copy of @scfloworg/ai. A tool you declare or register keeps parameters; the ai plugin hands it to the package as inputSchema (the MCP name). Only code that calls ai.chat() itself passes tools with inputSchema.

Model tiers. The settings name a fast model (model) and an optional strong one (strongModel). When the strong model is set and differs from the fast one, the service gives the connection models: { fast, strong }, so the tier of an AiChatRequest picks the model (docs/ai.md#model-tiers of @scfloworg/ai); without it every tier uses model, as before. The assistant keeps its own loop, because actions wait for the user’s approval between turns, and follows the escalation rule of runAgent: a request starts fast, moves to strong for the rest of the request after a turn where a call failed (arguments that are not JSON, an unknown tool, an execution that threw; a call the user refused does not count), and asks the same turn again with strong when the fast request fails with AiError kind unsupported-parameter. Each switch adds a note line to the conversation, never sent to the model. The question box offers a switch to ask the strong model from the start (ai/message.send with strong: true). The pure rules live in src/plugins/ai/tiers.ts.

Any plugin can give the model tools. Declare them in your ui.json:

"tools": [
{
"name": "list_commands",
"description": "tool.commands",
"kind": "read",
"handler": "listCommands",
"parameters": { "type": "object", "properties": {} }
}
]

then bind each handler to a function, through the ai plugin’s API:

const ai = ctx.useApi<AiApi>("ai");
ai?.registerManifestTools("my-plugin", manifest, {
listCommands: () => ({ commands: [] }),
});

kind: "read" runs as soon as the model asks. kind: "action" is shown in the conversation and only runs once the user approves it, as a UI transaction. Tools disappear with their plugin.

A plugin asks the ai plugin for an assistant; the shell renders the conversation, so the plugin writes no UI:

const assistant = ai.createAssistant(ctx, {
assistantId: "help", // panel id becomes "<pluginId>:help"
title: "my-plugin.panel.title", // i18n key
icon: "help",
surface: "leftSidebar",
systemPrompt: ctx.i18n.t("prompt"),
toolNames: ["list_commands"], // omit for every registered tool
});

The panel, its toolbar (new conversation, clear, stop) and the conversations belong to the calling plugin: disabling it removes them, while the AI settings stay. Call assistant.dispose() on plugins/deactivated. Several assistants can coexist — one for user help, another for the business data — each with its own instructions and its own subset of tools.

ai-help is a small example built on this: it exposes the live commands, panels and settings of the running app as read tools plus a run_command action, creates its own Help panel, and adds a command (View › Help: how do I…?) that opens it.

The same assistant can be declared instead of coded; keys are the plugin’s own i18n keys. actions become buttons of the assistant panel that open it and send their prompt:

"assistants": [{
"id": "help", "title": "panel.title", "icon": "help",
"surface": "leftSidebar", "order": 40,
"systemPrompt": "prompt",
"tools": ["list_commands", "run_command"],
"actions": [{ "id": "tour", "title": "action.tour", "prompt": "action.tourPrompt", "icon": "help", "order": 5 }]
}]
const assistants = ai.registerManifestAssistants(ctx, manifest); // dispose() each on deactivation

Any other plugin command can be put in the same panel toolbar with a normal placement: { "commandId": "myCommand", "toolbar": "panel:help" }. Its handler may call ai.ask("<pluginId>:help", text) to send a computed prompt.

Documentation sous licence CC BY 4.0 · © les contributeurs de SCFlow ·Retour à supplychainflow.org