Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 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
8 changes: 4 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,21 +98,21 @@ curl http://localhost:3000/api/v1/health # backend (via web proxy)

```text
src/ai_company/
api/ # Litestar REST + WebSocket API (controllers, guards, channels, JWT + API key auth)
api/ # Litestar REST + WebSocket API (controllers, guards, channels, JWT + API key auth, approval gate integration)
budget/ # Cost tracking, budget enforcement (pre-flight/in-flight checks, auto-downgrade), billing periods, cost tiers, quota/subscription tracking, CFO cost optimization (anomaly detection, efficiency analysis, downgrade recommendations, approval decisions), spending reports, budget errors (BudgetExhaustedError, DailyLimitExceededError, QuotaExhaustedError)
cli/ # CLI interface (future — thin API wrapper if needed)
communication/ # Message bus, dispatcher, messenger, channels, delegation, loop prevention, conflict resolution, meeting protocol
config/ # YAML company config loading and validation
core/ # Shared domain models, base classes, and resilience config (RetryConfig, RateLimiterConfig)
engine/ # Agent orchestration, execution loops, parallel execution, task decomposition, routing, task assignment, centralized single-writer task state engine (TaskEngine), task lifecycle, recovery, shutdown, workspace isolation, coordination (multi-agent pipeline: TopologyDispatcher protocol, 4 dispatchers — SAS/centralized/decentralized/context-dependent, wave execution, workspace lifecycle integration), coordination error classification, and prompt policy validation
engine/ # Agent orchestration, execution loops, parallel execution, task decomposition, routing, task assignment, centralized single-writer task state engine (TaskEngine), task lifecycle, recovery, shutdown, workspace isolation, coordination (multi-agent pipeline: TopologyDispatcher protocol, 4 dispatchers — SAS/centralized/decentralized/context-dependent, wave execution, workspace lifecycle integration), coordination error classification, prompt policy validation, approval gate (escalation detection, context parking/resume, EscalationInfo/ResumePayload models)
hr/ # HR engine: hiring, firing, onboarding, offboarding, agent registry, performance tracking (task metrics, collaboration scoring, trend detection), promotion/demotion (criteria evaluation, approval strategies, model mapping)
memory/ # Persistent agent memory (pluggable MemoryBackend protocol), backends/ (Mem0 adapter: backends/mem0/), retrieval pipeline (ranking, injection, context formatting, non-inferable filtering), shared org memory (org/), consolidation/archival (consolidation/)
persistence/ # Operational data persistence — pluggable PersistenceBackend protocol, SQLite initial (see Memory & Persistence design page)
observability/ # Structured logging, correlation tracking, log sinks
providers/ # LLM provider abstraction (LiteLLM adapter)
security/ # SecOps agent, rule engine (soft-allow/hard-deny, fail-closed), audit log, output scanner, output scan response policies (redact/withhold/log-only/autonomy-tiered), risk classifier, risk tier classifier, action type registry, ToolInvoker security integration, progressive trust (4 strategies: disabled/weighted/per-category/milestone), autonomy levels (presets, resolver, change strategy), timeout policies (park/resume)
templates/ # Pre-built company templates, personality presets, and builder
tools/ # Tool registry, built-in tools (file_system/, git, sandbox/, code_runner), MCP bridge (mcp/), role-based access
tools/ # Tool registry, built-in tools (file_system/, git, sandbox/, code_runner), MCP bridge (mcp/), role-based access, approval tool (request_human_approval)

web/ # Vue 3 + PrimeVue + Tailwind CSS dashboard
src/
Expand Down Expand Up @@ -151,7 +151,7 @@ web/ # Vue 3 + PrimeVue + Tailwind CSS dashboard
- **Every module** with business logic MUST have: `from ai_company.observability import get_logger` then `logger = get_logger(__name__)`
- **Never** use `import logging` / `logging.getLogger()` / `print()` in application code
- **Variable name**: always `logger` (not `_logger`, not `log`)
- **Event names**: always use constants from the domain-specific module under `ai_company.observability.events` (e.g. `PROVIDER_CALL_START` from `events.provider`, `BUDGET_RECORD_ADDED` from `events.budget`, `CFO_ANOMALY_DETECTED` from `events.cfo`, `CONFLICT_DETECTED` from `events.conflict`, `MEETING_STARTED` from `events.meeting`, `CLASSIFICATION_START` from `events.classification`, `CONSOLIDATION_START` from `events.consolidation`, `ORG_MEMORY_QUERY_START` from `events.org_memory`, `API_REQUEST_STARTED` from `events.api`, `API_ROUTE_NOT_FOUND` from `events.api`, `CODE_RUNNER_EXECUTE_START` from `events.code_runner`, `DOCKER_EXECUTE_START` from `events.docker`, `MCP_INVOKE_START` from `events.mcp`, `SECURITY_EVALUATE_START` from `events.security`, `HR_HIRING_REQUEST_CREATED` from `events.hr`, `PERF_METRIC_RECORDED` from `events.performance`, `TRUST_EVALUATE_START` from `events.trust`, `PROMOTION_EVALUATE_START` from `events.promotion`, `PROMPT_BUILD_START` from `events.prompt`, `MEMORY_RETRIEVAL_START` from `events.memory`, `MEMORY_BACKEND_CONNECTED` from `events.memory`, `MEMORY_ENTRY_STORED` from `events.memory`, `MEMORY_BACKEND_SYSTEM_ERROR` from `events.memory`, `AUTONOMY_ACTION_AUTO_APPROVED` from `events.autonomy`, `TIMEOUT_POLICY_EVALUATED` from `events.timeout`, `PERSISTENCE_AUDIT_ENTRY_SAVED` from `events.persistence`, `TASK_ENGINE_STARTED` from `events.task_engine`, `COORDINATION_STARTED` from `events.coordination`, `COMMUNICATION_DISPATCH_START` from `events.communication`, `COMPANY_STARTED` from `events.company`, `CONFIG_LOADED` from `events.config`, `CORRELATION_ID_CREATED` from `events.correlation`, `DECOMPOSITION_STARTED` from `events.decomposition`, `DELEGATION_STARTED` from `events.delegation`, `EXECUTION_LOOP_STARTED` from `events.execution`, `GIT_OPERATION_START` from `events.git`, `PARALLEL_EXECUTION_STARTED` from `events.parallel`, `PERSONALITY_LOADED` from `events.personality`, `QUOTA_CHECKED` from `events.quota`, `ROLE_ASSIGNED` from `events.role`, `ROUTING_STARTED` from `events.routing`, `SANDBOX_EXECUTE_START` from `events.sandbox`, `TASK_CREATED` from `events.task`, `TASK_ASSIGNMENT_STARTED` from `events.task_assignment`, `TASK_ROUTING_STARTED` from `events.task_routing`, `TEMPLATE_LOADED` from `events.template`, `TOOL_INVOKE_START` from `events.tool`, `WORKSPACE_CREATED` from `events.workspace`). Import directly: `from ai_company.observability.events.<domain> import EVENT_CONSTANT`
- **Event names**: always use constants from the domain-specific module under `ai_company.observability.events` (e.g. `PROVIDER_CALL_START` from `events.provider`, `BUDGET_RECORD_ADDED` from `events.budget`, `CFO_ANOMALY_DETECTED` from `events.cfo`, `CONFLICT_DETECTED` from `events.conflict`, `MEETING_STARTED` from `events.meeting`, `CLASSIFICATION_START` from `events.classification`, `CONSOLIDATION_START` from `events.consolidation`, `ORG_MEMORY_QUERY_START` from `events.org_memory`, `API_REQUEST_STARTED` from `events.api`, `API_ROUTE_NOT_FOUND` from `events.api`, `CODE_RUNNER_EXECUTE_START` from `events.code_runner`, `DOCKER_EXECUTE_START` from `events.docker`, `MCP_INVOKE_START` from `events.mcp`, `SECURITY_EVALUATE_START` from `events.security`, `HR_HIRING_REQUEST_CREATED` from `events.hr`, `PERF_METRIC_RECORDED` from `events.performance`, `TRUST_EVALUATE_START` from `events.trust`, `PROMOTION_EVALUATE_START` from `events.promotion`, `PROMPT_BUILD_START` from `events.prompt`, `MEMORY_RETRIEVAL_START` from `events.memory`, `MEMORY_BACKEND_CONNECTED` from `events.memory`, `MEMORY_ENTRY_STORED` from `events.memory`, `MEMORY_BACKEND_SYSTEM_ERROR` from `events.memory`, `AUTONOMY_ACTION_AUTO_APPROVED` from `events.autonomy`, `TIMEOUT_POLICY_EVALUATED` from `events.timeout`, `PERSISTENCE_AUDIT_ENTRY_SAVED` from `events.persistence`, `TASK_ENGINE_STARTED` from `events.task_engine`, `COORDINATION_STARTED` from `events.coordination`, `COMMUNICATION_DISPATCH_START` from `events.communication`, `COMPANY_STARTED` from `events.company`, `CONFIG_LOADED` from `events.config`, `CORRELATION_ID_CREATED` from `events.correlation`, `DECOMPOSITION_STARTED` from `events.decomposition`, `DELEGATION_STARTED` from `events.delegation`, `EXECUTION_LOOP_STARTED` from `events.execution`, `GIT_OPERATION_START` from `events.git`, `PARALLEL_EXECUTION_STARTED` from `events.parallel`, `PERSONALITY_LOADED` from `events.personality`, `QUOTA_CHECKED` from `events.quota`, `ROLE_ASSIGNED` from `events.role`, `ROUTING_STARTED` from `events.routing`, `SANDBOX_EXECUTE_START` from `events.sandbox`, `TASK_CREATED` from `events.task`, `TASK_ASSIGNMENT_STARTED` from `events.task_assignment`, `TASK_ROUTING_STARTED` from `events.task_routing`, `TEMPLATE_LOADED` from `events.template`, `TOOL_INVOKE_START` from `events.tool`, `WORKSPACE_CREATED` from `events.workspace`, `APPROVAL_GATE_ESCALATION_DETECTED` from `events.approval_gate`, `APPROVAL_GATE_INITIALIZED` from `events.approval_gate`, `APPROVAL_GATE_RISK_CLASSIFIED` from `events.approval_gate`, `APPROVAL_GATE_RESUME_TRIGGERED` from `events.approval_gate`). Import directly: `from ai_company.observability.events.<domain> import EVENT_CONSTANT`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider adding comma after "e.g." for consistency.

Static analysis flagged the missing comma. While minor, it improves readability in the dense event constant listing.

🧰 Tools
🪛 LanguageTool

[style] ~154-~154: A comma is missing here.
Context: ...nder ai_company.observability.events (e.g. PROVIDER_CALL_START from `events.prov...

(EG_NO_COMMA)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLAUDE.md` at line 154, The sentence listing event constants after "Event
names" is missing a comma after "e.g."; update the phrase "e.g.
`PROVIDER_CALL_START` ..." to "e.g., `PROVIDER_CALL_START` ..." so the
abbreviation is punctuated consistently in the long list (look for the "Event
names" paragraph and the "e.g." before the `PROVIDER_CALL_START`/events constant
list).

- **Structured kwargs**: always `logger.info(EVENT, key=value)` — never `logger.info("msg %s", val)`
- **All error paths** must log at WARNING or ERROR with context before raising
- **All state transitions** must log at INFO
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ graph TB

## Status

Core framework complete — agent engine, multi-agent coordination, API, security, HR, memory (including Mem0 backend adapter), and budget systems are implemented. Web dashboard (Vue 3 + PrimeVue + Tailwind CSS) is built. Remaining: approval workflow gates, CLI. See the [roadmap](docs/roadmap/index.md) for details.
Core framework complete — agent engine, multi-agent coordination, API, security, HR, memory (including Mem0 backend adapter), budget systems, and approval workflow gates are implemented. Web dashboard (Vue 3 + PrimeVue + Tailwind CSS) is built. Remaining: CLI. See the [roadmap](docs/roadmap/index.md) for details.

## License

Expand Down
5 changes: 5 additions & 0 deletions docs/design/engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,11 @@ async run(
`timeout_seconds` is set, wraps the call in `asyncio.wait`; on expiry
the run returns with `TerminationReason.ERROR` but cost recording and
post-execution processing still occur.
When escalations are detected after tool execution (via
`ToolInvoker.pending_escalations`), the `ApprovalGate` evaluates whether
parking is needed. If so, the context is serialized via `ParkService`
and persisted when a `ParkedContextRepository` is configured; the loop
then returns a `PARKED` result.
9. **Record costs** -- records accumulated `TokenUsage` to `CostTracker` (if
available). Cost recording failures are logged but do not affect the result.
10. **Apply post-execution transitions:**
Expand Down
30 changes: 29 additions & 1 deletion src/ai_company/api/approval_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,25 @@ async def save(self, item: ApprovalItem) -> ApprovalItem | None:
self._items[item.id] = item
return item

async def save_if_pending(
self,
item: ApprovalItem,
) -> ApprovalItem | None:
"""Update only if the stored item is still PENDING.

Returns the saved item on success, or ``None`` if the stored
item is no longer PENDING (concurrent decision detected).
"""
current = self._items.get(item.id)
if current is None:
return None
# Apply lazy expiration check before comparing status.
current = self._check_expiration(current)
if current.status != ApprovalStatus.PENDING:
return None
self._items[item.id] = item
return item

def _check_expiration(self, item: ApprovalItem) -> ApprovalItem:
"""Lazily expire a pending item past its ``expires_at``.

Expand All @@ -148,6 +167,15 @@ def _check_expiration(self, item: ApprovalItem) -> ApprovalItem:
approval_id=item.id,
)
if self._on_expire is not None:
self._on_expire(expired)
try:
self._on_expire(expired)
except MemoryError, RecursionError:
raise
Comment on lines +172 to +173

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Python 3 except tuple syntax missing parentheses — SyntaxError across all new files

except MemoryError, RecursionError: is Python 2 syntax. In Python 3 the comma-separated form (without parentheses) is a SyntaxError; the tuple must be explicitly wrapped. Any module that contains this form will fail to import entirely.

The same bug appears throughout all the new files added in this PR:

  • src/ai_company/api/approval_store.py:172
  • src/ai_company/api/controllers/approvals.py:100
  • src/ai_company/engine/approval_gate.py:127, 149, 202, 216
  • src/ai_company/engine/loop_helpers.py:69, 114, 186, 303, 387
  • src/ai_company/tools/approval_tool.py:162, 238
  • src/ai_company/tools/invoker.py:212, 295, 639

Note: the existing (pre-PR) code in invoker.py already uses the correct form except (MemoryError, RecursionError) as exc: at lines 453, 539, 575, and 698 — confirming this is an inconsistency introduced only in the new additions.

Fix every occurrence by wrapping the two types in parentheses:

Suggested change
except MemoryError, RecursionError:
raise
except (MemoryError, RecursionError):
raise

The pattern to apply globally is:

# Wrong (Python 2)
except MemoryError, RecursionError:

# Correct (Python 3)
except (MemoryError, RecursionError):
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ai_company/api/approval_store.py
Line: 172-173

Comment:
**Python 3 `except` tuple syntax missing parentheses — SyntaxError across all new files**

`except MemoryError, RecursionError:` is Python 2 syntax. In Python 3 the comma-separated form (without parentheses) is a `SyntaxError`; the tuple must be explicitly wrapped. Any module that contains this form will fail to import entirely.

The same bug appears throughout all the new files added in this PR:
- `src/ai_company/api/approval_store.py:172`
- `src/ai_company/api/controllers/approvals.py:100`
- `src/ai_company/engine/approval_gate.py:127, 149, 202, 216`
- `src/ai_company/engine/loop_helpers.py:69, 114, 186, 303, 387`
- `src/ai_company/tools/approval_tool.py:162, 238`
- `src/ai_company/tools/invoker.py:212, 295, 639`

Note: the existing (pre-PR) code in `invoker.py` already uses the correct form `except (MemoryError, RecursionError) as exc:` at lines 453, 539, 575, and 698 — confirming this is an inconsistency introduced only in the new additions.

Fix every occurrence by wrapping the two types in parentheses:

```suggestion
                except (MemoryError, RecursionError):
                    raise
```

The pattern to apply globally is:
```python
# Wrong (Python 2)
except MemoryError, RecursionError:

# Correct (Python 3)
except (MemoryError, RecursionError):
```

How can I resolve this? If you propose a fix, please make it concise.

except Exception:
logger.exception(
API_APPROVAL_EXPIRED,
approval_id=item.id,
note="on_expire callback failed",
)
Comment on lines +170 to +179

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Import the project MemoryError here.

src/ai_company/memory/errors.py defines the project-specific MemoryError, and src/ai_company/api/app.py deliberately re-raises it from _on_expire(). Because this module never imports that type, Line 153 only catches the built-in MemoryError; the project exception falls into except Exception and gets downgraded to a log.

Suggested fix
 from ai_company.api.errors import ConflictError
 from ai_company.core.approval import ApprovalItem  # noqa: TC001
 from ai_company.core.enums import (
     ApprovalRiskLevel,
     ApprovalStatus,
 )
+from ai_company.memory.errors import MemoryError as StoreMemoryError
 ...
-                except MemoryError, RecursionError:
+                except StoreMemoryError, MemoryError, RecursionError:
                     raise

As per coding guidelines, "Handle errors explicitly, never silently swallow exceptions."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ai_company/api/approval_store.py` around lines 151 - 160, The except
clause currently catches the built-in MemoryError but not the project-specific
MemoryError defined in ai_company.memory.errors, so re-raised project errors
fall into the generic Exception handler; import the project error (e.g., from
ai_company.memory.errors import MemoryError as ProjectMemoryError) and update
the except line to explicitly re-raise both RecursionError and the project
MemoryError (or include both types in the except tuple used for re-raising)
before the generic Exception that logs via
logger.exception(API_APPROVAL_EXPIRED, approval_id=item.id, note="on_expire
callback failed"); ensure the symbol _on_expire remains where called and no
error types are swallowed.

return expired
return item
Loading
Loading