@lerianstudio/matcher-mcp 1.3.0 → 1.4.0-beta.2

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.
Files changed (45) hide show
  1. package/README.md +26 -0
  2. package/dist/auth/request-token.js +50 -2
  3. package/dist/auth/request-token.js.map +1 -1
  4. package/dist/config.js +61 -0
  5. package/dist/config.js.map +1 -1
  6. package/dist/copilot/agent.js +181 -0
  7. package/dist/copilot/agent.js.map +1 -0
  8. package/dist/copilot/provider-anthropic.js +151 -0
  9. package/dist/copilot/provider-anthropic.js.map +1 -0
  10. package/dist/copilot/provider-factory.js +46 -0
  11. package/dist/copilot/provider-factory.js.map +1 -0
  12. package/dist/copilot/provider-openrouter.js +154 -0
  13. package/dist/copilot/provider-openrouter.js.map +1 -0
  14. package/dist/copilot/provider-shared.js +23 -0
  15. package/dist/copilot/provider-shared.js.map +1 -0
  16. package/dist/copilot/provider.js +15 -0
  17. package/dist/copilot/provider.js.map +1 -0
  18. package/dist/copilot/rate-limit.js +77 -0
  19. package/dist/copilot/rate-limit.js.map +1 -0
  20. package/dist/copilot/sse.js +69 -0
  21. package/dist/copilot/sse.js.map +1 -0
  22. package/dist/copilot/system-prompt.js +113 -0
  23. package/dist/copilot/system-prompt.js.map +1 -0
  24. package/dist/copilot/tenant-config.js +145 -0
  25. package/dist/copilot/tenant-config.js.map +1 -0
  26. package/dist/copilot/tools.js +241 -0
  27. package/dist/copilot/tools.js.map +1 -0
  28. package/dist/copilot/turns-handler.js +322 -0
  29. package/dist/copilot/turns-handler.js.map +1 -0
  30. package/dist/observability/otel.js +184 -2
  31. package/dist/observability/otel.js.map +1 -1
  32. package/dist/spec/openapi.yaml +82 -1
  33. package/dist/tools/context/create.js +3 -1
  34. package/dist/tools/context/create.js.map +1 -1
  35. package/dist/tools/context/index.js +3 -1
  36. package/dist/tools/context/index.js.map +1 -1
  37. package/dist/tools/context/next-step.js +93 -0
  38. package/dist/tools/context/next-step.js.map +1 -0
  39. package/dist/tools/source/create.js +3 -1
  40. package/dist/tools/source/create.js.map +1 -1
  41. package/dist/transport/body-limit.js +27 -0
  42. package/dist/transport/body-limit.js.map +1 -1
  43. package/dist/transport/http.js +35 -25
  44. package/dist/transport/http.js.map +1 -1
  45. package/package.json +3 -1
package/README.md CHANGED
@@ -197,6 +197,32 @@ spans and metrics record **only** the tool name and call outcome (plus the
197
197
  matcher HTTP status on failure) — never tool arguments, headers, tenant
198
198
  identifiers, or the token.
199
199
 
200
+ ### Copilot (`POST /agent/turns`)
201
+
202
+ The in-app copilot the matcher console's dock calls is **optional**: the tool
203
+ relay above works with no copilot config at all. Set a provider key to enable
204
+ the streaming agent endpoint; with none set, `POST /agent/turns` returns a calm
205
+ `copilot_unconfigured` and the rest of the server runs normally. **The provider
206
+ key lives in this MCP process — not the Matcher backend, not the UI** — and is
207
+ read server-side only, never sent to the browser.
208
+
209
+ | Variable | Default | Purpose |
210
+ |-------------------------------|----------------------|---------|
211
+ | `COPILOT_PROVIDER` | `anthropic` | Which LLM provider drives the copilot. One of `anthropic` \| `openrouter`. |
212
+ | `ANTHROPIC_API_KEY` | _(unset)_ | Anthropic API key. Required when `COPILOT_PROVIDER=anthropic`. Never logged, never returned. |
213
+ | `OPENROUTER_API_KEY` | _(unset)_ | OpenRouter API key. Required when `COPILOT_PROVIDER=openrouter`. Never logged, never returned. |
214
+ | `COPILOT_MODEL` | _(provider default)_ | Model id passed to the selected provider. Anthropic falls back to its own default when unset; OpenRouter has **no** safe default and requires `COPILOT_MODEL` to be set explicitly (an Anthropic id 404s there), otherwise provider construction raises `CopilotModelRequiredError`. |
215
+ | `COPILOT_MAX_TOOL_ITERATIONS` | `8` | Max read-tool rounds the agent loop runs before forcing a stop. Integer `1..20`. |
216
+ | `COPILOT_MAX_TURN_TOKENS` | `100000` | Per-turn token ceiling; the loop truncates between rounds once crossed. |
217
+ | `COPILOT_RATE_LIMIT_PER_MIN` | `60` | Per-tenant token-bucket refill rate (turns/min). Per-tenant-overridable. |
218
+ | `COPILOT_RATE_LIMIT_BURST` | `20` | Per-tenant token-bucket burst capacity. Per-tenant-overridable. |
219
+ | `COPILOT_TENANT_CONFIG_PATH` | _(unset)_ | Path to a JSON file mapping `tenantId → { provider, model?, apiKey, maxTurnTokens?, rateLimitPerMin?, rateLimitBurst? }` for per-tenant provider/key overrides. Unset = one global config (BYOC single-tenant). |
220
+
221
+ Before spending any provider tokens, a copilot turn relays the caller's bearer
222
+ to Matcher and probes Matcher's `Authorize` (`GET /v1/contexts`); a token Matcher
223
+ rejects never reaches the paid model, so the endpoint cannot be used as an
224
+ unauthenticated LLM proxy.
225
+
200
226
  ## Tool catalog
201
227
 
202
228
  The server exposes a **curated** typed surface (one ergonomic tool per common
@@ -17,8 +17,12 @@
17
17
  //
18
18
  // Security invariants (asserted by tests):
19
19
  // - The token is NEVER logged and NEVER placed in an error message.
20
- // - No tenant identifier is read or injected anywhere tenant identity
21
- // derives solely from the relayed JWT (matcher + plugin-auth own it).
20
+ // - The tenant id MAY be decoded from the relayed JWT (getRequestTenantId),
21
+ // but ONLY unverified and KEY-ONLY: used solely to SCOPE config, limits, and
22
+ // telemetry to an organization, NEVER as an authorization input. Matcher
23
+ // still owns enforcement via the verbatim relayed token, so the tenant
24
+ // third rail stays intact (the decode is not trusted for authz). The token
25
+ // and its decoded claims are never logged.
22
26
  import { AsyncLocalStorage } from 'node:async_hooks';
23
27
  import { loadConfig } from '../config.js';
24
28
  import { createMatcherClient } from '../matcher/client.js';
@@ -98,6 +102,50 @@ export function getRequestBearerToken(extra) {
98
102
  }
99
103
  return parseBearer(authStorage.getStore()?.authorization);
100
104
  }
105
+ /**
106
+ * Decode the tenant id from the relayed JWT, request-scoped. UNVERIFIED and
107
+ * KEY-ONLY: the value SCOPES config / rate limits / telemetry to an
108
+ * organization — it is NEVER an authorization input. Matcher owns enforcement
109
+ * via the verbatim relayed token, so the tenant third rail stays intact even
110
+ * though this reads the claim.
111
+ *
112
+ * Reads the same request-scoped bearer `getRequestBearerToken` resolves, splits
113
+ * the JWT, base64url-decodes the payload segment (Node stdlib `Buffer` — NO JWT
114
+ * library), and reads the tenant claim. Matches the Go/lib-auth extraction order
115
+ * (`claimString(claims, "tenant_id", "tenantId")`): prefers `tenant_id`, then
116
+ * `tenantId`. Returns `undefined` on ANY malformed/absent token or a
117
+ * missing/non-string/empty claim, and NEVER throws — a decode failure fails open
118
+ * to the global-config path rather than breaking the turn. The token and its
119
+ * claims are never logged.
120
+ */
121
+ export function getRequestTenantId(extra) {
122
+ const token = getRequestBearerToken(extra);
123
+ if (token === undefined) {
124
+ return undefined;
125
+ }
126
+ const segments = token.split('.');
127
+ if (segments.length !== 3) {
128
+ return undefined;
129
+ }
130
+ const payloadSegment = segments[1];
131
+ if (payloadSegment === undefined || payloadSegment.length === 0) {
132
+ return undefined;
133
+ }
134
+ try {
135
+ const decoded = Buffer.from(payloadSegment, 'base64url').toString('utf8');
136
+ const claims = JSON.parse(decoded);
137
+ if (typeof claims !== 'object' || claims === null) {
138
+ return undefined;
139
+ }
140
+ const record = claims;
141
+ const tenant = record.tenant_id ?? record.tenantId;
142
+ return typeof tenant === 'string' && tenant.length > 0 ? tenant : undefined;
143
+ }
144
+ catch {
145
+ // Any decode/parse failure fails open — the caller degrades to global config.
146
+ return undefined;
147
+ }
148
+ }
101
149
  /**
102
150
  * The single chokepoint every matcher-touching tool calls. Reads the caller's
103
151
  * request-scoped token and, when present, returns a `MatcherClient` bound to it.
@@ -1 +1 @@
1
- {"version":3,"file":"request-token.js","sourceRoot":"","sources":["../../src/auth/request-token.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"request-token.js","sourceRoot":"","sources":["../../src/auth/request-token.ts"],"names":[],"mappings":"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"}
package/dist/config.js CHANGED
@@ -5,11 +5,17 @@
5
5
  // 4018; this server defaults to 4019). The matcher base URL is surfaced here
6
6
  // so the HTTP client (task 1.3.1) and the generic invoke / curated tools
7
7
  // consume a single source of truth instead of reading process.env directly.
8
+ import { loadTenantConfig } from './copilot/tenant-config.js';
8
9
  const DEFAULT_PORT = 4019;
9
10
  const DEFAULT_MATCHER_API_URL = 'http://localhost:4018';
10
11
  const DEFAULT_TIMEOUT_MS = 30_000;
11
12
  const DEFAULT_MAX_BODY_BYTES = 1_048_576; // 1 MiB
12
13
  const DEFAULT_OTEL_SERVICE_NAME = 'matcher-mcp';
14
+ const DEFAULT_COPILOT_PROVIDER = 'anthropic';
15
+ const DEFAULT_COPILOT_MAX_TOOL_ITERATIONS = 8;
16
+ const DEFAULT_COPILOT_MAX_TURN_TOKENS = 100_000;
17
+ const DEFAULT_COPILOT_RATE_LIMIT_PER_MIN = 60;
18
+ const DEFAULT_COPILOT_RATE_LIMIT_BURST = 20;
13
19
  /**
14
20
  * Parse a port from a raw env string, falling back to the default when unset.
15
21
  * An explicitly-set but invalid value is a misconfiguration and throws, rather
@@ -74,6 +80,52 @@ function parseBool(raw, fallback, key) {
74
80
  }
75
81
  throw new Error(`invalid ${key}: ${raw} (expected one of true, false, 1, 0)`);
76
82
  }
83
+ /**
84
+ * Parse the copilot provider from a raw env string, falling back to the default
85
+ * when unset. Trimmed + case-insensitive. An explicitly-set but unrecognized
86
+ * value is a misconfiguration and throws, rather than silently coercing to a
87
+ * default (fail-loud at boot), mirroring parseBool.
88
+ */
89
+ function parseCopilotProvider(raw, fallback) {
90
+ if (raw === undefined || raw === '') {
91
+ return fallback;
92
+ }
93
+ const normalized = raw.trim().toLowerCase();
94
+ if (normalized === 'anthropic' || normalized === 'openrouter') {
95
+ return normalized;
96
+ }
97
+ throw new Error(`invalid COPILOT_PROVIDER: ${raw} (expected one of anthropic, openrouter)`);
98
+ }
99
+ /**
100
+ * Parse the agent-loop iteration cap from a raw env string, falling back to the
101
+ * default when unset. Bounded to 1..20. An explicitly-set but out-of-range or
102
+ * non-integer value is a misconfiguration and throws (fail-loud at boot).
103
+ */
104
+ function parseMaxToolIterations(raw, fallback) {
105
+ if (raw === undefined || raw === '') {
106
+ return fallback;
107
+ }
108
+ const value = Number(raw);
109
+ if (!Number.isInteger(value) || value < 1 || value > 20) {
110
+ throw new Error(`invalid COPILOT_MAX_TOOL_ITERATIONS: ${raw} (expected an integer in 1..20)`);
111
+ }
112
+ return value;
113
+ }
114
+ /**
115
+ * Parse a positive integer from a raw env string, falling back to the default
116
+ * when unset. An explicitly-set non-positive or non-integer value is a
117
+ * misconfiguration and throws (fail-loud at boot), mirroring parseMaxBodyBytes.
118
+ */
119
+ function parsePositiveInt(raw, fallback, key) {
120
+ if (raw === undefined || raw === '') {
121
+ return fallback;
122
+ }
123
+ const value = Number(raw);
124
+ if (!Number.isInteger(value) || value < 1) {
125
+ throw new Error(`invalid ${key}: ${raw} (expected a positive integer)`);
126
+ }
127
+ return value;
128
+ }
77
129
  /**
78
130
  * Build the config from a given environment map (defaults to process.env).
79
131
  * Pure and side-effect-free so tests can drive it with a synthetic env.
@@ -90,6 +142,15 @@ export function loadConfig(env = process.env) {
90
142
  enableTelemetry: parseBool(env.ENABLE_TELEMETRY, false, 'ENABLE_TELEMETRY'),
91
143
  otelExporterOtlpEndpoint: otelEndpoint || undefined,
92
144
  otelServiceName,
145
+ anthropicApiKey: env.ANTHROPIC_API_KEY?.trim() || undefined,
146
+ openrouterApiKey: env.OPENROUTER_API_KEY?.trim() || undefined,
147
+ copilotProvider: parseCopilotProvider(env.COPILOT_PROVIDER, DEFAULT_COPILOT_PROVIDER),
148
+ copilotModel: env.COPILOT_MODEL?.trim() || undefined,
149
+ copilotMaxToolIterations: parseMaxToolIterations(env.COPILOT_MAX_TOOL_ITERATIONS, DEFAULT_COPILOT_MAX_TOOL_ITERATIONS),
150
+ copilotMaxTurnTokens: parsePositiveInt(env.COPILOT_MAX_TURN_TOKENS, DEFAULT_COPILOT_MAX_TURN_TOKENS, 'COPILOT_MAX_TURN_TOKENS'),
151
+ copilotRateLimitPerMin: parsePositiveInt(env.COPILOT_RATE_LIMIT_PER_MIN, DEFAULT_COPILOT_RATE_LIMIT_PER_MIN, 'COPILOT_RATE_LIMIT_PER_MIN'),
152
+ copilotRateLimitBurst: parsePositiveInt(env.COPILOT_RATE_LIMIT_BURST, DEFAULT_COPILOT_RATE_LIMIT_BURST, 'COPILOT_RATE_LIMIT_BURST'),
153
+ copilotTenantConfig: loadTenantConfig(env),
93
154
  });
94
155
  }
95
156
  //# sourceMappingURL=config.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,181 @@
1
+ // The copilot agent loop — orchestrates one provider ↔ tool-executor turn.
2
+ //
3
+ // One turn is a bounded series of provider round-trips: `streamTurn` streams
4
+ // assistant text (forwarded to the SSE sink) and may end requesting tool calls.
5
+ // The loop executes those reads through the capability layer, feeds the results
6
+ // back as the next round's `toolResults`, and repeats until the model produces a
7
+ // final answer or the iteration cap trips. It holds the within-turn tool
8
+ // round-trips in memory only — nothing persists across turns (the browser
9
+ // replays the conversation each POST).
10
+ //
11
+ // The loop is provider- and matcher-agnostic: `system`, `tools`, and the sink
12
+ // are handed in, and the executor is injectable, so it is fully unit-testable
13
+ // with a fake provider + fake executor. The bearer token never reaches this
14
+ // module — the request-scoped executor resolves it from the ALS store.
15
+ import { instrumentCopilotTool, instrumentCopilotTurn, instrumentProviderStream, recordCopilotTokens, } from '../observability/otel.js';
16
+ import { InvokeError, Invoker } from '../tools/generic/invoke.js';
17
+ import { executeReadTool, WRITE_ALLOWLIST } from './tools.js';
18
+ /**
19
+ * Sum a round's input+output tokens, failing open: `undefined` usage (a provider
20
+ * that never reports it) is 0, and a non-finite field (a misreporting stream) is
21
+ * treated as 0 rather than poisoning the running sum with NaN. Metering must
22
+ * never turn an otherwise-successful turn into a crash.
23
+ */
24
+ function usageTokens(usage) {
25
+ if (usage === undefined) {
26
+ return 0;
27
+ }
28
+ const input = Number.isFinite(usage.inputTokens) ? usage.inputTokens : 0;
29
+ const output = Number.isFinite(usage.outputTokens) ? usage.outputTokens : 0;
30
+ return input + output;
31
+ }
32
+ /**
33
+ * The write-barrier. The model asked for an allow-listed config write: NEVER
34
+ * execute it. Mint the payload through `Invoker.assemble` — the same pure path
35
+ * `matcher_invoke` uses, so the tenant guardrail (third rail) and ajv body
36
+ * validation run before anything leaves the server — then emit a `proposal` and
37
+ * end the turn with `done('proposal')`. There is no server-side dispatch on this
38
+ * path; the browser renders a confirmation card and executes the write itself.
39
+ *
40
+ * `params` carries the assembled request body (params-as-new per the v1 card
41
+ * design); the path already has its params substituted by `assemble`. If
42
+ * `assemble` rejects (bad params / missing path param / tenant guardrail / body
43
+ * validation), emit an `error` and end with `done('end')` — never a proposal for
44
+ * an unvalidated write. `InvokeError` messages are safe, operator-facing text;
45
+ * anything else collapses to a fixed message so no internal detail leaks.
46
+ *
47
+ * Returns the terminal stop reason (`proposal` on success, `end` when the write
48
+ * was rejected) so the caller can label the turn span's `copilot.stop` honestly.
49
+ */
50
+ function emitProposal(call, index, sink) {
51
+ const pathParams = call.input.pathParams;
52
+ const query = call.input.query;
53
+ const body = call.input.body;
54
+ try {
55
+ // ponytail: one Invoker per proposal (a fresh Ajv2020 compiling the body
56
+ // schema once); proposals are human-gated and rare, so negligible.
57
+ const request = new Invoker(index).assemble({
58
+ operationId: call.name,
59
+ ...(pathParams !== undefined ? { pathParams } : {}),
60
+ ...(query !== undefined ? { query } : {}),
61
+ ...(body !== undefined ? { body } : {}),
62
+ });
63
+ // Fail closed on a dropped query: `assemble` honors query params, but the
64
+ // proposal forwards only method/path/body-derived params — `request.query`
65
+ // is discarded. No write-allowlist op declares a query param today, so this
66
+ // is latent; but a future one would execute WITHOUT it (a silently wrong
67
+ // write). Refuse loudly instead of minting a lossy proposal.
68
+ if (request.query !== undefined && Object.keys(request.query).length > 0) {
69
+ sink.error('proposal_invalid', 'write proposals cannot carry query parameters');
70
+ sink.done('end');
71
+ return 'end';
72
+ }
73
+ const op = index.operations.get(call.name);
74
+ const summary = op !== undefined && op.summary.length > 0
75
+ ? op.summary
76
+ : `${request.method} ${call.name}`;
77
+ const params = request.body !== null &&
78
+ typeof request.body === 'object' &&
79
+ !Array.isArray(request.body)
80
+ ? request.body
81
+ : {};
82
+ sink.proposal({
83
+ operationId: call.name,
84
+ method: request.method,
85
+ path: request.path,
86
+ params,
87
+ summary,
88
+ });
89
+ sink.done('proposal');
90
+ return 'proposal';
91
+ }
92
+ catch (err) {
93
+ const detail = err instanceof InvokeError ? err.message : 'failed to prepare the proposal';
94
+ sink.error('proposal_invalid', detail);
95
+ sink.done('end');
96
+ return 'end';
97
+ }
98
+ }
99
+ /**
100
+ * Run one agent turn to completion, emitting the full SSE event stream via the
101
+ * sink. Returns once the turn is done (a clean `done('end')`) — including the
102
+ * iteration-cap path, which emits `error('tool_iteration_limit', …)` then
103
+ * `done('end')` so the browser always sees a terminal event.
104
+ */
105
+ export async function runAgentTurn(opts) {
106
+ const { provider, tools, index, system, messages, sink, maxIterations } = opts;
107
+ const { maxTurnTokens, model, tenant } = opts;
108
+ const providerName = opts.providerName ?? 'unknown';
109
+ const executeTool = opts.executeTool ?? executeReadTool;
110
+ await instrumentCopilotTurn({
111
+ provider: providerName,
112
+ ...(model !== undefined ? { model } : {}),
113
+ ...(tenant !== undefined ? { tenant } : {}),
114
+ maxIterations,
115
+ }, async (setStop) => {
116
+ let toolResults;
117
+ let totalTokens = 0;
118
+ for (let iteration = 0; iteration < maxIterations; iteration += 1) {
119
+ // Client hung up: stop before billing another LLM round-trip / matcher
120
+ // read. Bounds waste to at most the in-flight round (AbortSignal is Phase 4).
121
+ if (sink.isClosed()) {
122
+ setStop('aborted');
123
+ return;
124
+ }
125
+ const result = await instrumentProviderStream({ provider: providerName, ...(model !== undefined ? { model } : {}) }, () => provider.streamTurn({
126
+ system,
127
+ messages,
128
+ tools,
129
+ ...(toolResults !== undefined ? { toolResults } : {}),
130
+ }, { onTextDelta: (delta) => sink.text(delta) }));
131
+ // Meter this round (fail-open: a provider that omits usage counts as 0,
132
+ // so a turn that would otherwise succeed never crashes on missing usage).
133
+ recordCopilotTokens(providerName, result.usage);
134
+ totalTokens += usageTokens(result.usage);
135
+ if (result.stop === 'end') {
136
+ // A completed answer is NEVER truncated, whatever it cost — the ceiling
137
+ // only bounds a model that wants to keep going.
138
+ setStop('end');
139
+ sink.done('end');
140
+ return;
141
+ }
142
+ // Runaway guard: the model wants more rounds but the turn's token budget
143
+ // is spent. Truncate BETWEEN rounds — this round's tools do not run and
144
+ // no new model round-trip is billed. A real truncation (NOT calm): it
145
+ // renders as an error, mirroring the iteration-cap path.
146
+ if (maxTurnTokens !== undefined && totalTokens >= maxTurnTokens) {
147
+ setStop('token_limit');
148
+ sink.error('turn_token_limit', `reached the per-turn token limit (${maxTurnTokens})`);
149
+ sink.done('end');
150
+ return;
151
+ }
152
+ // ponytail: sequential tool exec; parallelize only if a turn routinely
153
+ // fans out many reads. Matcher reads are cheap, so ordering keeps the SSE
154
+ // chip stream legible for little cost.
155
+ const results = [];
156
+ for (const call of result.calls) {
157
+ // Write-barrier: an allow-listed config write is PROPOSED, never
158
+ // executed. The first write ends the turn (one proposal per turn);
159
+ // reads keep going.
160
+ if (WRITE_ALLOWLIST.has(call.name)) {
161
+ setStop(emitProposal(call, index, sink));
162
+ return;
163
+ }
164
+ sink.tool(call.name, 'running');
165
+ // Active tool span (via instrumentCopilotTool) fixes matcher trace
166
+ // correlation and records the outcome from the swallowed isError.
167
+ const r = await instrumentCopilotTool(call.name, () => executeTool(call, index));
168
+ sink.tool(call.name, r.isError === true ? 'error' : 'ok');
169
+ results.push(r);
170
+ }
171
+ // Accumulate across rounds so a later round grounds on every prior round's
172
+ // results — replacing would drop earlier reads a dependent answer needs.
173
+ toolResults = [...(toolResults ?? []), ...results];
174
+ }
175
+ // Ran `maxIterations` tool rounds and the model still wants more: hard stop.
176
+ setStop('iteration_limit');
177
+ sink.error('tool_iteration_limit', `reached the tool-call limit (${maxIterations})`);
178
+ sink.done('end');
179
+ });
180
+ }
181
+ //# sourceMappingURL=agent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent.js","sourceRoot":"","sources":["../../src/copilot/agent.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,151 @@
1
+ // Anthropic-native LLMProvider.
2
+ //
3
+ // Translates the normalized LLMTurnInput into the Anthropic Messages API's
4
+ // streaming tool-use format and back into the LLMProvider seam. Stateless per
5
+ // call — nothing persists across turns.
6
+ import Anthropic from '@anthropic-ai/sdk';
7
+ import { CopilotUnconfiguredError } from './provider-factory.js';
8
+ import { parseToolInput } from './provider-shared.js';
9
+ // Max tokens the Messages API requires per request. A fixed ceiling is fine for
10
+ // grounded read-only answers.
11
+ // ponytail: fixed cap; make configurable if turns start truncating.
12
+ const ANTHROPIC_MAX_TOKENS = 4096;
13
+ // Anthropic-specific default model, used when COPILOT_MODEL is unset. Kept here
14
+ // (not in config) so the zero-config anthropic path resolves its own default
15
+ // while the openrouter provider — which has no safe default — fails loud instead.
16
+ const ANTHROPIC_DEFAULT_MODEL = 'claude-opus-4-8';
17
+ // The seam's LLMToolResult carries only { id, content } — not the name/input of
18
+ // the tool_use it answers. Anthropic requires each tool_result to follow a
19
+ // tool_use with a matching id, so we synthesize one with a placeholder name and
20
+ // empty input. The model answers from the tool_result content, which is intact.
21
+ // ponytail: placeholder name/input is a known ceiling — enrich LLMToolResult
22
+ // with the originating name/input if the model needs to see its prior args.
23
+ const SYNTHETIC_TOOL_USE_NAME = 'tool';
24
+ /**
25
+ * Build a provider backed by Anthropic. When `streamFn` is omitted the key is
26
+ * required (its absence throws CopilotUnconfiguredError here, at construction);
27
+ * tests inject a `streamFn` and skip the live client entirely.
28
+ */
29
+ export function createAnthropicProvider(config, streamFn) {
30
+ const openStream = streamFn ?? defaultAnthropicStream(config);
31
+ return {
32
+ async streamTurn(input, handlers) {
33
+ const tools = input.tools.map(toAnthropicTool);
34
+ const params = {
35
+ model: config.copilotModel ?? ANTHROPIC_DEFAULT_MODEL,
36
+ max_tokens: ANTHROPIC_MAX_TOKENS,
37
+ system: input.system,
38
+ messages: buildAnthropicMessages(input),
39
+ ...(tools.length > 0 ? { tools } : {}),
40
+ };
41
+ // Accumulate streamed tool_use blocks by their content-block index; their
42
+ // input arrives as fragmented input_json_delta pieces we parse at the end.
43
+ const toolBlocks = new Map();
44
+ // Token usage: input arrives once on message_start, output accrues on
45
+ // message_delta (cumulative). Left undefined when the stream reports
46
+ // neither, so the loop meters this round as 0 (fail-open).
47
+ let inputTokens;
48
+ let outputTokens;
49
+ for await (const event of openStream(params)) {
50
+ if (event.type === 'message_start') {
51
+ inputTokens = event.message.usage.input_tokens;
52
+ outputTokens = event.message.usage.output_tokens;
53
+ }
54
+ else if (event.type === 'message_delta') {
55
+ outputTokens = event.usage.output_tokens;
56
+ }
57
+ else if (event.type === 'content_block_start' &&
58
+ event.content_block.type === 'tool_use') {
59
+ toolBlocks.set(event.index, {
60
+ id: event.content_block.id,
61
+ name: event.content_block.name,
62
+ json: '',
63
+ });
64
+ }
65
+ else if (event.type === 'content_block_delta') {
66
+ if (event.delta.type === 'text_delta') {
67
+ handlers.onTextDelta(event.delta.text);
68
+ }
69
+ else if (event.delta.type === 'input_json_delta') {
70
+ const block = toolBlocks.get(event.index);
71
+ if (block !== undefined) {
72
+ block.json += event.delta.partial_json;
73
+ }
74
+ }
75
+ }
76
+ }
77
+ const usage = usageFrom(inputTokens, outputTokens);
78
+ if (toolBlocks.size === 0) {
79
+ // ponytail: turn-end is inferred from the absence of tool_use blocks —
80
+ // we do NOT inspect stop_reason, so a max_tokens-truncated text answer
81
+ // renders as a clean end. Acceptable: 4096 tokens is ample for grounded
82
+ // config answers, and a truncated WRITE fails safe (partial tool JSON →
83
+ // parseToolInput → {} → assemble rejects → proposal_invalid). Upgrade
84
+ // path: capture stop_reason into the done label if operators hit it.
85
+ return { stop: 'end', ...(usage !== undefined ? { usage } : {}) };
86
+ }
87
+ const calls = [...toolBlocks.values()].map((block) => ({
88
+ id: block.id,
89
+ name: block.name,
90
+ input: parseToolInput(block.json),
91
+ }));
92
+ return { stop: 'tool_use', calls, ...(usage !== undefined ? { usage } : {}) };
93
+ },
94
+ };
95
+ }
96
+ /** Bind a live Anthropic client, failing closed on a missing key at construction. */
97
+ function defaultAnthropicStream(config) {
98
+ const apiKey = config.anthropicApiKey;
99
+ if (apiKey === undefined) {
100
+ throw new CopilotUnconfiguredError('anthropic');
101
+ }
102
+ const client = new Anthropic({ apiKey });
103
+ return (params) => client.messages.stream(params);
104
+ }
105
+ /**
106
+ * Build an LLMUsage from the tracked token counts, or `undefined` when the
107
+ * stream reported neither (so the loop meters this round as 0 — fail-open). A
108
+ * missing half defaults to 0 rather than dropping the whole usage.
109
+ */
110
+ function usageFrom(inputTokens, outputTokens) {
111
+ if (inputTokens === undefined && outputTokens === undefined) {
112
+ return undefined;
113
+ }
114
+ return { inputTokens: inputTokens ?? 0, outputTokens: outputTokens ?? 0 };
115
+ }
116
+ function toAnthropicTool(def) {
117
+ return {
118
+ name: def.name,
119
+ description: def.description,
120
+ input_schema: def.inputSchema,
121
+ };
122
+ }
123
+ /**
124
+ * Map the turn's conversation to Anthropic messages. When prior tool results are
125
+ * present, append the synthetic assistant tool_use turn they answer followed by
126
+ * the tool_result user turn (Anthropic requires that pairing).
127
+ */
128
+ function buildAnthropicMessages(input) {
129
+ const messages = input.messages.map((message) => ({
130
+ role: message.role,
131
+ content: message.content,
132
+ }));
133
+ if (input.toolResults !== undefined && input.toolResults.length > 0) {
134
+ const toolUseBlocks = input.toolResults.map((result) => ({
135
+ type: 'tool_use',
136
+ id: result.id,
137
+ name: SYNTHETIC_TOOL_USE_NAME,
138
+ input: {},
139
+ }));
140
+ const toolResultBlocks = input.toolResults.map((result) => ({
141
+ type: 'tool_result',
142
+ tool_use_id: result.id,
143
+ content: result.content,
144
+ ...(result.isError === true ? { is_error: true } : {}),
145
+ }));
146
+ messages.push({ role: 'assistant', content: toolUseBlocks });
147
+ messages.push({ role: 'user', content: toolResultBlocks });
148
+ }
149
+ return messages;
150
+ }
151
+ //# sourceMappingURL=provider-anthropic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-anthropic.js","sourceRoot":"","sources":["../../src/copilot/provider-anthropic.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ // Provider factory — resolves the config-selected LLMProvider.
2
+ //
3
+ // `getProvider(config)` switches on `config.copilotProvider` and returns a
4
+ // working provider backed by Anthropic (native) or OpenRouter (OpenAI-compatible).
5
+ // The API key is the point-of-use secret: it is validated here (via the
6
+ // per-provider constructor), never at boot, so a matcher deploy without the
7
+ // copilot still starts — only hitting `/agent/turns` surfaces a missing key.
8
+ import { createAnthropicProvider } from './provider-anthropic.js';
9
+ import { createOpenRouterProvider } from './provider-openrouter.js';
10
+ /**
11
+ * Raised when the selected copilot provider has no API key. Thrown at provider
12
+ * construction (endpoint-hit time), not at `loadConfig` — the handler maps it to
13
+ * a single SSE `copilot_unconfigured` error. The message never includes the key.
14
+ */
15
+ export class CopilotUnconfiguredError extends Error {
16
+ provider;
17
+ constructor(provider) {
18
+ super(`copilot provider "${provider}" is not configured: missing API key`);
19
+ this.provider = provider;
20
+ this.name = 'CopilotUnconfiguredError';
21
+ }
22
+ }
23
+ /**
24
+ * Raised when the selected provider needs an explicit COPILOT_MODEL and none was
25
+ * set — the openrouter provider has no safe default (an Anthropic id 404s there).
26
+ * Subclasses CopilotUnconfiguredError so the turns handler's existing catch maps
27
+ * it to the same `copilot_unconfigured` SSE error (a named config fault, not an
28
+ * opaque `internal`). Thrown at provider construction. Message bears no secret.
29
+ */
30
+ export class CopilotModelRequiredError extends CopilotUnconfiguredError {
31
+ constructor(provider) {
32
+ super(provider);
33
+ this.message = `copilot provider "${provider}" requires COPILOT_MODEL to be set (no default model for this provider)`;
34
+ this.name = 'CopilotModelRequiredError';
35
+ }
36
+ }
37
+ /** Resolve the LLMProvider the config selects. Throws CopilotUnconfiguredError on a missing key. */
38
+ export function getProvider(config) {
39
+ switch (config.copilotProvider) {
40
+ case 'anthropic':
41
+ return createAnthropicProvider(config);
42
+ case 'openrouter':
43
+ return createOpenRouterProvider(config);
44
+ }
45
+ }
46
+ //# sourceMappingURL=provider-factory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-factory.js","sourceRoot":"","sources":["../../src/copilot/provider-factory.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,EAAE;AACF,2EAA2E;AAC3E,mFAAmF;AACnF,wEAAwE;AACxE,4EAA4E;AAC5E,6EAA6E;AAG7E,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAA;AACjE,OAAO,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAA;AAGnE;;;;GAIG;AACH,MAAM,OAAO,wBAAyB,SAAQ,KAAK;IAC5B;IAArB,YAAqB,QAAoC;QACvD,KAAK,CAAC,qBAAqB,QAAQ,sCAAsC,CAAC,CAAA;QADvD,aAAQ,GAAR,QAAQ,CAA4B;QAEvD,IAAI,CAAC,IAAI,GAAG,0BAA0B,CAAA;IACxC,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,OAAO,yBAA0B,SAAQ,wBAAwB;IACrE,YAAY,QAAoC;QAC9C,KAAK,CAAC,QAAQ,CAAC,CAAA;QACf,IAAI,CAAC,OAAO,GAAG,qBAAqB,QAAQ,yEAAyE,CAAA;QACrH,IAAI,CAAC,IAAI,GAAG,2BAA2B,CAAA;IACzC,CAAC;CACF;AAED,oGAAoG;AACpG,MAAM,UAAU,WAAW,CAAC,MAAc;IACxC,QAAQ,MAAM,CAAC,eAAe,EAAE,CAAC;QAC/B,KAAK,WAAW;YACd,OAAO,uBAAuB,CAAC,MAAM,CAAC,CAAA;QACxC,KAAK,YAAY;YACf,OAAO,wBAAwB,CAAC,MAAM,CAAC,CAAA;IAC3C,CAAC;AACH,CAAC"}