@lerianstudio/matcher-mcp 1.3.0 → 1.4.0-beta.1
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/README.md +26 -0
- package/dist/auth/request-token.js +50 -2
- package/dist/auth/request-token.js.map +1 -1
- package/dist/config.js +61 -0
- package/dist/config.js.map +1 -1
- package/dist/copilot/agent.js +181 -0
- package/dist/copilot/agent.js.map +1 -0
- package/dist/copilot/provider-anthropic.js +151 -0
- package/dist/copilot/provider-anthropic.js.map +1 -0
- package/dist/copilot/provider-factory.js +46 -0
- package/dist/copilot/provider-factory.js.map +1 -0
- package/dist/copilot/provider-openrouter.js +154 -0
- package/dist/copilot/provider-openrouter.js.map +1 -0
- package/dist/copilot/provider-shared.js +23 -0
- package/dist/copilot/provider-shared.js.map +1 -0
- package/dist/copilot/provider.js +15 -0
- package/dist/copilot/provider.js.map +1 -0
- package/dist/copilot/rate-limit.js +77 -0
- package/dist/copilot/rate-limit.js.map +1 -0
- package/dist/copilot/sse.js +69 -0
- package/dist/copilot/sse.js.map +1 -0
- package/dist/copilot/system-prompt.js +80 -0
- package/dist/copilot/system-prompt.js.map +1 -0
- package/dist/copilot/tenant-config.js +145 -0
- package/dist/copilot/tenant-config.js.map +1 -0
- package/dist/copilot/tools.js +241 -0
- package/dist/copilot/tools.js.map +1 -0
- package/dist/copilot/turns-handler.js +322 -0
- package/dist/copilot/turns-handler.js.map +1 -0
- package/dist/observability/otel.js +184 -2
- package/dist/observability/otel.js.map +1 -1
- package/dist/spec/openapi.yaml +1 -1
- package/dist/transport/body-limit.js +27 -0
- package/dist/transport/body-limit.js.map +1 -1
- package/dist/transport/http.js +35 -25
- package/dist/transport/http.js.map +1 -1
- 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
|
-
// -
|
|
21
|
-
//
|
|
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":"AAAA,0DAA0D;AAC1D,EAAE;AACF,8EAA8E;AAC9E,8EAA8E;AAC9E,sBAAsB;AACtB,EAAE;AACF,8EAA8E;AAC9E,+EAA+E;AAC/E,oEAAoE;AACpE,0EAA0E;AAC1E,+EAA+E;AAC/E,wEAAwE;AACxE,6EAA6E;AAC7E,gFAAgF;AAChF,oEAAoE;AACpE,+BAA+B;AAC/B,EAAE;AACF,2CAA2C;AAC3C,sEAAsE;AACtE,
|
|
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
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,EAAE;AACF,4EAA4E;AAC5E,4EAA4E;AAC5E,6EAA6E;AAC7E,yEAAyE;AACzE,4EAA4E;
|
|
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"}
|