Skip to content
Draft
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
1 change: 1 addition & 0 deletions apps/agenstra/backend-agent-controller/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
"executor": "nx:run-commands",
"options": {
"commands": [
"rm -rf dist/apps/agenstra/backend-agent-controller/src/migrations",
"mkdir -p dist/apps/agenstra/backend-agent-controller/src/migrations",
"(npx tsc $(find apps/agenstra/backend-agent-controller/src/migrations -name '*.ts' 2>/dev/null || echo '') --outDir dist/apps/agenstra/backend-agent-controller/src/migrations --module commonjs --target es2021 --moduleResolution node --esModuleInterop --skipLibCheck --resolveJsonModule --declaration false --rootDir apps/agenstra/backend-agent-controller/src/migrations 2>/dev/null || true)",
"npx tsc $(find libs/domains/identity/backend/util-auth/src/lib/migrations -name '*.ts') --outDir dist/apps/agenstra/backend-agent-controller/src/migrations --module commonjs --target es2021 --moduleResolution node --esModuleInterop --skipLibCheck --resolveJsonModule --declaration false --rootDir libs/domains/identity/backend/util-auth/src/lib/migrations",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm';

/**
* Durable chat-scoped plan aggregates for Agenstra chat plan mode.
*/
export class CreateChatPlan1775500000000 implements MigrationInterface {
name = 'CreateChatPlan1775500000000';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TYPE "statistics_interaction_kind_enum" ADD VALUE IF NOT EXISTS 'chat_plan_turn'
`);
await queryRunner.query(`
ALTER TYPE "statistics_interaction_kind_enum" ADD VALUE IF NOT EXISTS 'chat_plan_execute'
`);

await queryRunner.query(`
DO $$ BEGIN
CREATE TYPE "chat_plan_status_enum" AS ENUM (
'pending', 'exploring', 'ready', 'refining', 'executing', 'executed', 'failed', 'cancelled'
);
EXCEPTION WHEN duplicate_object THEN null; END $$;
`);
await queryRunner.query(`
DO $$ BEGIN
CREATE TYPE "chat_plan_phase_enum" AS ENUM (
'explore', 'draft', 'refine', 'ready'
);
EXCEPTION WHEN duplicate_object THEN null; END $$;
`);

await queryRunner.createTable(
new Table({
name: 'chat_plan',
columns: [
{
name: 'id',
type: 'uuid',
isPrimary: true,
generationStrategy: 'uuid',
default: 'uuid_generate_v4()',
},
{ name: 'client_id', type: 'uuid', isNullable: false },
{ name: 'agent_id', type: 'uuid', isNullable: false },
{ name: 'chat_id', type: 'uuid', isNullable: false },
{
name: 'status',
type: 'enum',
enum: ['pending', 'exploring', 'ready', 'refining', 'executing', 'executed', 'failed', 'cancelled'],
enumName: 'chat_plan_status_enum',
isNullable: false,
},
{
name: 'phase',
type: 'enum',
enum: ['explore', 'draft', 'refine', 'ready'],
enumName: 'chat_plan_phase_enum',
isNullable: false,
},
{ name: 'source_prompt', type: 'text', isNullable: false },
{ name: 'plan_markdown', type: 'text', isNullable: true },
{ name: 'summary', type: 'varchar', length: '512', isNullable: true },
{ name: 'context_injection', type: 'jsonb', isNullable: true },
{ name: 'model', type: 'varchar', length: '256', isNullable: true },
{ name: 'resume_session_suffix', type: 'varchar', length: '128', isNullable: false },
{ name: 'completion_signal_seen', type: 'boolean', default: false, isNullable: false },
{ name: 'failure_code', type: 'varchar', length: '64', isNullable: true },
{ name: 'failure_message', type: 'varchar', length: '512', isNullable: true },
{ name: 'created_by_user_id', type: 'uuid', isNullable: true },
{ name: 'started_at', type: 'timestamptz', isNullable: false },
{ name: 'finished_at', type: 'timestamptz', isNullable: true },
{
name: 'created_at',
type: 'timestamptz',
default: 'CURRENT_TIMESTAMP',
isNullable: false,
},
{
name: 'updated_at',
type: 'timestamptz',
default: 'CURRENT_TIMESTAMP',
isNullable: false,
},
],
}),
true,
);

await queryRunner.createIndex(
'chat_plan',
new TableIndex({
name: 'IDX_chat_plan_client_agent_chat',
columnNames: ['client_id', 'agent_id', 'chat_id'],
}),
);
await queryRunner.createIndex(
'chat_plan',
new TableIndex({
name: 'IDX_chat_plan_agent_chat_status',
columnNames: ['agent_id', 'chat_id', 'status'],
}),
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropIndex('chat_plan', 'IDX_chat_plan_agent_chat_status');
await queryRunner.dropIndex('chat_plan', 'IDX_chat_plan_client_agent_chat');
await queryRunner.dropTable('chat_plan', true);
await queryRunner.query(`DROP TYPE IF EXISTS "chat_plan_phase_enum"`);
await queryRunner.query(`DROP TYPE IF EXISTS "chat_plan_status_enum"`);
// Postgres cannot remove enum values from statistics_interaction_kind_enum safely.
}
}
2 changes: 2 additions & 0 deletions apps/agenstra/backend-agent-controller/src/typeorm.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
TicketAutomationRunEntity,
TicketAutomationRunStepEntity,
TicketAutomationEntity,
ChatPlanEntity,
TicketBodyGenerationSessionEntity,
TicketCommentEntity,
TicketEntity,
Expand Down Expand Up @@ -103,6 +104,7 @@ export const typeormConfig: DataSourceOptions = {
TicketAutomationRunEntity,
TicketAutomationLeaseEntity,
TicketAutomationRunStepEntity,
ChatPlanEntity,
ClientAgentAutonomyEntity,
AtlassianSiteConnectionEntity,
ExternalImportConfigEntity,
Expand Down
60 changes: 60 additions & 0 deletions apps/agenstra/frontend-agent-console/src/i18n/messages.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,66 @@
<trans-unit id="featureChat-enhancePromptAria" datatype="html">
<source>Enhance prompt with AI</source>
</trans-unit>
<trans-unit id="featureChat-planPromptTitle" datatype="html">
<source>Create plan with AI</source>
</trans-unit>
<trans-unit id="featureChat-planPromptAria" datatype="html">
<source>Create plan with AI</source>
</trans-unit>
<trans-unit id="featureChat-planModalTitle" datatype="html">
<source>Plan</source>
</trans-unit>
<trans-unit id="featureChat-planRefinePlaceholder" datatype="html">
<source>Describe changes to the plan…</source>
</trans-unit>
<trans-unit id="featureChat-planRefineSubmit" datatype="html">
<source>Refine</source>
</trans-unit>
<trans-unit id="featureChat-planExecute" datatype="html">
<source>Execute</source>
</trans-unit>
<trans-unit id="featureChat-planCancel" datatype="html">
<source>Cancel plan</source>
</trans-unit>
<trans-unit id="featureChat-planEmptyMarkdown" datatype="html">
<source>Plan content will appear here as the agent explores…</source>
</trans-unit>
<trans-unit id="featureChat-planStatusPending" datatype="html">
<source>Pending</source>
</trans-unit>
<trans-unit id="featureChat-planStatusExploring" datatype="html">
<source>Exploring</source>
</trans-unit>
<trans-unit id="featureChat-planStatusReady" datatype="html">
<source>Ready</source>
</trans-unit>
<trans-unit id="featureChat-planStatusRefining" datatype="html">
<source>Refining</source>
</trans-unit>
<trans-unit id="featureChat-planStatusExecuting" datatype="html">
<source>Executing</source>
</trans-unit>
<trans-unit id="featureChat-planStatusExecuted" datatype="html">
<source>Executed</source>
</trans-unit>
<trans-unit id="featureChat-planStatusFailed" datatype="html">
<source>Failed</source>
</trans-unit>
<trans-unit id="featureChat-planStatusCancelled" datatype="html">
<source>Cancelled</source>
</trans-unit>
<trans-unit id="featureChat-planPhaseExplore" datatype="html">
<source>Explore</source>
</trans-unit>
<trans-unit id="featureChat-planPhaseDraft" datatype="html">
<source>Draft</source>
</trans-unit>
<trans-unit id="featureChat-planPhaseRefine" datatype="html">
<source>Refine</source>
</trans-unit>
<trans-unit id="featureChat-planPhaseReady" datatype="html">
<source>Ready</source>
</trans-unit>
<trans-unit id="featureChat-model" datatype="html">
<source>Model</source>
</trans-unit>
Expand Down
3 changes: 2 additions & 1 deletion docs/agenstra/applications/frontend-agent-console.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ On reconnection:
2. Restores client context (`setClient`)
3. Restores agent login (if previously logged in)
4. Clears stale local buffers where required to avoid duplicates
5. Receives chat history for the active session (`chatId` / primary) and ticket automation cards on the **primary** session only (as implemented in NgRx selectors/effects); session switches use `restoreChat`
5. Receives chat history for the active session (`chatId` / primary) and ticket automation cards on the **primary** session only (as implemented in NgRx selectors/effects); **chat plan cards** hydrate for the matching `plan.chatId` (primary or user session). Session switches use `restoreChat`

## Authentication

Expand Down Expand Up @@ -299,6 +299,7 @@ Before deploying to production:
## Related documentation

- **[Chat Interface Feature](../features/chat-interface.md)** Chat functionality guide
- **[Chat plan mode](../features/chat-plan-mode.md)** Explore-only plan cards, refine, and execute
- **[Web IDE Feature](../features/web-ide.md)** Code editor guide
- **[File Management Feature](../features/file-management.md)** File operations guide
- **[Version Control Feature](../features/version-control.md)** Git operations guide
Expand Down
13 changes: 13 additions & 0 deletions docs/agenstra/features/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Agenstra provides a complete set of features for managing distributed AI agent i
- **Version Control** Git operations directly from the web interface
- **Web IDE** Monaco Editor integration for code editing
- **Chat Interface** AI chat functionality with real-time responses
- **[Chat plan mode](./chat-plan-mode.md)** Explore-only OpenCode planning with refine and execute-into-chat
- **Deployment** CI/CD pipeline management and deployment functionality
- **Authentication** Multiple authentication methods with configurable user registration
- **Tickets and Workspaces** Ticket boards, migration, and automation on the controller
Expand Down Expand Up @@ -131,6 +132,18 @@ AI chat functionality with real-time responses. Send messages to agents and rece
- Markdown rendering
- Automatic history restoration

### [Chat plan mode](./chat-plan-mode.md)

Explore-only OpenCode planning from the composer: live chat-scoped cards, refine in a modal, execute into the same visible chat.

**Key Capabilities**:

- Plan button beside prompt enhance
- Full composer context injection snapshot
- Explore-only hidden `-plan-*` sessions
- Chat-scoped hydrate across hard reload
- Execute plan into the current chat

### [Deployment](./deployment.md)

CI/CD pipeline management and deployment functionality. Configure CI/CD providers (GitHub Actions), trigger pipeline runs, monitor their status, and view logs directly from the Agenstra console.
Expand Down
4 changes: 3 additions & 1 deletion docs/agenstra/features/chat-interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ Background and helper flows use reserved ACP `resumeSessionSuffix` values. Those
- `-prompt-enhance`
- `-ticket-body`
- `-ticket-auto-*` (for example `-ticket-auto-pre`, `-ticket-auto-loop`, `-ticket-auto-commit-msg`)
- `-plan-{planId}` (chat plan mode explore/refine; explore-only permissions)

See [Agent Client Protocol](../ai-agents/agent-client-protocol.md) for suffix rules.
See [Agent Client Protocol](../ai-agents/agent-client-protocol.md) for suffix rules and [Chat plan mode](./chat-plan-mode.md) for the plan-mode product flow.

Ticket automation **run cards** in the chat timeline are environment-scoped ACP work, but the console shows those embeddings on the **primary chat session only**. Side (`user`) sessions show that session’s messages without automation cards.

**Chat plan cards** are scoped to the **visible chat** that created them (`plan.chatId`): they appear on primary or user sessions accordingly, and survive hard reload via hydrate.
Unread badges in the chat session dropdown follow the same rule: each visible session has its own unread flag (shown even when that session is selected); automation activity only marks the primary session unread. Selecting a session marks that session read. The dropdown toggle shows a badge when any visible session for the environment has unread.

### REST API
Expand Down
58 changes: 58 additions & 0 deletions docs/agenstra/features/chat-plan-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Chat plan mode

Operators can turn the current chat composer prompt (plus selected context) into an **explore-then-plan** workflow. A hidden OpenCode session investigates the repository with **explore-only** permissions, a chat-scoped timeline card shows live status and plan markdown, a detail modal supports refine, and **Execute plan** injects the plan into the same visible chat.

This is an Agenstra productivity feature built on OpenCode (not a native OpenCode “plan mode” product API). It mirrors ticket-automation durability and hydrate patterns, but is user-triggered and chat-session scoped.

## Prerequisites

- Authenticated console user with access to the client and `agents:chats` scope
- Selected agent environment with a working OpenCode worker and up-to-date config sync (platform plan agent/skill injected)
- Non-empty composer prompt and a selected visible chat session (`primary` or `user`)

## Context injection

Plan creation uses the **same composer context** as Send (`ContextInjectionPayload`): workspace, related environments, ticket SHAs, knowledge SHAs, and auto-enrichment. The controller stores a snapshot on `chat_plan.context_injection` and reuses it for explore, refine, and execute turns. Refine may optionally send a new snapshot to replace the stored one.

## Phases (high level)

1. **Create** Console emits `createChatPlan` with `chatId`, prompt, model, and context. Controller inserts a `chat_plan` row (`exploring` / `explore`), emits `chatPlanUpsert`, and starts the orchestrator.
2. **Explore** Hidden OpenCode session `-plan-{planId}` with platform agent `agenstra-plan` and explore-only session permission ruleset (deny edit/write/patch/bash). Live markdown/status updates via throttled `chatPlanUpsert`.
3. **Ready** Structured turn status reports `ready` with `planMarkdown` / `summary` (or best-effort text extraction). Card becomes executable.
4. **Refine** (optional) `refineChatPlan` continues the same hidden session; status moves through `refining` then back to `ready`.
5. **Execute** `executeChatPlan` prompts the **visible** chat with the plan body and stored context (normal interactive permissions). Plan status becomes `executed`.
6. **Cancel** `cancelChatPlan` or REST cancel stops active explore/refine.

At most one **active** plan (`exploring` / `refining`) is allowed per `(agentId, chatId)`.

## Unattended OpenCode sessions (explore-only)

Reserved resume suffix `-plan-{planId}`:

- Hidden / ephemeral — no `agent_messages` rows; not listed in session switcher
- Session-scoped permission override: allow read/glob/grep/(web explore); **deny** write/mutation tools (including bash)
- Runtime auto-replies residual permission asks: allow explore, **reject** write/unknown; not automation allow-all and not `unattendedAutomation`
- Platform-injected agent `agenstra-plan` and skill `agenstra-chat-plan` (config sync bump via `AGENSTRA_OPENCODE_PLATFORM_WIRE_VERSION`)

Interactive chat and execute paths keep normal worker permissions.

## Persistence and restore

Source of truth is the controller `chat_plan` table (not chat message history). After agent login, the controller unicasts recent plans via `chatPlanUpsert` with `hydrate: true` (capped similarly to automation hydrate). Live updates broadcast to room `client:{clientId}`. Frontend merges cards into the timeline only when `plan.chatId` matches the selected chat (unlike automation cards, which are primary-only).

## HTTP and realtime

- **REST** `GET /clients/{id}/agents/{agentId}/chats/{chatId}/plans`, `GET .../plans/{planId}`, `POST .../plans/{planId}/cancel` (OpenAPI operationIds `listChatPlans`, `getChatPlan`, `cancelChatPlan`)
- **WS (clients namespace, controller-handled)** `createChatPlan`, `refineChatPlan`, `executeChatPlan`, `cancelChatPlan`, `chatPlanUpsert`
- Statistics kinds: `chat_plan_turn`, `chat_plan_execute`

See [WebSocket communication](./websocket-communication.md), [Chat Interface](./chat-interface.md), and the agent-controller AsyncAPI / OpenAPI.

## Related documentation

- [Chat Interface](./chat-interface.md) Hidden `-plan-*` suffixes and chat-scoped cards
- [Ticket automation](./ticket-automation.md) Parallel durability / hydrate pattern (primary-only cards; allow-all sessions)
- [Agent configuration](./agent-configuration.md) Platform wire / OpenCode config sync
- [Usage statistics](./usage-statistics.md) Interaction kinds
- [Backend Agent Controller](../applications/backend-agent-controller.md)
- [Frontend Agent Console](../applications/frontend-agent-console.md)
2 changes: 1 addition & 1 deletion docs/agenstra/features/websocket-communication.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ The controller re-emits manager events **using their original event names** to t

#### Controller-originated ticket events (still on `clients`)

To refresh ticket metadata in chat without subscribing to `tickets`, the controller may emit `ticketChatTicketUpsert` and automation timeline payloads such as `ticketAutomationRunChatUpsert` to room `client:{clientId}`. See the agent-controller AsyncAPI for fields.
To refresh ticket metadata in chat without subscribing to `tickets`, the controller may emit `ticketChatTicketUpsert` and automation timeline payloads such as `ticketAutomationRunChatUpsert` to room `client:{clientId}`. Chat plan mode uses controller-handled `createChatPlan` / `refineChatPlan` / `executeChatPlan` / `cancelChatPlan` and emits `chatPlanUpsert` (hydrate on login + live room broadcast). See the agent-controller AsyncAPI for fields and [Chat plan mode](./chat-plan-mode.md).

### Manager → Controller

Expand Down
Loading
Loading