Skip to content

Commit ad68021

Browse files
committed
chore: import agent history from relay monorepo
2 parents bdf5d47 + 51119e9 commit ad68021

10 files changed

Lines changed: 2504 additions & 0 deletions

File tree

packages/agent/README.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
![Agent Relay](https://agentrelay.com/readme-banners/relay.png)
2+
3+
<div align="center">
4+
5+
# @agent-relay/agent
6+
7+
The proactive agent runtime — one handler, three triggers, one workspace.
8+
9+
[![npm](https://img.shields.io/npm/v/@agent-relay/agent)](https://www.npmjs.com/package/@agent-relay/agent)
10+
[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](../../LICENSE)
11+
12+
**Website:** [agentrelay.com](https://agentrelay.com) · **Docs:** [agentrelay.com/docs](https://agentrelay.com/docs)
13+
14+
</div>
15+
16+
## What it does
17+
18+
`@agent-relay/agent` lets a developer ship a proactive agent in under 10 minutes and ~30 lines of code. One `agent({ ... })` call, one `onEvent` handler, and the runtime wires up cron schedules, file watches, and inbox messages into a single normalized stream — with shared workspace + `ctx`, policy gating, retry, dedup, and tracing.
19+
20+
It is built on [`@agent-relay/events`](../events) (Layer 2 — the transport-agnostic event stream) and exposes the workspace-aware Layer 3 surface developers actually want to call.
21+
22+
## Install
23+
24+
```bash
25+
npm install @agent-relay/agent
26+
```
27+
28+
## Quick start
29+
30+
```ts
31+
import { agent } from '@agent-relay/agent';
32+
33+
const handle = agent({
34+
workspace: 'support',
35+
schedule: { every: '5m' },
36+
watch: ['/linear/issues/**'],
37+
onEvent: async (event, ctx) => {
38+
switch (event.type) {
39+
case 'cron.tick':
40+
await ctx.messages.post({ channel: '#triage', text: 'tick' });
41+
return;
42+
43+
case 'relayfile.changed':
44+
const issue = await event.expand('full');
45+
await ctx.once(`triage:${event.id}`, async () => {
46+
// dedup-safe work
47+
});
48+
return;
49+
}
50+
},
51+
});
52+
53+
// ... later
54+
await handle.stop();
55+
```
56+
57+
## The handler contract
58+
59+
Every handler receives `(event, ctx)`:
60+
61+
- `event` — a normalized [`AgentEvent`](../events) envelope (lightweight; call `event.expand('full')` to materialize the resource)
62+
- `ctx` — workspace-aware context: `ctx.workspace`, `ctx.agentId`, `ctx.logger`, `ctx.signal` (AbortSignal), `ctx.schedule.{at,every,cancel}`, `ctx.once(key, fn)`, `ctx.files`, `ctx.messages`
63+
64+
## Policy enforcement
65+
66+
```ts
67+
agent({
68+
workspace: 'support',
69+
policy: { mode: 'approval-required', approvals: ['#triage-leads'] },
70+
onEvent: async (event, ctx) => {
71+
// each side-effect is gated through policy
72+
await ctx.messages.post({ channel: '#triage', text: 'ack' });
73+
},
74+
});
75+
```
76+
77+
| Mode | Behavior |
78+
| ------------------- | ------------------------------------------------ |
79+
| `suggest` | Surface intent to ctx; do not execute |
80+
| `auto` | Execute immediately (default) |
81+
| `approval-required` | Block on human approval via `approvals` channels |
82+
83+
## Hosted Agent deployment
84+
85+
```ts
86+
import { deployAgent } from '@agent-relay/agent';
87+
88+
await deployAgent({
89+
name: 'support-triage',
90+
workspace: 'support',
91+
source: './agent.ts',
92+
provider: { mode: 'byok' },
93+
});
94+
```
95+
96+
(See the [Proactive Agent Runtime spec §7](https://github.com/AgentWorkforce/cloud/blob/main/docs/proactive-runtime/spec.md) for Hosted Agent semantics.)
97+
98+
## Burn (LLM call tagging)
99+
100+
LLM calls inside `ctx.*` are auto-tagged with `(workspace, agentId, event.type, event.id)` for observability via `burn`. Wrap any code path explicitly with `withBurnTags({...}, fn)` if you need to add custom dimensions.
101+
102+
## Related
103+
104+
- [`@agent-relay/events`](../events) — Layer 2 normalized event stream that this builds on
105+
- [`@agent-relay/sdk`](../sdk) — broker control + workflow orchestration
106+
- [Proactive Agent Runtime spec](https://github.com/AgentWorkforce/cloud/blob/main/docs/proactive-runtime/spec.md) — the design this implements
107+
108+
## License
109+
110+
Apache-2.0 — see [LICENSE](../../LICENSE).

packages/agent/package.json

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"name": "@agent-relay/agent",
3+
"version": "7.1.1",
4+
"type": "module",
5+
"main": "dist/index.js",
6+
"types": "dist/index.d.ts",
7+
"exports": {
8+
".": {
9+
"types": "./dist/index.d.ts",
10+
"import": "./dist/index.js",
11+
"default": "./dist/index.js"
12+
}
13+
},
14+
"scripts": {
15+
"build": "tsc",
16+
"clean": "rm -rf dist",
17+
"test": "vitest run",
18+
"test:watch": "vitest",
19+
"typecheck": "tsc -p tsconfig.json --noEmit"
20+
},
21+
"dependencies": {
22+
"@agent-relay/events": "7.1.1",
23+
"@relaycast/sdk": "1.1.2",
24+
"@relayfile/sdk": "^0.7.2"
25+
},
26+
"devDependencies": {
27+
"@types/node": "^25.5.0",
28+
"typescript": "^5.7.0"
29+
},
30+
"repository": {
31+
"type": "git",
32+
"url": "git+https://github.com/AgentWorkforce/relay.git",
33+
"directory": "packages/agent"
34+
},
35+
"publishConfig": {
36+
"access": "public"
37+
},
38+
"files": [
39+
"dist",
40+
"README.md"
41+
]
42+
}

packages/agent/src/burn.ts

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import { AsyncLocalStorage } from 'node:async_hooks';
2+
3+
export interface BurnTags {
4+
workspace: string;
5+
agentId: string;
6+
eventType: string;
7+
eventId: string;
8+
occurredAt: string;
9+
}
10+
11+
const storage = new AsyncLocalStorage<BurnTags | null>();
12+
13+
const BURN_SOURCE_HEADER = 'x-relayburn-source';
14+
const BURN_TAG_HEADERS = {
15+
workspace: 'x-relayburn-tag-workspace',
16+
agentId: 'x-relayburn-tag-agent-id',
17+
eventType: 'x-relayburn-tag-event-type',
18+
eventId: 'x-relayburn-tag-event-id',
19+
occurredAt: 'x-relayburn-tag-occurred-at',
20+
} as const;
21+
const TRUSTED_PROVIDER_HOSTS = new Set(['api.openai.com', 'api.anthropic.com']);
22+
23+
const FETCH_WRAPPED = Symbol.for('@agent-relay/agent/burn-fetch-wrapped');
24+
const CLIENT_WRAPPED = Symbol.for('@agent-relay/agent/burn-client-wrapped');
25+
26+
type FetchLike = typeof globalThis.fetch;
27+
28+
type MaybeWrappedFetch = FetchLike & {
29+
[FETCH_WRAPPED]?: true;
30+
};
31+
32+
type MaybeWrappedClient = Record<PropertyKey, unknown> & {
33+
[CLIENT_WRAPPED]?: true;
34+
};
35+
36+
export async function withBurnTags<T>(tags: BurnTags, fn: () => Promise<T>): Promise<T> {
37+
ensureGlobalFetchWrapped();
38+
return await storage.run(tags, fn);
39+
}
40+
41+
export function tagWithCurrentBurnTags<T>(value: T): T {
42+
const tags = storage.getStore();
43+
return tags ? tagValue(value, tags) : value;
44+
}
45+
46+
function ensureGlobalFetchWrapped(): void {
47+
if (typeof globalThis.fetch !== 'function') {
48+
return;
49+
}
50+
51+
const currentFetch = globalThis.fetch as MaybeWrappedFetch;
52+
if (currentFetch[FETCH_WRAPPED]) {
53+
return;
54+
}
55+
56+
const original = currentFetch.bind(globalThis);
57+
const wrapped = createTaggedFetch(original, () => storage.getStore() ?? null);
58+
globalThis.fetch = wrapped;
59+
}
60+
61+
function createTaggedFetch(fetchImpl: FetchLike, getTags: () => BurnTags | null): FetchLike {
62+
const wrapped = (async (input: RequestInfo | URL, init?: RequestInit) => {
63+
const tags = getTags();
64+
if (!tags) {
65+
return await fetchImpl(input, init);
66+
}
67+
68+
const request = new Request(input, init);
69+
if (!shouldTagRequest(request)) {
70+
return await fetchImpl(request);
71+
}
72+
73+
const headers = new Headers(request.headers);
74+
headers.set(BURN_SOURCE_HEADER, 'agent-relay');
75+
headers.set(BURN_TAG_HEADERS.workspace, tags.workspace);
76+
headers.set(BURN_TAG_HEADERS.agentId, tags.agentId);
77+
headers.set(BURN_TAG_HEADERS.eventType, tags.eventType);
78+
headers.set(BURN_TAG_HEADERS.eventId, tags.eventId);
79+
headers.set(BURN_TAG_HEADERS.occurredAt, tags.occurredAt);
80+
81+
return await fetchImpl(new Request(request, { headers }));
82+
}) as MaybeWrappedFetch;
83+
84+
wrapped[FETCH_WRAPPED] = true;
85+
return wrapped;
86+
}
87+
88+
function shouldTagRequest(request: Request): boolean {
89+
const url = new URL(request.url);
90+
const host = url.hostname.toLowerCase();
91+
if (TRUSTED_PROVIDER_HOSTS.has(host)) {
92+
return true;
93+
}
94+
95+
const headers = request.headers;
96+
if (
97+
headers.has('anthropic-version') ||
98+
headers.has('openai-beta') ||
99+
headers.has('x-stainless-lang') ||
100+
headers.has('x-stainless-package-version')
101+
) {
102+
return true;
103+
}
104+
105+
const path = url.pathname.replace(/\/+$/, '') || '/';
106+
return (
107+
TRUSTED_PROVIDER_HOSTS.has(host) &&
108+
(path === '/v1/chat/completions' ||
109+
path === '/v1/responses' ||
110+
path === '/v1/completions' ||
111+
path === '/v1/embeddings' ||
112+
path === '/v1/messages' ||
113+
path === '/v1/messages/count_tokens' ||
114+
path === '/v1/complete')
115+
);
116+
}
117+
118+
function tagValue<T>(value: T, tags: BurnTags): T {
119+
if (typeof value === 'function') {
120+
return createTaggedFetch(value as FetchLike, () => tags) as T;
121+
}
122+
if (!value || typeof value !== 'object') {
123+
return value;
124+
}
125+
126+
const client = value as MaybeWrappedClient;
127+
if (client[CLIENT_WRAPPED]) {
128+
return value;
129+
}
130+
131+
if (patchFetchProperty(client, 'fetch', tags)) {
132+
client[CLIENT_WRAPPED] = true;
133+
return value;
134+
}
135+
if (patchOptionsFetch(client, '_options', tags)) {
136+
client[CLIENT_WRAPPED] = true;
137+
return value;
138+
}
139+
if (patchOptionsFetch(client, 'options', tags)) {
140+
client[CLIENT_WRAPPED] = true;
141+
return value;
142+
}
143+
if (patchNestedClient(client, '_client', tags)) {
144+
client[CLIENT_WRAPPED] = true;
145+
return value;
146+
}
147+
148+
return value;
149+
}
150+
151+
function patchNestedClient(target: MaybeWrappedClient, key: string, tags: BurnTags): boolean {
152+
const nested = target[key];
153+
if (!nested || typeof nested !== 'object') {
154+
return false;
155+
}
156+
157+
const nestedClient = nested as MaybeWrappedClient;
158+
return (
159+
patchFetchProperty(nestedClient, 'fetch', tags) ||
160+
patchOptionsFetch(nestedClient, '_options', tags) ||
161+
patchOptionsFetch(nestedClient, 'options', tags)
162+
);
163+
}
164+
165+
function patchOptionsFetch(target: MaybeWrappedClient, key: string, tags: BurnTags): boolean {
166+
const options = target[key];
167+
if (!options || typeof options !== 'object') {
168+
return false;
169+
}
170+
171+
const record = options as Record<string, unknown>;
172+
const existing = typeof record.fetch === 'function' ? (record.fetch as FetchLike) : globalThis.fetch;
173+
if (typeof existing !== 'function') {
174+
return false;
175+
}
176+
177+
record.fetch = createTaggedFetch(existing, () => tags);
178+
return true;
179+
}
180+
181+
function patchFetchProperty(target: MaybeWrappedClient, key: string, tags: BurnTags): boolean {
182+
const existing = target[key];
183+
if (typeof existing !== 'function') {
184+
return false;
185+
}
186+
187+
target[key] = createTaggedFetch(existing.bind(target) as FetchLike, () => tags) as unknown;
188+
return true;
189+
}

0 commit comments

Comments
 (0)