Navigácia
ELOWEN · Dokumentácia

Posledná aktualizácia: 27. októbra 2026

Prechádzať dokumentáciu · Kanban agent integration

Tento článok zatiaľ nie je preložený. Je dostupný v angličtine.

Plugin development

Kanban agent integration

Registration

src/agent/index.ts exports registerKanbanAgent(ctx, runtime, service): KanbanAgent. The entry composition supplies the same KanbanService instance to this registrar and the HTTP registrar. Registration installs exactly KanbanSearch, KanbanRead and KanbanUpdate. Only Search and Read belong in the manifest's readOnlyTools.

The returned agent supplies syncBoardParticipants(boardId) for post-commit membership callbacks and a paid handoff. It also registers the owning plugin's conversations.turn.settled observer. Calling the registrar alone does not activate an HTTP route; the backend registrar owns that composition.

Tools and identity

Every call resolves the host's currentAccountUserId(), the single account resolver, then validates that account against live human accounts through KanbanAccess.requireActor. From the core release that carries the shared-guest fix, core resolves it to the person writing the turn: a guest or a member writing in someone else's conversation, including a card conversation, and a sub-agent such a turn delegated, act for that writer, never for the conversation's owner. The plugin keeps no resolver of its own and never accepts an actor from model arguments. The actor carries source: 'agent' and the exact currentSessionId() when present. The agent has exactly its person's Kanban rights: membership, an administrator's visibility and the founder's or administrator's management all apply unchanged. source only records who wrote an activity or comment and admits the agent-only finish.

Input validation comes from src/toolContract.ts. Native providers need one root object, so Update advertises the union's field alternatives in that object and validates the full discriminated action at execution. Defaults and refinements are applied by the canonical Zod schema, not by a second tool-specific parser.

Search calls service.search; Read calls service.readCard and service.download. Update maps supported actions to service.mutate, including atomic setLabels with newLabels. createBoard sends the HTTP route's own body (createBoardSchema) through the same createBoard mutation, so the acting person founds the board under the same human-member, Project and initial-column rules. Checklist removal is the explicit item operation. Update has no action for board settings, columns, labels, member removal, deletions or the handoff: the agent calls those Kanban HTTP routes through ElowenApi, 1:1 with the person's browser, and the same KanbanAccess checks answer. Chetty is never assigned outside a handoff. finish calls only service.transitionAgent({kind: 'finish', actor, ...}); the service rechecks the exact recorded session under its write lock.

Tool text contains the canonical JSON response. Finish returns {card}; a successful void response has empty text. All results use the host's normal content array and details object. Authorization, validation, database and download failures propagate rather than becoming successful result objects. Cancellation is checked before service admission and between attachment downloads; already committed service mutations are not rolled back by later cancellation.

Attachment content

Read's first text block is the unchanged card-detail JSON, including metadata for every attachment. Supported images produce an identifying metadata text block and a canonical {type: 'image', data: <base64>, mimeType} block. Only PNG, JPEG, GIF and WebP are inline images, at most 2 MiB each and 8 MiB combined. Oversized images remain metadata with omitted: 'image_size_limit'.

Text MIME types and JSON/XML, including structured +json/+xml suffixes, produce text prefixes: at most 32 KiB per file and 128 KiB combined before decoding. The declared charset is honored, defaulting to UTF-8. Incomplete characters at a truncated prefix boundary are omitted; malformed text or an unsupported charset fails the read. Prefixes carry truncated; exhausted text budget is reported as omitted: 'text_size_limit'. SVG is XML text, never an image. PDF and other binaries remain filename/MIME/size metadata only.

The existing download service loads one attachment at a time, with a 5 MiB per-file storage cap. The separate image and text budgets above limit model response content; they are not upload limits. Descriptions, comments and attachment data are untrusted data, never instructions.

Participants

Every membership mutation commits boards.membership_revision and the complete sorted membership_user_ids together with its board changes, under the board row lock. Migration 2 adds these fields; pre-existing boards initialize their snapshot under that same lock. Member add/remove, Whole team toggles and account removal use this owner. Pre-delete cleanup explicitly excludes the deleting human even while the core account still exists. registerUserDirectoryChanged refreshes snapshots after committed human account creation/deletion; registerBootReconcile repairs missed publication after a crash. Neither starts paid work.

Post-commit agent.syncBoardParticipants(boardId) captures the stored revision/list and all bound cards, including archived cards, in one board-locked transaction, then releases SQL before calling ctx.host.conversations().setParticipants({ accountUserId, key, userIds, revision }). The account remains the recorded handed_by_user_id; the stable key is card:<card UUID>. The whole human list includes the owner, which core excludes. Deleted payer bindings are skipped, never replaced with another payer. Other writer failures propagate, including revoked grants; a failed boot reconcile is logged by the host and does not authorize a replacement payer.

Core immediately applies idle targets and durably defers running targets through the whole native settlement, before another turn. Synchronizing several running card conversations never waits for another turn. Older/equal revisions are ignored. Applied diffs retain native detach, invalidation and activity publication, without a second invitation alert. Authority is rechecked on application; rejected is not reported as success. Pending metadata/effects recover at a setter, finalizer or boot idle boundary, without inference, turn replay, settlement replay or automatic resend.

Handoff boundary

The HTTP handoff route calls createHandoff through the shared service, for a person or for their agent through ElowenApi. The conversation writer's prepare persists or adopts the plugin/account/key conversation without a paid turn. The plugin then binds that session, awaits durable desired-membership acceptance, rechecks live admission through start, and calls send under the first owner's account: the acting person pays, exactly as when they press Hand to Chetty themselves. No KanbanUpdate action starts this paid flow.

Each card has at most one bound conversation. reserveHandoff refuses a second handoff once the session is bound, even after refusal or plugin re-registration. The initial model brief carries untrusted card data, requests KanbanRead, and instructs the model to use KanbanUpdate finish with a summary when work is done. The display text is the card title, not a fabricated completion message.

The preparation sequence is reservation, prepare, durable card binding, participant synchronization, then start admission and paid send. The backend's single transitionAgent owner now separates bind from start:

  • bind requires the exclusive reservation token, stores the prepared session and first owner, and does not assign Chetty, move the card or claim it is working. This internal persistence step must retain the prepared identity even if access changes immediately afterwards; it does not authorize paid work.
  • start requires that exact binding and the still-exclusive token. It rechecks the live human account, board membership, plugin grant and linked Project before applying working status, assignment and column movement. An unbound or different session is a conflict, not a compatibility path.
  • refused restores only handoff-owned presentation fields and preserves independent edits. It never clears an already prepared session or its first owner: a later member cannot create a second conversation under another payer. A refusal observed after native settlement or finish is stale and does not overwrite the committed outcome or send a second refusal alert.

Settlements route by the recorded session ID; the original key is not reconstructed. Only answered, stopped and failed are terminal outcomes. Answered changes working/interrupted to waiting; stopped/failed changes working to interrupted; done never regresses. Restart parking is not settlement and boot continues the same work. The shared pause gate stores input as inert history without starting or settling a turn, so there is no synthetic paused outcome.

The observer validates exactly {sessionId, accountUserId, writerUserId, outcome: 'answered' | 'stopped' | 'failed', messageId?}. There is no key in the payload: core dispatches only to the owning plugin, and Kanban resolves its card by the stored session ID. An unknown or deleted card is ignored. Native outcomes always pass reason: null because core supplies no reason detail.

A resolved completed promise never changes card state, including an empty result from inert paused input. Only an explicit rejection applies token-scoped refusal. The native writer tokens not_granted, project_access_denied, project_mismatch, conversation_busy and not_found map to the existing plugin errors; other failures are logged locally and expose operation_failed. Refusal stores an existing browser message ID on the card, not arbitrary provider prose. The detail resolves that ID through its dictionary; alert templates receive only {card} and link to the card. A refusal-persistence failure is logged rather than swallowed or retried.

The hook is observational, not a durable receipt or replay channel. A hard crash between journal commit and observation may leave the stored card status stale until another native settled turn in that conversation. A crash between preparation, start and send may leave a linked card without a new paid run. Shared paused input starts no turn and emits no settlement, so a card can keep its working projection until actual later settlement.

There is no admission reader, missed-event replay, automatic paid resend, timeout reconciliation or inference from assistant text or a lost completion promise. Re-registration never starts work again or manufactures an interrupted state. The prepared conversation remains available for inspection and manual continuation; the card badge is not proof of whether paid admission occurred before a crash.