Skip to content
Merged
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
44 changes: 44 additions & 0 deletions .claude/reference/reuse-before-you-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Reuse before you build

Before you write a new interface, wrapper, or helper, find out if a package
already does the job.

**Scope**
- Applies when you are about to add an abstraction or a utility.
- Read `Directory.Packages.props` for the packages we already have.
- Search `Microsoft.Extensions.AI*` and `Microsoft.Agents.AI*` first. They are
our base. Then search the rest.

**Prove the overlap**
- "The framework already does this" is proof only when a scratch probe
compiles and runs against the pinned version.
- Put the probe in the scratchpad. Do not commit it.
- Name the exact type and member in your report, such as
`DelegatingAIFunction`.
- If the probe fails, say what is missing, then write our own.
- Reason: in the 2026-08-26 framework audit, 4 of 5 "already covered" claims
were wrong.

**A package we do not have yet**
- Propose the package before you write the code. Do not hand-roll it and
mention the package afterwards.
- Propose one when it replaces more than ~100 lines of our code, or when the
job is a known-hard domain: parsing, unicode, time zones, crypto, retry and
backoff, globbing, schema validation.
- Do not install it. Give the owner a short note with:
- the package, the exact version, and the licence
- what it replaces, in lines and files
- how many packages it pulls in, and any native binaries
- the date of the last release
- AgentCore ships as NuGet packages. Every dependency lands on every consumer.
- MIT, Apache-2.0, BSD, and MS-PL are safe. A copyleft licence (GPL, AGPL,
LGPL, MPL) is a stop. Report it and stop.
- Use a stable version. Prerelease needs the owner's OK.
- Pin the version in `Directory.Packages.props`.

**Write our own when**
- No maintained package exists.
- The library type would leak into our public API.
- Our need is under ~20 lines and the package is not.

Say which one applies in your report.
74 changes: 74 additions & 0 deletions .claude/rules/comments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
paths:
- "**/*.cs"
- "**/*.sql"
- "**/*.csproj"
- "**/*.props"
- "**/*.targets"
---

# Comments

Write comments a maintainer needs. Do not write comments that repeat the code.

**Default: no comment.** Code should say *what*. A comment says *why*.
Before you keep a comment, ask: "Would a good teammate be surprised here
without it?" If no, delete it.

If you cannot name the reader and the surprise, do not write the comment.

## Do write these

- Why a non-obvious choice was made, when the obvious choice is wrong
- A workaround, with the cause and a link (bug report, issue, ticket)
- An invariant or precondition a caller must hold
- Units, ranges, and limits that the type does not show (`ms`, `0-1`, `UTC`)
- A pointer to the spec or design section a rule comes from
- A real safety warning: races, ordering, money, data loss
- Doc comments on public API, in the language's normal format

## Never write these

- Restating the line: `// increment the counter`, `// return the result`
- Step narration: `// Step 1: ...`, `// First we ..., then we ...`
- Changelog or diff talk: `// added for task 5`, `// was 30s before`,
`// new`, `// removed the old path`
- Talking to the reviewer: `// as requested`, `// note: I chose this because`
- Obvious types or names: `// the user id (string)`
- Banner art or divider lines: `// ===== HELPERS =====`
- Commented-out code. Delete it. Git has it.
- Bare `TODO`. Only keep a TODO with a ticket link.

## Style

- Match the comment density of the file you are editing. Do not add a new
style to an old file.
- One idea per comment. Keep it short.
- Say it in plain words. No hedging, no apologies, no jokes.
- Keep the comment next to the code it explains.
- If a comment explains a confusing block, try to fix the code first. A better
name or a small function often beats the comment.

## Cleaning up old comments

Fix comment slop in code you are **already editing**. Do not go hunting.

**Scope**
- Only inside the function, block, or file you are changing for the task.
- Never open an unrelated file just to clean comments.
- Never make a "comment cleanup" commit unless the user asks for one.

**Do this when you see it**
- Delete any comment that matches the "Never write these" list.
- Delete a comment that the code no longer matches. A stale comment is worse
than no comment.
- Delete commented-out code.
- If a comment says *why* but says it badly, keep the *why* and shorten it.

**Do not touch these**
- Licence and copyright headers.
- Machine-read comments: `// eslint-disable`, `# type: ignore`, `#pragma`,
`// nolint`, `<!-- prettier-ignore -->`, codegen markers.
- Doc comments that a doc tool publishes.
- A comment you do not understand. It may hold knowledge that is not in the
code. Leave it, and say so in your report.
48 changes: 48 additions & 0 deletions .claude/rules/csharp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
paths:
- "**/*.cs"
---

# C#

## Style comes from the analyzers

`.editorconfig` raises every IDE style rule to a suggestion, and SonarQube reports the rest. Those
diagnostics are the style guide. The owner ruled the repo's older habits to be slop, so follow the
diagnostic, not the code around it.

- Clear every IDE and Sonar diagnostic in the code you touch. Most of them push you to the newer
C# form: collection expressions, pattern matching, `is null`, primary constructors.
- Namespaces are block-scoped. That is the Roslyn default here; do not convert a file to
file-scoped.
- `dotnet format style` is safe except for these fixers. Pass them to `--exclude-diagnostics` and
fix by hand:
- IDE0010, IDE0072: they write `throw new NotImplementedException()` stubs. Name every member
and keep `_ =>` for CS8524.
- IDE0130: crashes the workspace. Move or rename the file by hand.
- IDE0032: rewrites `ref _field` to `ref Property` and breaks `Interlocked`.
- IDE0001, IDE0002: shorten a cref to `ShellKind.X`, which is ambiguous with MAF Tools.Shell's
`ShellKind`. Keep the `using ShellKind = ...` alias.
- IDE0010 and Sonar S3458 conflict on empty cases before `default:`. Give the stacked group its
own body, or replace the switch with a `FrozenDictionary` (see `ConversationEventKinds`).

## File size

Keep each `.cs` file under 400 lines.

**Scope**
- Apply this to files you are **already editing** for the task. Do not go hunting.
- If a file you must edit is over 400 lines, split it first, then make your change.
- Skip generated files and `*.schema.json`.

**Rules**
- One public type per file. Name the file after the type.
- A private helper type gets its own file when it passes 20 lines.
- Split by responsibility. Move one cohesive group of members into its own
class, in the same namespace and folder.
- A split that only moves lines does not count. `FooPart2.cs` and a new
`partial` are not a split.
- `partial` is for source generators.

If you cannot find a clean seam, stop and say so in your report. Do not
split at a random line.
44 changes: 44 additions & 0 deletions .claude/rules/tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
paths:
- "tests/**"
---

# Tests

When you finish adding or changing tests, run the `test-cleanup-audit` skill on the diff before you
report done.

## Prove each new test fails when the code is wrong

A test that passes whatever the code does proves nothing. Before you report done, do this for
every test you add or change:

1. Break the production line the test guards: return the wrong value, skip the call, or flip
the setting.
2. Run the test. It must fail.
3. Put the line back. Check with `git diff` that the file is byte-identical to before.

List each check in your report: the test, the line you broke, and the failure you saw. If a
test still passes, fix the test before you report done.

## Live-server tests skip without a word

`[PostgresFact]`, `[QdrantFact]`, and `[S3Fact]` skip when their variables are unset. A green
`dotnet test` then proves nothing about them. Name the variables to run them:

- `AGENTCORE_TEST_POSTGRES`: a connection string. The role must be allowed to create databases
and roles.
- `AGENTCORE_TEST_QDRANT`: `host:port` of the **gRPC** port (6334), not 6333.
- `AGENTCORE_TEST_S3_ENDPOINT` and `AGENTCORE_TEST_S3_BUCKET`.

A full run against one Qdrant reports about 26 false `RpcException: Timeout expired` failures in
`KbShapedCorpusFixture`. The 30 s gRPC deadline expires under load. Re-run the failing class with
`--filter` before you blame your change. Run `pgrep -f testhost.dll` first: a second run against
the same Qdrant always times out.

## Conventions

- Drive time with `FakeTimeProvider` and `WaitForTimersAsync`. A `Task.Delay` settle loop is
flaky.
- Mark a shared helper that asserts with `[AssertionMethod]` from `AgentCore.TestSupport`.
SonarQube S2699 honours it by name.
132 changes: 132 additions & 0 deletions .claude/rules/voice-components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
---
paths:
- "src/AgentCore.AspNetCore/Voice/**"
- "tests/AgentCore.AspNetCore.Tests/Voice/**"
---

# Voice components

```mermaid
flowchart TB
classDef vendor fill:#fde7d9,stroke:#c2410c,color:#1d1d1f
classDef port fill:#e0ecff,stroke:#1d4ed8,color:#1d1d1f
classDef turn fill:#e6f6ea,stroke:#15803d,color:#1d1d1f
classDef speech fill:#f3e8ff,stroke:#7e22ce,color:#1d1d1f
classDef engine fill:#fff4c2,stroke:#a16207,color:#1d1d1f

Caller(["📞 Caller via Telnyx"])

subgraph Route["Routing"]
MapCall["MapCall<br/>/v1/{entry}/call"]
Boot["AgentCoreBoot<br/>ConversationHandler"]
end

subgraph Vendor["Vendors/TelnyxRelay (one per call)"]
Conn["TelnyxRelayConnection"]
Pump["JsonWebSocketPump<br/>read loop"]
Reader["TelnyxRelayFrameReader"]
In["TelnyxRelayInput<br/>(IConversationInputPort)"]
Out["TelnyxRelayOutput<br/>(IConversationOutputPort)"]
Sender["JsonWebSocketSender<br/>write loop"]
Obs["ConnectionTaskObserver"]
end

subgraph Turn["Turn taking"]
Loop["VoiceConversationLoop"]
UTH["UserTurnHandler"]
Act["VoiceActivity"]
end

subgraph Speech["Speech control"]
VS["VoiceSession<br/>agent/user state, away timer"]
Sched["SpeechScheduler<br/>queue + background speeches"]
Handle["SpeechHandle<br/>one per speech"]
Pipe["PipelineReply<br/>step loop"]
Say["SayReply<br/>fixed text"]
Fwd["TextForwarding"]
Hear["ReplyHearing<br/>what caller heard"]
Fill["ToolFillerScope<br/>→ FillerScheduler"]
Met["TurnMetrics"]
end

subgraph Engine["AgentCore.Application"]
Stream["EngineReplyStream"]
Sessions["IConversationSessions"]
CS["ConversationSession<br/>(IConversationPort)"]
end

Caller <-->|WebSocket JSON| MapCall
MapCall --> Boot --> Conn
Conn --> Pump & Sender & Loop
Pump --> Reader --> In
In -->|"ConversationInput:<br/>Started / Utterance / Barge"| Loop
Loop -->|GetOrOpen / Close| Sessions --> CS
Loop -->|interim / final| UTH
Loop -->|Barge| Act
Loop -->|Barge: StopAsync| Out
UTH -->|TryGenerateReply| Act
UTH --> VS
Act --> Stream
Act --> Pipe
Act -->|schedule| Sched
VS --> Sched
VS -->|Say| Say
Sched -->|authorize| Handle
Pipe --> Handle
Pipe -->|read step text| Stream
Stream -->|StartTurnAsync / Cut| CS
Pipe --> Fwd -->|BeginReply / SpeakAsync / CompleteAsync| Out
Say --> Fwd
Pipe --> Hear -->|TurnCut| Stream
Pipe --> Fill -->|Say| VS
Pipe --> Met
Out --> Sender -->|JSON frames| Caller
Conn --> Obs

class Conn,Pump,Reader,In,Out,Sender,Obs vendor
class MapCall,Boot port
class Loop,UTH,Act turn
class VS,Sched,Handle,Pipe,Say,Fwd,Hear,Fill,Met speech
class Stream,Sessions,CS engine
```

```mermaid
sequenceDiagram
autonumber
participant T as Telnyx
participant P as Pump + Input
participant L as VoiceConversationLoop
participant U as UserTurnHandler
participant A as VoiceActivity
participant S as SpeechScheduler
participant R as PipelineReply
participant E as EngineReplyStream → ConversationSession
participant O as Output + Sender

T->>P: setup frame
P->>L: Started(conversationId)
L->>L: GetOrOpen session, make VoiceActivity + UserTurnHandler
T->>P: interim transcript
P->>L: Utterance(IsFinal=false)
L->>U: OnInterimTranscript → user Speaking
T->>P: final transcript
P->>L: Utterance(IsFinal=true)
L->>U: OnFinalTranscript
U->>A: InterruptByFinalTranscript (cut old speech)
U->>A: TryGenerateReply(text)
A->>E: Start engine turn
A->>S: ScheduleSpeech(handle)
S->>R: authorize speech
R->>R: wait for user silence
loop each model step
E-->>R: step text
R->>O: BeginReply / SpeakAsync / CompleteAsync
O->>T: speak frames
opt step calls a tool
R->>R: WaitForToolsAsync (filler may Say)
R->>S: re-schedule (force) for next step
end
end
R->>S: speech done → agent Listening, away timer armed
Note over T,O: Barge-in: Telnyx sends Barge → Loop calls Output.StopAsync<br/>and VoiceActivity.InterruptByAudioActivity → ReplyHearing cuts the engine turn to what was heard
```
13 changes: 13 additions & 0 deletions .claude/rules/voice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
paths:
- "src/AgentCore.AspNetCore/Voice/**"
- "tests/AgentCore.AspNetCore.Tests/Voice/**"
---

# Voice

Read `.claude/rules/voice-components.md` before you change this folder. It maps the parts of one phone
call: the three loops (read, listen, write), who decides *when* the agent talks, and where the
engine (`IConversationPort`) decides *what* it says.

When your change adds, removes, or rewires a component, update the diagram in the same change.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,6 @@ eval-report.html

# Harness-created agent worktrees. Never commit these.
.claude/worktrees/
CLAUDE.md

docs/
.superpowers/
Expand Down
26 changes: 26 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
AgentCore is a .NET 10 library. It ships as NuGet packages, so every dependency lands on every consumer.

Rules for C#, comments, tests, and the Voice folder live in `.claude/rules/`. Each one loads when you open a file it covers.

## Build and test

- `dotnet build` fails on any warning. Fix the cause. Ask the owner before you add a `NoWarn`, a `#pragma warning disable`, or an `.editorconfig` severity change.
- `dotnet test` skips the Postgres, Qdrant, and S3 tests without a word when their `AGENTCORE_TEST_*` variables are unset. A green run does not cover them. See `.claude/rules/tests.md`.

## Before you add an interface, wrapper, helper, or package

Read `.claude/reference/reuse-before-you-build.md` first.

## Agent skills

### Issue tracker

Issues and specs live as GitHub issues in MatthewHsu1/AgentCore, used through the `gh` CLI. See `docs/agents/issue-tracker.md`.

### Triage labels

The five default labels: `needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`. See `docs/agents/triage-labels.md`.

### Domain docs

Single-context: one `CONTEXT.md` and `docs/adr/` at the repo root. See `docs/agents/domain.md`.
Loading
Loading