Aller au contenu

Extensions

Guide principal pour la création d’extensions runtime dans packages/coding-agent.

Ce document couvre le runtime d’extension actuel dans :

  • src/extensibility/extensions/types.ts
  • src/extensibility/extensions/runner.ts
  • src/extensibility/extensions/wrapper.ts
  • src/extensibility/extensions/index.ts
  • src/modes/controllers/extension-ui-controller.ts

Pour les chemins de découverte et les règles de chargement du système de fichiers, consultez docs/extension-loading.md.

Une extension est un module TS/JS exportant une factory par défaut :

import type { ExtensionAPI } from "@f5-sales-demo/xcsh";
export default function myExtension(pi: ExtensionAPI) {
// register handlers/tools/commands/renderers
}

Les extensions peuvent combiner tous les éléments suivants dans un seul module :

  • gestionnaires d’événements (pi.on(...))
  • outils appelables par LLM (pi.registerTool(...))
  • commandes slash (pi.registerCommand(...))
  • raccourcis clavier et indicateurs
  • rendu de messages personnalisé
  • API d’injection de session/message (sendMessage, sendUserMessage, appendEntry)
  1. Les extensions sont importées et leurs fonctions factory sont exécutées.
  2. Durant cette phase de chargement, les méthodes d’enregistrement sont valides ; les méthodes d’action runtime ne sont pas encore initialisées.
  3. ExtensionRunner.initialize(...) connecte les actions/contextes actifs pour le mode en cours.
  4. Les événements du cycle de vie de session/agent/outil sont émis vers les gestionnaires.
  5. Chaque exécution d’outil est enveloppée avec l’interception d’extension (tool_call / tool_result).
Extension lifecycle (simplified)
load paths
import module + run factory (registration only)
ExtensionRunner.initialize(mode/session/tool registry)
├─ emit session/agent events to handlers
├─ wrap tool execution (tool_call/tool_result)
└─ expose runtime actions (sendMessage, setActiveTools, ...)

Contrainte importante de loader.ts :

  • appeler des méthodes d’action comme pi.sendMessage() lors du chargement de l’extension lève ExtensionRuntimeNotInitializedError
  • enregistrez d’abord ; effectuez le comportement runtime depuis les événements/commandes/outils
import type { ExtensionAPI } from "@f5-sales-demo/xcsh";
import { Type } from "@sinclair/typebox";
export default function (pi: ExtensionAPI) {
pi.setLabel("Safety + Utilities");
pi.on("session_start", async (_event, ctx) => {
ctx.ui.notify(`Extension loaded in ${ctx.cwd}`, "info");
});
pi.on("tool_call", async (event) => {
if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
return { block: true, reason: "Blocked by extension policy" };
}
});
pi.registerTool({
name: "hello_extension",
label: "Hello Extension",
description: "Return a greeting",
parameters: Type.Object({ name: Type.String() }),
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
return {
content: [{ type: "text", text: `Hello, ${params.name}` }],
details: { greeted: params.name },
};
},
});
pi.registerCommand("hello-ext", {
description: "Show queue state",
handler: async (_args, ctx) => {
ctx.ui.notify(`pending=${ctx.hasPendingMessages()}`, "info");
},
});
}

Méthodes principales :

  • on(event, handler)
  • registerTool, registerCommand, registerShortcut, registerFlag
  • registerMessageRenderer
  • sendMessage, sendUserMessage, appendEntry
  • getActiveTools, getAllTools, setActiveTools
  • getSessionName, setSessionName
  • setModel, getThinkingLevel, setThinkingLevel
  • registerProvider
  • events (bus d’événements partagé)

En mode interactif, les gestionnaires input s’exécutent avant la vérification automatique du titre du premier message intégrée. Les extensions qui appellent await pi.setSessionName(...) depuis input peuvent définir le nom de session persisté et empêcher le titre auto-généré par défaut de s’exécuter pour cette session.

Également exposés :

  • pi.logger
  • pi.typebox
  • pi.pi (exports du paquet)

pi.sendMessage(message, options) prend en charge :

  • deliverAs: "steer" (par défaut) — interrompt l’exécution en cours
  • deliverAs: "followUp" — mis en file d’attente pour s’exécuter après l’exécution en cours
  • deliverAs: "nextTurn" — stocké et injecté lors de la prochaine invite utilisateur
  • triggerTurn: true — démarre un tour lorsqu’inactif (nextTurn ignore cela)

pi.sendUserMessage(content, { deliverAs }) passe toujours par le flux d’invite ; lors du streaming, il est mis en file d’attente en tant que steer/follow-up.

Les gestionnaires et les execute d’outils reçoivent ctx avec :

  • ui
  • hasUI
  • cwd
  • sessionManager (lecture seule)
  • modelRegistry, model
  • getContextUsage()
  • compact(...)
  • isIdle(), hasPendingMessages(), abort()
  • shutdown()
  • getSystemPrompt()

Les gestionnaires de commandes reçoivent en plus :

  • waitForIdle()
  • newSession(...)
  • switchSession(...)
  • branch(entryId)
  • navigateTree(targetId, { summarize })
  • reload()

Utilisez le contexte de commande pour les flux de contrôle de session ; ces méthodes sont intentionnellement séparées des gestionnaires d’événements généraux.

Surface des événements (noms et comportements actuels)

Section intitulée « Surface des événements (noms et comportements actuels) »

Les unions d’événements canoniques et les types de charge utile se trouvent dans types.ts.

  • session_start
  • session_before_switch / session_switch
  • session_before_branch / session_branch
  • session_before_compact / session.compacting / session_compact
  • session_before_tree / session_tree
  • session_shutdown

Pré-événements annulables :

  • session_before_switch{ cancel?: boolean }
  • session_before_branch{ cancel?: boolean; skipConversationRestore?: boolean }
  • session_before_compact{ cancel?: boolean; compaction?: CompactionResult }
  • session_before_tree{ cancel?: boolean; summary?: { summary: string; details?: unknown } }
  • input
  • before_agent_start
  • context
  • agent_start / agent_end
  • turn_start / turn_end
  • message_start / message_update / message_end
  • tool_call (pré-exécution, peut bloquer)
  • tool_result (post-exécution, peut modifier le contenu/les détails/isError)
  • tool_execution_start / tool_execution_update / tool_execution_end (observabilité)

tool_result est de style middleware : les gestionnaires s’exécutent dans l’ordre des extensions et chacun voit les modifications précédentes.

  • auto_compaction_start / auto_compaction_end
  • auto_retry_start / auto_retry_end
  • ttsr_triggered
  • todo_reminder
  • user_bash (remplacement avec { result })
  • user_python (remplacement avec { result })

resources_discover existe dans les types d’extension et ExtensionRunner. Note sur le runtime actuel : ExtensionRunner.emitResourcesDiscover(...) est implémenté, mais il n’existe aucun point d’appel AgentSession l’invoquant dans la base de code actuelle.

registerTool utilise ToolDefinition depuis types.ts.

Signature execute actuelle :

execute(
toolCallId,
params,
signal,
onUpdate,
ctx,
): Promise<AgentToolResult>

Modèle :

pi.registerTool({
name: "my_tool",
label: "My Tool",
description: "...",
parameters: Type.Object({}),
async execute(_id, _params, signal, onUpdate, ctx) {
if (signal?.aborted) {
return { content: [{ type: "text", text: "Cancelled" }] };
}
onUpdate?.({ content: [{ type: "text", text: "Working..." }] });
return { content: [{ type: "text", text: "Done" }], details: {} };
},
onSession(event, ctx) {
// reason: start|switch|branch|tree|shutdown
},
renderCall(args, theme) {
// optional TUI render
},
renderResult(result, options, theme, args) {
// optional TUI render
},
});

tool_call/tool_result intercepte tous les outils une fois le registre encapsulé dans sdk.ts, y compris les outils intégrés et les outils d’extension/personnalisés.

Points d’intégration de l’interface utilisateur

Section intitulée « Points d’intégration de l’interface utilisateur »

ctx.ui implémente l’interface ExtensionUIContext. La prise en charge varie selon le mode.

Pris en charge :

  • dialogues : select, confirm, input, editor
  • notifications/statut/texte de l’éditeur/saisie terminal/superpositions personnalisées
  • liste/chargement des thèmes par nom (setTheme prend en charge les noms de chaînes)
  • bascule d’expansion des outils

Méthodes sans effet dans ce contrôleur :

  • setFooter
  • setHeader
  • setEditorComponent

À noter également : setWidget route actuellement vers le texte de la ligne de statut via setHookWidget(...).

ctx.ui est soutenu par des événements RPC extension_ui_request :

  • les méthodes de dialogue (select, confirm, input, editor) effectuent des allers-retours vers les réponses client
  • les méthodes fire-and-forget émettent des requêtes (notify, setStatus, setWidget pour les tableaux de chaînes, setTitle, setEditorText)

Non pris en charge/sans effet dans l’implémentation RPC :

  • onTerminalInput
  • custom
  • setFooter, setHeader, setEditorComponent
  • setWorkingMessage
  • changement/chargement de thème (setTheme renvoie une erreur)
  • les contrôles d’expansion des outils sont inactifs

Lorsqu’aucun contexte UI n’est fourni à l’initialisation du runner, ctx.hasUI est false et les méthodes sont sans effet/retournent des valeurs par défaut.

Le mode arrière-plan installe un objet de contexte UI non interactif. Dans l’implémentation actuelle, ctx.hasUI peut toujours être true tandis que les dialogues interactifs retournent des valeurs par défaut/comportement sans effet.

Pour un état d’extension durable :

  1. Persistez avec pi.appendEntry(customType, data).
  2. Reconstruisez l’état depuis ctx.sessionManager.getBranch() lors de session_start, session_branch, session_tree.
  3. Maintenez les details des résultats d’outils structurés lorsque l’état doit être visible/reconstructible depuis l’historique des résultats d’outils.

Exemple de modèle de reconstruction :

pi.on("session_start", async (_event, ctx) => {
let latest;
for (const entry of ctx.sessionManager.getBranch()) {
if (entry.type === "custom" && entry.customType === "my-state") {
latest = entry.data;
}
}
// restore from latest
});
pi.registerMessageRenderer("my-type", (message, { expanded }, theme) => {
// return pi-tui Component
});

Utilisé par le rendu interactif lorsque des messages personnalisés sont affichés.

Fournissez renderCall / renderResult dans les définitions registerTool pour une visualisation personnalisée des outils dans le TUI.

  • Les actions runtime ne sont pas disponibles lors du chargement de l’extension.
  • Les erreurs tool_call bloquent l’exécution (échec fermé).
  • Les conflits de noms de commandes avec les commandes intégrées sont ignorés avec des diagnostics.
  • Les raccourcis réservés sont ignorés (ctrl+c, ctrl+d, ctrl+z, ctrl+k, ctrl+p, ctrl+l, ctrl+o, ctrl+t, ctrl+g, shift+tab, shift+ctrl+p, alt+enter, escape, enter).
  • Traitez ctx.reload() comme terminal pour le cadre du gestionnaire de commande en cours.

Utilisez la bonne surface :

  • Extensions (src/extensibility/extensions/*) : système unifié (événements + outils + commandes + renderers + enregistrement de fournisseur).
  • Hooks (src/extensibility/hooks/*) : API d’événements légacy séparée.
  • Custom-tools (src/extensibility/custom-tools/*) : modules axés sur les outils ; lorsqu’ils sont chargés aux côtés des extensions, ils sont adaptés et passent toujours par les wrappers d’interception d’extension.

Si vous avez besoin d’un seul paquet qui gère la politique, les outils, l’expérience utilisateur des commandes et le rendu ensemble, utilisez les extensions.