auggy 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +96 -0
- package/LICENSE +201 -0
- package/README.md +161 -0
- package/package.json +76 -0
- package/src/agent-card.ts +39 -0
- package/src/agent.ts +283 -0
- package/src/agentmail-client.ts +138 -0
- package/src/augments/bash/index.ts +463 -0
- package/src/augments/bash/skill/SKILL.md +156 -0
- package/src/augments/budgets/budget-store.ts +513 -0
- package/src/augments/budgets/index.ts +134 -0
- package/src/augments/budgets/preamble.ts +93 -0
- package/src/augments/budgets/types.ts +89 -0
- package/src/augments/file-memory/index.ts +71 -0
- package/src/augments/filesystem/index.ts +533 -0
- package/src/augments/filesystem/skill/SKILL.md +142 -0
- package/src/augments/filesystem/skill/references/mount-permissions.md +81 -0
- package/src/augments/layered-memory/extractor/buffer.ts +56 -0
- package/src/augments/layered-memory/extractor/frequency.ts +79 -0
- package/src/augments/layered-memory/extractor/inject-handler.ts +103 -0
- package/src/augments/layered-memory/extractor/parse.ts +75 -0
- package/src/augments/layered-memory/extractor/prompt.md +26 -0
- package/src/augments/layered-memory/index.ts +757 -0
- package/src/augments/layered-memory/skill/SKILL.md +153 -0
- package/src/augments/layered-memory/storage/migrations/README.md +16 -0
- package/src/augments/layered-memory/storage/migrations/supabase-add-fact-fields.sql +9 -0
- package/src/augments/layered-memory/storage/sqlite-store.ts +352 -0
- package/src/augments/layered-memory/storage/supabase-store.ts +263 -0
- package/src/augments/layered-memory/storage/types.ts +98 -0
- package/src/augments/link/index.ts +489 -0
- package/src/augments/link/translate.ts +261 -0
- package/src/augments/notify/adapters/agentmail.ts +70 -0
- package/src/augments/notify/adapters/telegram.ts +60 -0
- package/src/augments/notify/adapters/webhook.ts +55 -0
- package/src/augments/notify/index.ts +284 -0
- package/src/augments/notify/skill/SKILL.md +150 -0
- package/src/augments/org-context/index.ts +721 -0
- package/src/augments/org-context/skill/SKILL.md +96 -0
- package/src/augments/skills/index.ts +103 -0
- package/src/augments/supabase-memory/index.ts +151 -0
- package/src/augments/telegram-transport/index.ts +312 -0
- package/src/augments/telegram-transport/polling.ts +55 -0
- package/src/augments/telegram-transport/webhook.ts +56 -0
- package/src/augments/turn-control/index.ts +61 -0
- package/src/augments/turn-control/skill/SKILL.md +155 -0
- package/src/augments/visitor-auth/email-validation.ts +66 -0
- package/src/augments/visitor-auth/index.ts +779 -0
- package/src/augments/visitor-auth/rate-limiter.ts +90 -0
- package/src/augments/visitor-auth/skill/SKILL.md +55 -0
- package/src/augments/visitor-auth/storage/sqlite-store.ts +398 -0
- package/src/augments/visitor-auth/storage/types.ts +164 -0
- package/src/augments/visitor-auth/types.ts +123 -0
- package/src/augments/visitor-auth/verify-page.ts +179 -0
- package/src/augments/web-fetch/index.ts +331 -0
- package/src/augments/web-fetch/skill/SKILL.md +100 -0
- package/src/cli/agent-index.ts +289 -0
- package/src/cli/augment-catalog.ts +320 -0
- package/src/cli/augment-resolver.ts +597 -0
- package/src/cli/commands/add-skill.ts +194 -0
- package/src/cli/commands/add.ts +87 -0
- package/src/cli/commands/chat.ts +207 -0
- package/src/cli/commands/create.ts +462 -0
- package/src/cli/commands/dev.ts +139 -0
- package/src/cli/commands/eval.ts +180 -0
- package/src/cli/commands/ls.ts +66 -0
- package/src/cli/commands/remove.ts +95 -0
- package/src/cli/commands/restart.ts +40 -0
- package/src/cli/commands/start.ts +123 -0
- package/src/cli/commands/status.ts +104 -0
- package/src/cli/commands/stop.ts +84 -0
- package/src/cli/commands/visitors-revoke.ts +155 -0
- package/src/cli/commands/visitors.ts +101 -0
- package/src/cli/config-parser.ts +1034 -0
- package/src/cli/engine-resolver.ts +68 -0
- package/src/cli/index.ts +178 -0
- package/src/cli/model-picker.ts +89 -0
- package/src/cli/pid-registry.ts +146 -0
- package/src/cli/plist-generator.ts +117 -0
- package/src/cli/resolve-config.ts +56 -0
- package/src/cli/scaffold-skills.ts +158 -0
- package/src/cli/scaffold.ts +291 -0
- package/src/cli/skill-frontmatter.ts +51 -0
- package/src/cli/skill-validator.ts +151 -0
- package/src/cli/types.ts +228 -0
- package/src/cli/yaml-helpers.ts +66 -0
- package/src/engines/_shared/cost.ts +55 -0
- package/src/engines/_shared/schema-normalize.ts +75 -0
- package/src/engines/anthropic/pricing.ts +117 -0
- package/src/engines/anthropic.ts +483 -0
- package/src/engines/openai/pricing.ts +67 -0
- package/src/engines/openai.ts +446 -0
- package/src/engines/openrouter/pricing.ts +83 -0
- package/src/engines/openrouter.ts +185 -0
- package/src/helpers.ts +24 -0
- package/src/http.ts +387 -0
- package/src/index.ts +165 -0
- package/src/kernel/capability-table.ts +172 -0
- package/src/kernel/context-allocator.ts +161 -0
- package/src/kernel/history-manager.ts +198 -0
- package/src/kernel/lifecycle-manager.ts +106 -0
- package/src/kernel/output-validator.ts +35 -0
- package/src/kernel/preamble.ts +23 -0
- package/src/kernel/route-collector.ts +97 -0
- package/src/kernel/timeout.ts +21 -0
- package/src/kernel/tool-selector.ts +47 -0
- package/src/kernel/trace-emitter.ts +66 -0
- package/src/kernel/transport-queue.ts +147 -0
- package/src/kernel/turn-loop.ts +1148 -0
- package/src/memory/context-synthesis.ts +83 -0
- package/src/memory/memory-bus.ts +61 -0
- package/src/memory/registry.ts +80 -0
- package/src/memory/tools.ts +320 -0
- package/src/memory/types.ts +8 -0
- package/src/parts.ts +30 -0
- package/src/scaffold-templates/identity.md +31 -0
- package/src/telegram-client.ts +145 -0
- package/src/tokenizer.ts +14 -0
- package/src/transports/ag-ui-events.ts +253 -0
- package/src/transports/visitor-token.ts +82 -0
- package/src/transports/web-transport.ts +948 -0
- package/src/types.ts +1009 -0
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* link augment — peer-to-peer A2A v0.2 transport.
|
|
3
|
+
*
|
|
4
|
+
* Wires the @auggy/link library into augment-1 so two Auggy agents (or any
|
|
5
|
+
* A2A v0.2-speaking peer: LangChain agent, Python A2A agent, future Mesh
|
|
6
|
+
* coordinator) can exchange messages over HTTP with mutual bearer auth and
|
|
7
|
+
* no central service. Imported as an npm dependency at @auggy/link v0.1.1.
|
|
8
|
+
*
|
|
9
|
+
* What this augment owns:
|
|
10
|
+
* - The link HTTP service: GET /health, GET /.well-known/agent.json,
|
|
11
|
+
* POST /a2a/v1, GET /a2a/v1/stream (501 stub at v0.1). Bound on its own
|
|
12
|
+
* Bun.serve port (default 8081), SEPARATE from webTransport.
|
|
13
|
+
* - BearerAuthProvider config: which peers can call this Auggy, with which
|
|
14
|
+
* bearers, and what verified Participant identity is minted on match.
|
|
15
|
+
* - SqliteTaskStore: durable persistence for inbound async tasks (capacity
|
|
16
|
+
* reserved for v0.2+ TaskCreateOutcome work — see ADR-022 sequencing).
|
|
17
|
+
* - Outbound PeerClient: enumerated via an EnvAddressBook constructed from
|
|
18
|
+
* the agent.yaml `peers` config.
|
|
19
|
+
* - Tools: `link_send` (text-only synchronous send), `link_list` (enumerate
|
|
20
|
+
* configured peers so the LLM knows who it can call).
|
|
21
|
+
*
|
|
22
|
+
* Inbound flow:
|
|
23
|
+
* 1. @auggy/link receives an HTTP request on /a2a/v1.
|
|
24
|
+
* 2. BearerAuthProvider verifies the bearer → mints a verified Participant.
|
|
25
|
+
* 3. createLinkApp invokes onMessage(ctx: HandlerContext).
|
|
26
|
+
* 4. This module translates ctx → TurnTrigger and calls kernel.handleInbound.
|
|
27
|
+
* 5. The TurnResult is translated back → HandlerOutcome.
|
|
28
|
+
* 6. v0.1 returns ONLY MessageOutcome (sync) or ErrorOutcome.
|
|
29
|
+
* TaskCreateOutcome is deferred until augment-1 grows long-running task
|
|
30
|
+
* semantics — see ADR-022 for sequencing.
|
|
31
|
+
*
|
|
32
|
+
* Why this is NOT webTransport:
|
|
33
|
+
* - webTransport speaks AG-UI (SSE event protocol shaped for browser chat).
|
|
34
|
+
* - link speaks A2A v0.2 (JSON-RPC over HTTP, peer-to-peer agent traffic).
|
|
35
|
+
* - Operators run both simultaneously; they bind different ports and own
|
|
36
|
+
* non-overlapping path prefixes.
|
|
37
|
+
*
|
|
38
|
+
* Trust model:
|
|
39
|
+
* - BearerAuthProvider only admits configured peers, all of whom carry
|
|
40
|
+
* `trust: "agent"` at v0.1. Public/anonymous traffic NEVER reaches the
|
|
41
|
+
* onMessage callback — it's rejected with 401 before we see it.
|
|
42
|
+
* - The translation layer preserves trust_level verbatim; if v0.2 admits
|
|
43
|
+
* `creator` or `public` peers, augment-1 sees them as such automatically.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import { z } from "zod";
|
|
47
|
+
import {
|
|
48
|
+
BearerAuthProvider,
|
|
49
|
+
EnvAddressBook,
|
|
50
|
+
PeerClient,
|
|
51
|
+
SqliteTaskStore,
|
|
52
|
+
buildAgentCard,
|
|
53
|
+
createLinkApp,
|
|
54
|
+
isTaskResult,
|
|
55
|
+
type AgentCard as LinkAgentCard,
|
|
56
|
+
type HandlerContext as LinkHandlerContext,
|
|
57
|
+
type LinkAppHandle,
|
|
58
|
+
type MessageHandler as LinkMessageHandler,
|
|
59
|
+
type PeerBearerConfig,
|
|
60
|
+
} from "@auggy/link";
|
|
61
|
+
|
|
62
|
+
import { defineTool } from "../../helpers";
|
|
63
|
+
import type { Augment, ToolExecuteContext, TransportKernel, TransportSpec } from "../../types";
|
|
64
|
+
import { handlerContextToTrigger, turnResultToHandlerOutcome } from "./translate";
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Options
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A configured peer. Each peer pair is symmetric: this Auggy uses `bearer` to
|
|
72
|
+
* call the peer (outbound), and accepts `inboundBearer` FROM the peer (inbound).
|
|
73
|
+
* The two bearers are independent — operators rotate them on independent
|
|
74
|
+
* schedules so an in-flight rotation never breaks both directions at once.
|
|
75
|
+
*
|
|
76
|
+
* url — peer's link endpoint (e.g. https://researcher.example.org)
|
|
77
|
+
* bearer — bearer this Auggy sends ON outbound requests TO the peer
|
|
78
|
+
* participantId — peer's UUID (the verified Participant.id our messages
|
|
79
|
+
* get tagged with on the peer's side; this Auggy must
|
|
80
|
+
* know it for AddressBook lookup symmetry)
|
|
81
|
+
* inboundBearer — bearer this Auggy ACCEPTS on inbound from the peer
|
|
82
|
+
* inboundBearerId — opaque audit id paired with inboundBearer (logged on
|
|
83
|
+
* verify; never on the wire)
|
|
84
|
+
*/
|
|
85
|
+
export interface LinkPeerConfig {
|
|
86
|
+
url: string;
|
|
87
|
+
bearer: string;
|
|
88
|
+
participantId: string;
|
|
89
|
+
inboundBearer: string;
|
|
90
|
+
inboundBearerId: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Agent-card fields the link augment serves at /.well-known/agent.json.
|
|
95
|
+
* Note this is the LINK card, not augment-1's general agent card — they
|
|
96
|
+
* exist for different consumers (A2A peers vs AG-UI browsers).
|
|
97
|
+
*/
|
|
98
|
+
export interface LinkAugmentAgentCard {
|
|
99
|
+
/** Stable UUID for this agent in the A2A network. */
|
|
100
|
+
id: string;
|
|
101
|
+
/** Display name shown in peer logs and address books. */
|
|
102
|
+
name: string;
|
|
103
|
+
/** One-line description of the agent's role. */
|
|
104
|
+
description: string;
|
|
105
|
+
/** Public URL this agent's link endpoint is reachable at. */
|
|
106
|
+
endpointUrl: string;
|
|
107
|
+
/** Capabilities the agent advertises (free-form strings at v0.1). */
|
|
108
|
+
capabilities?: string[];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Operator-facing options for the link augment. Configured in agent.yaml:
|
|
113
|
+
*
|
|
114
|
+
* augments:
|
|
115
|
+
* - name: link
|
|
116
|
+
* type: link
|
|
117
|
+
* options:
|
|
118
|
+
* port: 8081
|
|
119
|
+
* dbPath: ./link.db
|
|
120
|
+
* agentCard:
|
|
121
|
+
* id: <uuid>
|
|
122
|
+
* name: zip
|
|
123
|
+
* description: Front-door agent
|
|
124
|
+
* endpointUrl: https://zip.example.org
|
|
125
|
+
* peers:
|
|
126
|
+
* researcher:
|
|
127
|
+
* url: https://researcher.example.org
|
|
128
|
+
* bearer: ${RESEARCHER_BEARER}
|
|
129
|
+
* participantId: <uuid>
|
|
130
|
+
* inboundBearer: ${RESEARCHER_INBOUND_BEARER}
|
|
131
|
+
* inboundBearerId: <uuid>
|
|
132
|
+
*/
|
|
133
|
+
export interface LinkAugmentOptions {
|
|
134
|
+
/** Port for the Bun.serve binding the link HTTP service. Default 8081. */
|
|
135
|
+
port?: number;
|
|
136
|
+
/** Path to the SQLite file backing SqliteTaskStore. */
|
|
137
|
+
dbPath: string;
|
|
138
|
+
/** Agent-card fields. */
|
|
139
|
+
agentCard: LinkAugmentAgentCard;
|
|
140
|
+
/**
|
|
141
|
+
* Configured peers keyed by their short name (the name the LLM uses with
|
|
142
|
+
* `link_send`). Empty map = the augment runs but has no callable peers,
|
|
143
|
+
* which is legal (operator may rely on inbound-only at first).
|
|
144
|
+
*/
|
|
145
|
+
peers: Record<string, LinkPeerConfig>;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Test-only options. Production callers leave these unset; the augment
|
|
150
|
+
* defaults to real Bun.serve binding and real SqliteTaskStore.
|
|
151
|
+
*/
|
|
152
|
+
export interface LinkAugmentInternalOptions extends LinkAugmentOptions {
|
|
153
|
+
/**
|
|
154
|
+
* Skip the Bun.serve binding. Useful for unit tests that exercise the
|
|
155
|
+
* MessageHandler closure directly without claiming a port. When set, the
|
|
156
|
+
* augment never calls Bun.serve — it builds the link handle and stores it
|
|
157
|
+
* so tests can drive it via the exported `_dispatchInbound` test hook.
|
|
158
|
+
*/
|
|
159
|
+
_skipServer?: boolean;
|
|
160
|
+
/**
|
|
161
|
+
* Inject a custom PeerClient. Tests use this to replace the real outbound
|
|
162
|
+
* client with an in-process recorder.
|
|
163
|
+
*/
|
|
164
|
+
_peerClient?: PeerClient;
|
|
165
|
+
/**
|
|
166
|
+
* Inject the AddressBook env used by the default PeerClient. Tests skip
|
|
167
|
+
* the synthesized PEERS / PEER_*_BEARER env mangling by passing an env
|
|
168
|
+
* object directly.
|
|
169
|
+
*/
|
|
170
|
+
_addressBookEnv?: Record<string, string | undefined>;
|
|
171
|
+
/**
|
|
172
|
+
* Test-only side channel. When provided, the factory writes the production
|
|
173
|
+
* MessageHandler closure into `out.handler` so the test harness can drive
|
|
174
|
+
* the EXACT same code path that real link traffic exercises. Avoids
|
|
175
|
+
* duplicating the try/catch / translation pipeline in test code.
|
|
176
|
+
*/
|
|
177
|
+
_captureMessageHandler?: { handler?: LinkMessageHandler };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// Helpers
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Build a frozen env map from the peers config for EnvAddressBook.
|
|
186
|
+
*
|
|
187
|
+
* EnvAddressBook reads:
|
|
188
|
+
* - `PEERS=a,b,c` (comma-separated peer names)
|
|
189
|
+
* - `PEER_<UPPER_NAME>_URL` / `PEER_<UPPER_NAME>_BEARER` for each name
|
|
190
|
+
*
|
|
191
|
+
* Names are uppercased and underscored exactly the way EnvAddressBook
|
|
192
|
+
* expects. The resulting map is passed to `new EnvAddressBook(env)`.
|
|
193
|
+
*/
|
|
194
|
+
function buildAddressBookEnv(peers: Record<string, LinkPeerConfig>): Record<string, string> {
|
|
195
|
+
const names = Object.keys(peers);
|
|
196
|
+
const env: Record<string, string> = {
|
|
197
|
+
PEERS: names.join(","),
|
|
198
|
+
};
|
|
199
|
+
for (const [name, cfg] of Object.entries(peers)) {
|
|
200
|
+
const upper = name.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
201
|
+
env[`PEER_${upper}_URL`] = cfg.url;
|
|
202
|
+
env[`PEER_${upper}_BEARER`] = cfg.bearer;
|
|
203
|
+
}
|
|
204
|
+
return env;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Build the BearerAuthProvider peers map. The auth provider expects keys to
|
|
209
|
+
* EQUAL the verified Participant.id; values carry the active bearer + audit
|
|
210
|
+
* id. v0.1 trusts every configured peer with `trust: "agent"` (link's
|
|
211
|
+
* design — see plan §6 for the trust alphabet).
|
|
212
|
+
*/
|
|
213
|
+
function buildAuthPeers(
|
|
214
|
+
peers: Record<string, LinkPeerConfig>,
|
|
215
|
+
): Readonly<Record<string, PeerBearerConfig>> {
|
|
216
|
+
const out: Record<string, PeerBearerConfig> = {};
|
|
217
|
+
for (const [name, cfg] of Object.entries(peers)) {
|
|
218
|
+
out[cfg.participantId] = {
|
|
219
|
+
participant: {
|
|
220
|
+
id: cfg.participantId,
|
|
221
|
+
locator: cfg.url,
|
|
222
|
+
type: "agent",
|
|
223
|
+
trust: "agent",
|
|
224
|
+
},
|
|
225
|
+
active: {
|
|
226
|
+
bearer: cfg.inboundBearer,
|
|
227
|
+
bearer_id: cfg.inboundBearerId,
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
// Reference `name` in a no-op so it appears in error messages if needed.
|
|
231
|
+
// (Useful for future expansion; intentional placeholder.)
|
|
232
|
+
void name;
|
|
233
|
+
}
|
|
234
|
+
return Object.freeze(out);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Build the link agent card from the operator config.
|
|
239
|
+
*
|
|
240
|
+
* v0.1 advertises an empty `skills: []` — augment-1's skill catalogue is
|
|
241
|
+
* not yet mapped to link's SkillDescriptor shape. v0.2 will harvest skill
|
|
242
|
+
* metadata from the agent's mounted skill folders.
|
|
243
|
+
*/
|
|
244
|
+
function buildLinkAgentCard(card: LinkAugmentAgentCard): LinkAgentCard {
|
|
245
|
+
return buildAgentCard({
|
|
246
|
+
id: card.id,
|
|
247
|
+
name: card.name,
|
|
248
|
+
description: card.description,
|
|
249
|
+
endpoint_url: card.endpointUrl,
|
|
250
|
+
capabilities: card.capabilities ?? [],
|
|
251
|
+
skills: [],
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Stable threadId for a given peer. */
|
|
256
|
+
function threadIdForPeer(participantId: string): string {
|
|
257
|
+
return `link-${participantId}`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
// Factory
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
export function link(opts: LinkAugmentInternalOptions): Augment {
|
|
265
|
+
const port = opts.port ?? 8081;
|
|
266
|
+
const peers = opts.peers ?? {};
|
|
267
|
+
|
|
268
|
+
// Build the AddressBook env once at construction time. The library's
|
|
269
|
+
// EnvAddressBook takes a frozen snapshot, so this can't be hot-reloaded
|
|
270
|
+
// until @auggy/link v0.2 lands richer config plumbing.
|
|
271
|
+
const addressBookEnv = opts._addressBookEnv ?? buildAddressBookEnv(peers);
|
|
272
|
+
const addressBook = new EnvAddressBook(addressBookEnv);
|
|
273
|
+
|
|
274
|
+
const peerClient = opts._peerClient ?? new PeerClient({ addressBook });
|
|
275
|
+
|
|
276
|
+
let kernel: TransportKernel | null = null;
|
|
277
|
+
let registeredName = "link";
|
|
278
|
+
let linkHandle: LinkAppHandle | null = null;
|
|
279
|
+
let server: ReturnType<typeof Bun.serve> | null = null;
|
|
280
|
+
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
// Inbound MessageHandler — the bridge from link → augment-1 kernel
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
const onMessage: LinkMessageHandler = async function onMessage(ctx: LinkHandlerContext) {
|
|
286
|
+
if (!kernel) {
|
|
287
|
+
// Defense-in-depth: link's middleware shouldn't be wired before
|
|
288
|
+
// register fires (createLinkApp runs inside register), but if a future
|
|
289
|
+
// ordering bug pre-binds it, fail loudly rather than swallow the
|
|
290
|
+
// message.
|
|
291
|
+
return {
|
|
292
|
+
kind: "error",
|
|
293
|
+
code: -32603,
|
|
294
|
+
message: "link augment: kernel not yet registered",
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const threadId = threadIdForPeer(ctx.from.id);
|
|
299
|
+
const trigger = handlerContextToTrigger(ctx, registeredName, threadId);
|
|
300
|
+
try {
|
|
301
|
+
const result = await kernel.handleInbound(trigger);
|
|
302
|
+
return turnResultToHandlerOutcome(result);
|
|
303
|
+
} catch (err) {
|
|
304
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
305
|
+
return {
|
|
306
|
+
kind: "error",
|
|
307
|
+
code: -32603,
|
|
308
|
+
message: `link augment: turn dispatch failed: ${message}`,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
if (opts._captureMessageHandler) {
|
|
314
|
+
opts._captureMessageHandler.handler = onMessage;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
// TransportSpec.identify
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
//
|
|
321
|
+
// link auth happens inside @auggy/link's BearerAuthProvider before
|
|
322
|
+
// `onMessage` fires — by the time the augment-1 kernel sees a trigger,
|
|
323
|
+
// the peer identity has already been resolved via
|
|
324
|
+
// participantToPeerIdentity. There's no transport-level identify path
|
|
325
|
+
// for link; the TransportSpec stub returns null.
|
|
326
|
+
const identify: TransportSpec["identify"] = () => null;
|
|
327
|
+
|
|
328
|
+
const transport: TransportSpec = {
|
|
329
|
+
async register(k: TransportKernel, augmentName: string) {
|
|
330
|
+
kernel = k;
|
|
331
|
+
registeredName = augmentName;
|
|
332
|
+
|
|
333
|
+
// Construct the link handle BEFORE binding Bun.serve so any
|
|
334
|
+
// configuration error surfaces synchronously at boot.
|
|
335
|
+
const taskStore = new SqliteTaskStore({ path: opts.dbPath });
|
|
336
|
+
const auth = new BearerAuthProvider({ peers: buildAuthPeers(peers) });
|
|
337
|
+
const agentCard = buildLinkAgentCard(opts.agentCard);
|
|
338
|
+
|
|
339
|
+
linkHandle = createLinkApp({
|
|
340
|
+
agentCard,
|
|
341
|
+
auth,
|
|
342
|
+
taskStore,
|
|
343
|
+
onMessage,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
if (!opts._skipServer) {
|
|
347
|
+
server = Bun.serve({ port, fetch: linkHandle.fetch });
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
identify,
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
// Outbound tools
|
|
355
|
+
// ---------------------------------------------------------------------------
|
|
356
|
+
|
|
357
|
+
const linkSendTool = defineTool({
|
|
358
|
+
name: "link_send",
|
|
359
|
+
description:
|
|
360
|
+
"Send a text message to another agent via A2A peer-to-peer. The `to` parameter must be one of the peers configured in this agent's link config — call `link_list` to see them. Returns the peer's synchronous reply text when available, or a task id if the peer chose to handle the request asynchronously.",
|
|
361
|
+
category: "communication",
|
|
362
|
+
input: z.object({
|
|
363
|
+
to: z
|
|
364
|
+
.string()
|
|
365
|
+
.describe("Peer short name from the link config (also surfaced by `link_list`)."),
|
|
366
|
+
text: z.string().describe("Message text to send. v0.1 link traffic is text-only."),
|
|
367
|
+
}),
|
|
368
|
+
execute: async ({ to, text }, _ctx?: ToolExecuteContext) => {
|
|
369
|
+
const result = await peerClient.send({
|
|
370
|
+
to,
|
|
371
|
+
parts: [{ kind: "text", text }],
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
if (!result.ok) {
|
|
375
|
+
return JSON.stringify({
|
|
376
|
+
ok: false,
|
|
377
|
+
error: result.error.code,
|
|
378
|
+
message: result.error.message,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const { outcome } = result.value;
|
|
383
|
+
if (isTaskResult(outcome)) {
|
|
384
|
+
// Async task path — peer chose to create a Task. v0.1 doesn't yet
|
|
385
|
+
// wire task polling into augment-1's tool surface; return the id so
|
|
386
|
+
// the LLM (or operator) can follow up via future tools.
|
|
387
|
+
return JSON.stringify({
|
|
388
|
+
ok: true,
|
|
389
|
+
outcome: "task",
|
|
390
|
+
taskId: outcome.id,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Sync message path — concatenate text parts (link is text-only at
|
|
395
|
+
// v0.1, so every part is a TextPart).
|
|
396
|
+
const replyText = outcome.parts.map((p) => (p.kind === "text" ? p.text : "")).join("");
|
|
397
|
+
return JSON.stringify({
|
|
398
|
+
ok: true,
|
|
399
|
+
outcome: "message",
|
|
400
|
+
text: replyText,
|
|
401
|
+
});
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const linkListTool = defineTool({
|
|
406
|
+
name: "link_list",
|
|
407
|
+
description:
|
|
408
|
+
"List the peers configured for outbound A2A traffic. Returns an array of short names that can be used as the `to` argument to `link_send`.",
|
|
409
|
+
category: "communication",
|
|
410
|
+
input: z.object({}),
|
|
411
|
+
execute: async () => {
|
|
412
|
+
return JSON.stringify({ peers: Object.keys(peers) });
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
return {
|
|
417
|
+
name: "link",
|
|
418
|
+
capabilities: ["transport", "tools"],
|
|
419
|
+
transport,
|
|
420
|
+
tools: [linkSendTool, linkListTool],
|
|
421
|
+
async onShutdown() {
|
|
422
|
+
// Stop the server first so no new admissions enter; THEN drain
|
|
423
|
+
// in-flight requests via linkHandle.shutdown() so the store closes
|
|
424
|
+
// cleanly. Order matters: if the store closes while a request is
|
|
425
|
+
// still in flight, that request would see a store error.
|
|
426
|
+
if (server) {
|
|
427
|
+
try {
|
|
428
|
+
server.stop();
|
|
429
|
+
} catch (err) {
|
|
430
|
+
console.warn(`[link] server.stop() failed: ${(err as Error).message}`);
|
|
431
|
+
}
|
|
432
|
+
server = null;
|
|
433
|
+
}
|
|
434
|
+
if (linkHandle) {
|
|
435
|
+
try {
|
|
436
|
+
await linkHandle.shutdown();
|
|
437
|
+
} catch (err) {
|
|
438
|
+
console.warn(`[link] linkHandle.shutdown() failed: ${(err as Error).message}`);
|
|
439
|
+
}
|
|
440
|
+
linkHandle = null;
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ---------------------------------------------------------------------------
|
|
447
|
+
// Test-only hooks
|
|
448
|
+
// ---------------------------------------------------------------------------
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Test-only helper: build an augment alongside a handle to the MessageHandler
|
|
452
|
+
* closure so integration tests can fire fake A2A messages without spinning up
|
|
453
|
+
* a real Bun.serve.
|
|
454
|
+
*
|
|
455
|
+
* Returns:
|
|
456
|
+
* - augment: the assembled Augment (caller passes to defineAgent)
|
|
457
|
+
* - dispatch: pass a fake HandlerContext to invoke onMessage directly;
|
|
458
|
+
* returns the resolved HandlerOutcome
|
|
459
|
+
*
|
|
460
|
+
* This intentionally lives in the same file as the factory so the closure
|
|
461
|
+
* over `onMessage` is in scope. The factory ignores the test hooks when
|
|
462
|
+
* called from production paths.
|
|
463
|
+
*/
|
|
464
|
+
export function _createLinkForTesting(opts: LinkAugmentInternalOptions): {
|
|
465
|
+
augment: Augment;
|
|
466
|
+
dispatch: (ctx: LinkHandlerContext) => Promise<ReturnType<LinkMessageHandler>>;
|
|
467
|
+
} {
|
|
468
|
+
// Capture the production MessageHandler via the side channel so the test
|
|
469
|
+
// exercises THE SAME closure that real link traffic runs through. No
|
|
470
|
+
// duplicated try/catch or translation logic in test code.
|
|
471
|
+
const capture: { handler?: LinkMessageHandler } = {};
|
|
472
|
+
const internalOpts: LinkAugmentInternalOptions = {
|
|
473
|
+
...opts,
|
|
474
|
+
_skipServer: true,
|
|
475
|
+
_captureMessageHandler: capture,
|
|
476
|
+
};
|
|
477
|
+
const augment = link(internalOpts);
|
|
478
|
+
|
|
479
|
+
const dispatch = async (ctx: LinkHandlerContext): Promise<ReturnType<LinkMessageHandler>> => {
|
|
480
|
+
if (!capture.handler) {
|
|
481
|
+
throw new Error(
|
|
482
|
+
"_createLinkForTesting: production handler not captured — link() factory contract changed?",
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
return capture.handler(ctx);
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
return { augment, dispatch };
|
|
489
|
+
}
|