Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5b6080b
CMP-1 chore: add agent interface skills
ripgrim Aug 4, 2026
7cdd315
CMP-1 feat: build durable custom agents
ripgrim Aug 4, 2026
46960a8
CMP-1 feat: refine agent builder interactions
ripgrim Aug 4, 2026
f75332e
CMP-36 merge: integrate upstream organization routing
ripgrim Aug 4, 2026
40e8063
CMP-36 merge: reconcile latest upstream navigation
ripgrim Aug 5, 2026
d1b3919
CMP-34 feat: ship private durable agent creation workspace
ripgrim Aug 5, 2026
ef76072
CMP-46 feat: refine agent builder interactions
ripgrim Aug 5, 2026
7a94b19
CMP-46 fix: anchor builder questions to the composer
ripgrim Aug 5, 2026
f923dfd
CMP-46 fix: interleave follow-up answers in chat
ripgrim Aug 5, 2026
a482ce4
add skills
ripgrim Aug 6, 2026
c17d422
CMP-55 feat: harden agent and conversation persistence
ripgrim Aug 6, 2026
64df22f
CMP-59 refactor: eliminate React maintenance debt
ripgrim Aug 6, 2026
db34fe6
CMP-46 feat: complete agent chat and builder workflows
ripgrim Aug 6, 2026
f0050d9
CMP-47 fix: stabilize application and development runtimes
ripgrim Aug 6, 2026
323f9cd
documentation
ripgrim Aug 6, 2026
3a14a86
Update environment.md
ripgrim Aug 6, 2026
dfb9ab5
CMP-36 merge: sync latest upstream main
ripgrim Aug 6, 2026
09a2c12
CMP-1 fix: secure agent session boundaries
ripgrim Aug 6, 2026
7b8a90a
CMP-1 fix: validate agent builder permissions
ripgrim Aug 6, 2026
91f17f9
CMP-1 fix: enforce deployed agent permissions
ripgrim Aug 6, 2026
24d0f74
CMP-1 test: add agent builder eval
ripgrim Aug 6, 2026
5f1a7cd
CMP-1 docs: document agent safety model
ripgrim Aug 6, 2026
97fadf6
CMP-1 chore: remove agent skills from builder
ripgrim Aug 6, 2026
064935e
Delete THIRD_PARTY_NOTICES.md
ripgrim Aug 6, 2026
4718cc9
add pazazz
ripgrim Aug 6, 2026
ac976c8
CMP-1 fix: polish agent builder interactions
ripgrim Aug 6, 2026
2e9e57b
CMP-1 fix: detect conversational agent requests
ripgrim Aug 6, 2026
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
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ GOOGLE_CLIENT_SECRET=""
# out on its next minute.
# AGENT_URL="http://127.0.0.1:2000"

# Port used by the self-hosted Eve service. Hosted runtimes can keep injecting
# their standard PORT variable instead.
# AGENT_PORT="2000"

# Lets a signed-in rep talk to the agent from the contact sheet.
#
# The browser never calls the agent directly. The app proxies /eve/v1/* on its
Expand Down
51 changes: 51 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,54 @@ rules and skills you read.
## Design

@docs/design.md

## Median Tasks

Median can use a project-local workspace binding. If this repository has
`.median/config.json`, run `mdn` commands from inside this repository so the
correct Median workspace profile is selected. The local config stores only a
profile name; API keys stay in your user config.

To bind this repository to a workspace:

```
mdn setup --local
```

Before starting work, check your assigned tasks:

```
mdn tasks --agent <your-agent-name>
```

When picking up a task:

```
mdn status <TASK-CODE> in_progress --agent <your-agent-name>
```

When completing a task:

```
mdn status <TASK-CODE> ready --agent <your-agent-name>
```

To create a new task:

```
mdn create --title "Description" --status todo --priority medium --agent <your-agent-name>
```

## Commit Messages & Pull Requests

Always include the Median task ID in commit messages and PR titles so tasks get marked automatically.

```
git commit -m "MDN-42 fix: resolve auth token expiry"
```

For pull requests, include the task ID in the title:

```
MDN-42 fix: resolve auth token expiry
```
52 changes: 52 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1 +1,53 @@
@AGENTS.md


## Median Tasks

Median can use a project-local workspace binding. If this repository has
`.median/config.json`, run `mdn` commands from inside this repository so the
correct Median workspace profile is selected. The local config stores only a
profile name; API keys stay in your user config.

To bind this repository to a workspace:

```
mdn setup --local
```

Before starting work, check your assigned tasks:

```
mdn tasks --agent <your-agent-name>
```

When picking up a task:

```
mdn status <TASK-CODE> in_progress --agent <your-agent-name>
```

When completing a task:

```
mdn status <TASK-CODE> ready --agent <your-agent-name>
```

To create a new task:

```
mdn create --title "Description" --status todo --priority medium --agent <your-agent-name>
```

## Commit Messages & Pull Requests

Always include the Median task ID in commit messages and PR titles so tasks get marked automatically.

```
git commit -m "MDN-42 fix: resolve auth token expiry"
```

For pull requests, include the task ID in the title:

```
MDN-42 fix: resolve auth token expiry
```
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ short version:

| Command | |
| --- | --- |
| `bun run dev` | Everything, in watch mode |
| `bun run dev` | Prepare the local database, then run everything in dependency-aware watch mode |
| `bun run build` | Build all apps and packages |
| `bun run test` | Run the test suite |
| `bun run check-types` | `tsc --noEmit` everywhere |
Expand Down
5 changes: 5 additions & 0 deletions apps/agent/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,9 @@ export default defineAgent({
fallback: DEFAULT_AGENT_MODEL.id,
events: { "session.started": () => selectedModel() },
}),
limits: {
maxInputTokensPerSession: 500_000,
maxOutputTokensPerSession: 50_000,
sessionTimeoutMs: 30 * 24 * 60 * 60 * 1000,
},
});
140 changes: 129 additions & 11 deletions apps/agent/agent/channels/crm.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,33 @@
import { timingSafeEqual } from "node:crypto";
import { EnrichmentStatus } from "@crm/db";
import { defineChannel, POST } from "eve/channels";
import { verifyKey } from "../lib/context-dev";
import {
builderIdFromToken,
dispatchAgentRun,
dispatchBuilderSubmission,
drainAgentRuns,
drainBuilder,
failRun,
runIdFromToken,
} from "../lib/custom-agent-dispatch";
import { brief, drainAll, taskAuth } from "../lib/dispatch";
import { settle } from "../lib/enrichment";
import { finishRun } from "../lib/run-runtime";
import { completeTask, taskSubject } from "../lib/tasks";

const TASK_MARKER = "task:";

function authorised(request: Request): boolean {
const secret = process.env.AGENT_BRIDGE_SECRET?.trim();
if (!secret) return false;
const header = request.headers.get("authorization");
if (!header?.startsWith("Bearer ")) return false;
const candidate = Buffer.from(header.slice("Bearer ".length));
const expected = Buffer.from(secret);
if (candidate.length !== expected.length) return false;

return request.headers.get("authorization") === `Bearer ${secret}`;
return timingSafeEqual(candidate, expected);
}

export function taskToken(taskId: string): string {
Expand Down Expand Up @@ -47,6 +63,30 @@ export default defineChannel({
return new Response(null, { status: 202 });
}),

POST(
"/internal/crm/builder-dispatch",
async (request, { send, waitUntil }) => {
if (!authorised(request)) {
return new Response("Unauthorized", { status: 401 });
}

waitUntil(drainBuilder(send));
return new Response(null, { status: 202 });
},
),

POST(
"/internal/crm/agent-dispatch",
async (request, { send, waitUntil }) => {
if (!authorised(request)) {
return new Response("Unauthorized", { status: 401 });
}

waitUntil(drainAgentRuns(send));
return new Response(null, { status: 202 });
},
),

POST("/internal/crm/verify-key", async (request) => {
if (!authorised(request)) {
return new Response("Unauthorized", { status: 401 });
Expand All @@ -71,29 +111,107 @@ export default defineChannel({
],

events: {
async "message.completed"(data, channel) {
const conversationId = builderIdFromToken(channel.continuationToken);
if (!conversationId || !data.message?.trim()) return;

await import("@crm/db").then(({ db }) =>
db.agentConversation.updateMany({
where: { id: conversationId, kind: "BUILDER" },
data: {
lastAssistantAt: new Date(),
lastMessageAt: new Date(),
messageCount: { increment: 1 },
},
}),
);
},

async "session.waiting"(_data, channel) {
const taskId = taskFromToken(channel.continuationToken);
if (!taskId) return;
if (taskId) {
const subject = await completeTask(taskId, "ran");
if (subject) await settle(subject, EnrichmentStatus.COMPLETE);
return;
}

const subject = await completeTask(taskId, "ran");
if (subject) await settle(subject, EnrichmentStatus.COMPLETE);
const conversationId = builderIdFromToken(channel.continuationToken);
if (!conversationId) return;

await import("@crm/db").then(({ db }) =>
db.agentConversation.updateMany({
where: { id: conversationId, kind: "BUILDER" },
data: { continuationToken: channel.continuationToken },
}),
);
},

async "turn.failed"(data, channel) {
const taskId = taskFromToken(channel.continuationToken);
if (!taskId) return;

const reason =
typeof data === "object" && data && "error" in data
? String((data as { error: unknown }).error)
: "The research turn failed.";
typeof data === "object" && data && "message" in data
? String((data as { message: unknown }).message)
: "The agent turn failed.";

if (taskId) {
const subject = await taskSubject(taskId);
if (subject) await settle(subject, EnrichmentStatus.FAILED, reason);
return;
}

const subject = await taskSubject(taskId);
if (subject) await settle(subject, EnrichmentStatus.FAILED, reason);
const runId = runIdFromToken(channel.continuationToken);
if (runId) await failRun(runId, "TURN_FAILED", reason);
},

async "session.completed"(_data, channel) {
const runId = runIdFromToken(channel.continuationToken);
if (!runId) return;

const { db } = await import("@crm/db");
const run = await db.agentRun.findUnique({
where: { id: runId },
select: { status: true, summary: true },
});
if (run?.status === "RUNNING") {
await finishRun(runId, {
summary: run.summary ?? "The agent run completed.",
});
}
},

async "session.failed"(data, channel) {
const conversationId = builderIdFromToken(channel.continuationToken);
if (conversationId) {
const { db } = await import("@crm/db");
await db.agentConversation.updateMany({
where: { id: conversationId, kind: "BUILDER" },
data: {
continuationToken: channel.continuationToken,
lastAssistantAt: new Date(),
lastMessageAt: new Date(),
},
});
return;
}

const runId = runIdFromToken(channel.continuationToken);
if (runId) await failRun(runId, data.code, data.message);
},
},

async receive(input, { send }) {
const builderSubmissionId =
typeof input.target?.builderSubmissionId === "string"
? input.target.builderSubmissionId
: null;
if (builderSubmissionId) {
return dispatchBuilderSubmission(builderSubmissionId, send);
}

const runId =
typeof input.target?.runId === "string" ? input.target.runId : null;
if (runId) return dispatchAgentRun(runId, send);

const taskId =
typeof input.target?.taskId === "string" ? input.target.taskId : null;

Expand Down
Loading
Loading