Skip to content
Open
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 .kosmokrator/config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
kosmokrator:
agent:
mode: edit
mode: plan
tools:
default_permission_mode: prometheus
59 changes: 59 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# OpenCompany Repo Guide

This file is intentionally mirrored in both `AGENTS.md` and `CLAUDE.md`. Keep them identical.

## Local

- Local URL: `http://opencompany.test`
- Ngrok URL: `https://your-subdomain.ngrok-free.dev` for Telegram webhooks and external integrations; set up your own with `ngrok http 80`
- Use `http://opencompany.test` for local navigation and testing.
- Stack: Laravel 12, Vue 3, Inertia.js, Tailwind CSS v4, Reka UI

## Workspace

- OpenCompany is multi-workspace. Scope queries correctly.
- `ResolveWorkspace` binds the active workspace as `currentWorkspace`.
- `workspace()` returns the current `Workspace`.
- Models with `workspace_id` should use `forWorkspace()`.
- Related models should be scoped through the relation, typically with `whereHas(...)`.
- Humans belong to workspaces through `workspace_members`. Agents have a direct `workspace_id`.

## Runtime

- Main runtime agent class: `app/Agents/OpenCompanyAgent.php`
- Identity/system-prompt content is assembled from identity files and agent config, not from a static hardcoded prompt.

## UI

- Shared UI components live in `resources/js/Components/shared/`.
- Prefer wrapper components over native elements when equivalents already exist.
- Dark mode exists and should not be broken.

## Package Ownership

- Do not default to fixing generic bridge, provider, registry, caching, or integration-runtime behavior inside `app/`.
- First decide whether the behavior belongs to OpenCompany or to a sibling package.
- Inspect the real package source before patching. In this workspace, package code may be path-based or symlinked into `vendor/`.
- Common package sources:
- `tmp/prism-relay`
- `tmp/prism-codex`
- `../integrations/core`
- `../integrations/packages/*`
- Do not patch `vendor/` for durable fixes.
- If a fix stays app-local, note why it is OpenCompany-specific.

## Working Notes

- Prefer `rg` and `rg --files` for search.
- Keep edits targeted. Do not revert unrelated user changes.
- Put audits and investigations into `docs/`.
- MCP CLI: `~/.local/bin/mcp-cli`
- MCP config: `~/.config/mcp/mcp_servers.json`
- Common MCP usage: `mcp-cli`, `mcp-cli info <server>`, `mcp-cli call <server> <tool> '<json>'`

## Docs

- Repo rules and local setup: `CLAUDE.md`
- Docs index: `docs/INDEX.md`
- Runtime audit: `docs/architecture/runtime-alignment-implementation-audit.md`
- Plane tool is available via `mcp-cli`
74 changes: 51 additions & 23 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,59 @@
# OpenCompany Project Rules
# OpenCompany Repo Guide

## Local Development
This file is intentionally mirrored in both `AGENTS.md` and `CLAUDE.md`. Keep them identical.

- **Local URL**: `http://opencompany.test` (Laravel Valet domain, no SSL)
- **Ngrok URL**: `https://your-subdomain.ngrok-free.dev` (used for Telegram webhooks and external integrations; set up your own via `ngrok http 80`)
- When testing or navigating to the app locally, always use `http://opencompany.test`
## Local

## Tech Stack
- Local URL: `http://opencompany.test`
- Ngrok URL: `https://your-subdomain.ngrok-free.dev` for Telegram webhooks and external integrations; set up your own with `ngrok http 80`
- Use `http://opencompany.test` for local navigation and testing.
- Stack: Laravel 12, Vue 3, Inertia.js, Tailwind CSS v4, Reka UI

- **Backend**: Laravel 12
- **Frontend**: Vue 3 + Inertia.js
- **Styling**: Tailwind CSS v4 + Reka UI (headless primitives)
- **Icons**: @iconify/vue with Phosphor icons (`ph:` prefix)
## Workspace

## Multi-Workspace Architecture
- OpenCompany is multi-workspace. Scope queries correctly.
- `ResolveWorkspace` binds the active workspace as `currentWorkspace`.
- `workspace()` returns the current `Workspace`.
- Models with `workspace_id` should use `forWorkspace()`.
- Related models should be scoped through the relation, typically with `whereHas(...)`.
- Humans belong to workspaces through `workspace_members`. Agents have a direct `workspace_id`.

- The app supports multiple workspaces. All data is workspace-scoped.
- **URL structure**: `/w/{workspace_slug}/...` — the slug identifies the active workspace.
- **Middleware**: `ResolveWorkspace` resolves the workspace from URL slug, `X-Workspace-Id` header, session, or user's first workspace (fallback). Binds it to the container as `currentWorkspace`.
- **Helper**: `workspace()` returns the current `Workspace` model from the container.
- **Model scoping**: Models use the `BelongsToWorkspace` trait which provides `scopeForWorkspace()` (explicit, not a global scope). Use `Model::forWorkspace()->...` in queries.
- **Humans** belong to workspaces via the `workspace_members` pivot table (many-to-many). **Agents** have a direct `workspace_id` column (belong to one workspace).
- **Frontend**: `useWorkspace()` composable provides `workspace`, `workspaces`, `workspacePath()`, and `isAdmin`. Workspace switcher is in the sidebar header.
- When adding new queries, always scope by workspace. For models with `workspace_id`, use `forWorkspace()`. For related models (e.g., messages via channels), use `whereHas` to filter through the relation.
## Runtime

## Component Structure
- Main runtime agent class: `app/Agents/OpenCompanyAgent.php`
- Identity/system-prompt content is assembled from identity files and agent config, not from a static hardcoded prompt.

- Shared components are in `resources/js/Components/shared/`
- Use the wrapper components (Button, Modal, Badge, etc.) instead of native elements for consistency
- Dark mode is supported via the `useColorMode` composable
## UI

- Shared UI components live in `resources/js/Components/shared/`.
- Prefer wrapper components over native elements when equivalents already exist.
- Dark mode exists and should not be broken.

## Package Ownership

- Do not default to fixing generic bridge, provider, registry, caching, or integration-runtime behavior inside `app/`.
- First decide whether the behavior belongs to OpenCompany or to a sibling package.
- Inspect the real package source before patching. In this workspace, package code may be path-based or symlinked into `vendor/`.
- Common package sources:
- `tmp/prism-relay`
- `tmp/prism-codex`
- `../integrations/core`
- `../integrations/packages/*`
- Do not patch `vendor/` for durable fixes.
- If a fix stays app-local, note why it is OpenCompany-specific.

## Working Notes

- Prefer `rg` and `rg --files` for search.
- Keep edits targeted. Do not revert unrelated user changes.
- Put audits and investigations into `docs/`.
- MCP CLI: `~/.local/bin/mcp-cli`
- MCP config: `~/.config/mcp/mcp_servers.json`
- Common MCP usage: `mcp-cli`, `mcp-cli info <server>`, `mcp-cli call <server> <tool> '<json>'`

## Docs

- Repo rules and local setup: `CLAUDE.md`
- Docs index: `docs/INDEX.md`
- Runtime audit: `docs/architecture/runtime-alignment-implementation-audit.md`
- Plane tool is available via `mcp-cli`
89 changes: 80 additions & 9 deletions app/Agents/OpenCompanyAgent.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
use App\Models\TaskStep;
use App\Models\User;
use App\Services\AgentDocumentService;
use App\Services\Memory\ContextPruner;
use App\Services\Memory\PromptFrameBuilder;
use App\Services\Memory\ToolResultDeduplicator;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\Conversational;
use Laravel\Ai\Contracts\HasTools;
Expand All @@ -21,12 +24,18 @@
use Laravel\Ai\Responses\Data\ToolCall;
use Laravel\Ai\Responses\Data\ToolResult;
use Illuminate\Support\Str;
use OpenCompany\PrismRelay\Contracts\HasSystemPrompts;

#[MaxTokens(16_384)]
class OpenCompanyAgent implements Agent, HasTools, Conversational
class OpenCompanyAgent implements Agent, HasTools, Conversational, HasSystemPrompts
{
use Promptable;

/**
* @var array<string, mixed>|null
*/
private ?array $promptFrameCache = null;

/** @var array<string, mixed> */
private array $resolvedProvider;

Expand All @@ -39,6 +48,9 @@ public function __construct(
private ChannelConversationLoader $conversationLoader,
private DynamicProviderResolver $providerResolver,
private ToolRegistry $toolRegistry,
private PromptFrameBuilder $promptFrameBuilder,
private ToolResultDeduplicator $toolResultDeduplicator,
private ContextPruner $contextPruner,
private ?string $taskId = null,
) {
$this->resolvedProvider = $this->providerResolver->resolve($this->agent);
Expand Down Expand Up @@ -79,11 +91,51 @@ public function resumeFrom(string $taskId): static
/**
* Get the instructions (system prompt) for this agent.
*
* Assembles from identity files in the same order as AgentChatService.
* Returns the full concatenated prompt (stable + volatile). When a
* SystemPromptBag is bound, CachingPrismGateway uses the split prompts
* from the bag instead for cache-friendly framing.
*/
public function instructions(): string
{
return implode('', array_column($this->buildSections(), 'content'));
return $this->promptFrame()['full_prompt'];
}

/**
* Get the full instruction set before stable/volatile splitting.
*/
public function fullInstructions(): string
{
return $this->promptFrame()['full_prompt'];
}

/**
* Get the volatile runtime context that should travel with the user prompt.
*/
public function volatilePromptContext(): string
{
return $this->promptFrame()['volatile_prompt'];
}

/**
* Runtime context now travels as additional system prompts via the gateway,
* so the user prompt should remain unchanged.
*/
public function preparePrompt(string $prompt): string
{
return $prompt;
}

/**
* @return string[]
*/
public function systemPrompts(): array
{
$frame = $this->promptFrame();

return array_values(array_filter([
trim($frame['stable_prompt']),
trim($frame['volatile_prompt']),
], fn (string $prompt) => $prompt !== ''));
}

/**
Expand All @@ -94,10 +146,27 @@ public function instructions(): string
*/
public function instructionsBreakdown(): array
{
return array_values(array_map(
fn (array $s) => ['label' => $s['label'], 'chars' => mb_strlen($s['content'])],
$this->buildSections(),
));
return $this->promptFrame()['stable_breakdown'];
}

/**
* @return array<int, array{label: string, chars: int}>
*/
public function volatileInstructionsBreakdown(): array
{
return $this->promptFrame()['volatile_breakdown'];
}

/**
* @return array<string, mixed>
*/
public function promptFrame(): array
{
if ($this->promptFrameCache !== null) {
return $this->promptFrameCache;
}

return $this->promptFrameCache = $this->promptFrameBuilder->splitSections($this->buildSections());
}

/**
Expand Down Expand Up @@ -228,7 +297,7 @@ private function injectPeerCards(array &$sections, Channel $channel): void
*/
public function messages(): iterable
{
$messages = $this->conversationLoader->load($this->channelId, $this->agent, $this->instructions());
$messages = $this->conversationLoader->load($this->channelId, $this->agent, $this->fullInstructions());

if ($this->resumeFromTaskId) {
$messages = $this->injectCheckpointedSteps($messages);
Expand Down Expand Up @@ -289,7 +358,9 @@ private function injectCheckpointedSteps(iterable $messages): array
);
}

return $messages;
$deduplicated = $this->toolResultDeduplicator->deduplicate($messages)['messages'];

return $this->contextPruner->prune($deduplicated)['messages'];
}

/**
Expand Down
10 changes: 4 additions & 6 deletions app/Agents/Providers/CodexPrismGateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,16 @@
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException;
use Laravel\Ai\Gateway\Prism\PrismGateway;
use Laravel\Ai\Gateway\TextGenerationOptions;
use Laravel\Ai\Providers\Provider;
use OpenCompany\PrismRelay\Bridge\CachingPrismGateway;

/**
* Custom PrismGateway that routes Codex requests to the registered 'codex' Prism provider.
* Custom gateway that routes Codex requests to the registered 'codex' Prism provider.
*
* The Codex provider extends OpenAI and uses the same Responses API format, but routes
* requests through chatgpt.com/backend-api/codex/ using OAuth tokens from a ChatGPT
* Pro/Plus subscription — $0 token costs.
* Extends CachingPrismGateway for prompt cache support.
*/
class CodexPrismGateway extends PrismGateway
class CodexPrismGateway extends CachingPrismGateway
{
public function __construct(Dispatcher $events)
{
Expand Down
Loading
Loading