@covia/covia-sdk 1.17.0 → 1.19.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/dist/index.js CHANGED
@@ -37,6 +37,13 @@ var CoviaError = class extends Error {
37
37
  this.message = message;
38
38
  }
39
39
  };
40
+ var MCPError = class extends CoviaError {
41
+ constructor(message, code, data) {
42
+ super(message, code);
43
+ this.name = "MCPError";
44
+ this.data = data;
45
+ }
46
+ };
40
47
  var GridError = class extends CoviaError {
41
48
  constructor(statusCode, message, responseBody = null) {
42
49
  super(`HTTP ${statusCode}: ${message}`, statusCode);
@@ -1598,17 +1605,17 @@ async function* parseSSEStream(response, options) {
1598
1605
  // src/asset-cache.ts
1599
1606
  var PREFIX = "covia:asset-meta:";
1600
1607
  var LocalStorageMetadataStore = class {
1601
- get(hash) {
1608
+ get(key) {
1602
1609
  try {
1603
- const raw = localStorage.getItem(PREFIX + hash);
1610
+ const raw = localStorage.getItem(PREFIX + key);
1604
1611
  return raw ? JSON.parse(raw) : void 0;
1605
1612
  } catch {
1606
1613
  return void 0;
1607
1614
  }
1608
1615
  }
1609
- put(hash, metadata) {
1616
+ put(key, metadata) {
1610
1617
  try {
1611
- localStorage.setItem(PREFIX + hash, JSON.stringify(metadata));
1618
+ localStorage.setItem(PREFIX + key, JSON.stringify(metadata));
1612
1619
  } catch {
1613
1620
  }
1614
1621
  }
@@ -1641,6 +1648,15 @@ function getAssetMetadataStore() {
1641
1648
  function normaliseHash(hash) {
1642
1649
  return (hash.startsWith("0x") ? hash.slice(2) : hash).toLowerCase();
1643
1650
  }
1651
+ function persistentCacheKey(ref) {
1652
+ const parsed = parseDidUrl(ref);
1653
+ if (!parsed.did) return null;
1654
+ if (parsed.namespace !== Namespace.ASSET) return null;
1655
+ const hash = assetHash(ref);
1656
+ if (!hash) return null;
1657
+ const bare = normaliseHash(hash);
1658
+ return /^[0-9a-f]+$/.test(bare) ? `${parsed.did}/${Namespace.ASSET}/${bare}` : null;
1659
+ }
1644
1660
 
1645
1661
  // src/AdapterManager.ts
1646
1662
  var ADAPTERS_PATH = "v/info/adapters";
@@ -1743,16 +1759,46 @@ async function sliceAll(workspace, path, pageSize = SLICE_PAGE_SIZE) {
1743
1759
  }
1744
1760
  return items;
1745
1761
  }
1762
+ function descWindow(total, offset, limit) {
1763
+ const end = Math.max(0, total - offset);
1764
+ return [Math.max(0, end - limit), end];
1765
+ }
1746
1766
 
1747
1767
  // src/AgentManager.ts
1768
+ var SESSION_ROLES = ["system", "user", "assistant", "tool"];
1769
+ var MAX_COMPACTION_DEPTH = 32;
1770
+ function collectTurns(entries, out, depth = 0) {
1771
+ if (depth > MAX_COMPACTION_DEPTH) return;
1772
+ for (const entry of entries) {
1773
+ const e = record(entry);
1774
+ if (!e) continue;
1775
+ if (Array.isArray(e.items) && typeof e.summary === "string") {
1776
+ collectTurns(e.items, out, depth + 1);
1777
+ continue;
1778
+ }
1779
+ if (typeof e.role === "string" && SESSION_ROLES.includes(e.role)) {
1780
+ out.push({ ...e, role: e.role });
1781
+ }
1782
+ }
1783
+ }
1784
+ function sessionConversation(frames) {
1785
+ const turns = [];
1786
+ for (const frame of frames) {
1787
+ const entries = record(frame)?.conversation;
1788
+ if (Array.isArray(entries)) collectTurns(entries, turns);
1789
+ }
1790
+ return turns;
1791
+ }
1748
1792
  function sessionRecord(sessionId, value) {
1749
1793
  const session = record(value) ?? {};
1750
1794
  const meta = record(session.meta);
1795
+ const frames = Array.isArray(session.frames) ? session.frames : [];
1751
1796
  return {
1752
1797
  id: sessionId,
1753
1798
  metadata: meta ?? {},
1754
1799
  pending: Array.isArray(session.pending) ? session.pending : [],
1755
- frames: Array.isArray(session.frames) ? session.frames : [],
1800
+ frames,
1801
+ conversation: sessionConversation(frames),
1756
1802
  wakeTime: typeof session.wakeTime === "number" ? session.wakeTime : void 0
1757
1803
  };
1758
1804
  }
@@ -1905,11 +1951,31 @@ var AgentManager = class {
1905
1951
  * available on each record's `value` while common runtime fields are typed.
1906
1952
  */
1907
1953
  async listSessions(agentId, options = {}) {
1908
- const result = await this.venue.workspace.slice(
1909
- `g/${agentId}/sessions`,
1910
- options.offset,
1911
- options.limit
1912
- );
1954
+ const path = `g/${agentId}/sessions`;
1955
+ if ((options.order ?? "asc") === "asc") {
1956
+ return this._sessionPage(path, await this.venue.workspace.slice(
1957
+ path,
1958
+ options.offset,
1959
+ options.limit
1960
+ ), options.offset ?? 0, options.limit);
1961
+ }
1962
+ const offset = Math.max(0, options.offset ?? 0);
1963
+ const limit = options.limit;
1964
+ const guess = (await this.venue.workspace.count(path)).count ?? 0;
1965
+ if (limit === 0) return { items: [], total: guess, offset, limit: 0 };
1966
+ const read = async (total2) => {
1967
+ const [start, end] = descWindow(total2, offset, limit ?? total2);
1968
+ return end > start ? this.venue.workspace.slice(path, start, end - start) : { exists: true, values: [], count: total2, offset: start };
1969
+ };
1970
+ let result = await read(guess);
1971
+ const total = result.count ?? guess;
1972
+ if (total !== guess) result = await read(total);
1973
+ const page = this._sessionPage(path, result, offset, limit);
1974
+ page.items.reverse();
1975
+ return page;
1976
+ }
1977
+ /** Shape a `{key, value}` session slice into a page. */
1978
+ _sessionPage(_path, result, offset, limit) {
1913
1979
  const sessions = (result.values ?? []).flatMap((entry) => {
1914
1980
  const pair = record(entry);
1915
1981
  if (!pair || typeof pair.key !== "string") return [];
@@ -1918,8 +1984,8 @@ var AgentManager = class {
1918
1984
  return {
1919
1985
  items: sessions,
1920
1986
  total: result.count ?? sessions.length,
1921
- offset: result.offset ?? options.offset ?? 0,
1922
- limit: options.limit ?? sessions.length
1987
+ offset,
1988
+ limit: limit ?? sessions.length
1923
1989
  };
1924
1990
  }
1925
1991
  /** Read one agent session directly from the workspace Values surface. */
@@ -2111,6 +2177,24 @@ var Job = class {
2111
2177
  };
2112
2178
 
2113
2179
  // src/JobManager.ts
2180
+ var JOB_INDEX = "j";
2181
+ var DEFAULT_HISTORY_LIMIT = 50;
2182
+ var MAX_SLICE_LIMIT = 100;
2183
+ function isSliceCapError(error) {
2184
+ return error instanceof Error && error.message.includes("exceeds maxSize");
2185
+ }
2186
+ function jobRecords(values) {
2187
+ const records = [];
2188
+ for (const entry of values) {
2189
+ const pair = record(entry);
2190
+ const value = record(pair?.value);
2191
+ if (!value) continue;
2192
+ const meta = value;
2193
+ const key = typeof pair?.key === "string" ? pair.key : "";
2194
+ records.push({ ...meta, id: meta.id ?? `0x${key}` });
2195
+ }
2196
+ return records;
2197
+ }
2114
2198
  function jobPage(value) {
2115
2199
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
2116
2200
  throw new CoviaError("Venue returned an invalid jobs listing");
@@ -2152,6 +2236,70 @@ var JobManager = class {
2152
2236
  }
2153
2237
  return items;
2154
2238
  }
2239
+ /**
2240
+ * A page of job history: `JobMetadata` rows, newest-first by default, with an
2241
+ * authoritative total.
2242
+ *
2243
+ * `list()` answers "which jobs exist" as ids; a history view needs a bounded
2244
+ * window of *records* and a count to page against. This reads the job index
2245
+ * directly, so each row arrives with its metadata already attached — the
2246
+ * alternative, listing ids and then fetching each one, is an N+1 fan-out per
2247
+ * page. Reads stay job-free (covia#177): paging job history must not itself
2248
+ * write a job record per page.
2249
+ *
2250
+ * The total is read before a `desc` window can be placed, so the index may
2251
+ * grow in between; the page self-corrects once against the count the slice
2252
+ * itself reports, which is authoritative for the rows returned with it.
2253
+ */
2254
+ async history(options = {}) {
2255
+ const offset = Math.max(0, Math.trunc(options.offset ?? 0));
2256
+ const limit = Math.max(0, Math.trunc(options.limit ?? DEFAULT_HISTORY_LIMIT));
2257
+ const descending = (options.order ?? "desc") === "desc";
2258
+ if (limit === 0) {
2259
+ const { count } = await this.venue.workspace.count(JOB_INDEX);
2260
+ return { items: [], total: count ?? 0, offset, limit };
2261
+ }
2262
+ if (!descending) {
2263
+ const asc = await this._readJobRange(offset, offset + limit);
2264
+ return { items: jobRecords(asc.values), total: asc.count ?? offset + asc.values.length, offset, limit };
2265
+ }
2266
+ const guess = (await this.venue.workspace.count(JOB_INDEX)).count ?? 0;
2267
+ let read = await this._readJobRange(...descWindow(guess, offset, limit));
2268
+ const total = read.count ?? guess;
2269
+ if (total !== guess) {
2270
+ read = await this._readJobRange(...descWindow(total, offset, limit));
2271
+ }
2272
+ return { items: jobRecords(read.values).reverse(), total: read.count ?? total, offset, limit };
2273
+ }
2274
+ /**
2275
+ * Read index ranks `[start, end)` in cap-aware chunks, halving on an oversize
2276
+ * response and skipping a record that is individually oversize.
2277
+ */
2278
+ async _readJobRange(start, end) {
2279
+ const values = [];
2280
+ let count;
2281
+ let pos = start;
2282
+ let chunk = MAX_SLICE_LIMIT;
2283
+ while (pos < end) {
2284
+ const want = Math.min(chunk, end - pos);
2285
+ try {
2286
+ const page = await this.venue.workspace.slice(JOB_INDEX, pos, want);
2287
+ count = page.count ?? count;
2288
+ const got = page.values ?? [];
2289
+ values.push(...got);
2290
+ if (got.length < want) break;
2291
+ pos += got.length;
2292
+ } catch (error) {
2293
+ if (!isSliceCapError(error)) throw error;
2294
+ if (want > 1) {
2295
+ chunk = Math.max(1, Math.floor(want / 2));
2296
+ continue;
2297
+ }
2298
+ pos += 1;
2299
+ }
2300
+ }
2301
+ return { count, values };
2302
+ }
2155
2303
  async get(jobId) {
2156
2304
  try {
2157
2305
  const data = await venueJson(this.venue, `/api/v1/jobs/${jobId}`);
@@ -2326,6 +2474,14 @@ var AssetManager = class {
2326
2474
  * a mutable lattice path the venue resolves (`w/my-assets/foo`, `o/my-op`,
2327
2475
  * `<DID>/w/...`). Only content-addressed (immutable) refs are cached;
2328
2476
  * mutable paths are always re-fetched, so a changed value is never served stale.
2477
+ *
2478
+ * **Content-addressed refs are caller-relative, not global** (covia#502). A
2479
+ * bare hash or `a/<hash>` names the asset in *your own* `a/` namespace, so it
2480
+ * 404s for a hash someone else registered, and for an anonymous request.
2481
+ * Address another owner's asset explicitly as `<ownerDID>/a/<hash>`, and the
2482
+ * venue's own catalog as `<venueDID>/a/<hash>`. Only the DID-qualified form
2483
+ * is portable, so only it is eligible for the cross-session metadata store;
2484
+ * caller-relative refs are cached in memory on this manager alone.
2329
2485
  * @param assetId - Asset identifier or lattice address
2330
2486
  * @returns Returns either an Operation or DataAsset based on the asset's metadata
2331
2487
  */
@@ -2334,8 +2490,9 @@ var AssetManager = class {
2334
2490
  return this._wrap(assetId, this.cache.get(assetId));
2335
2491
  }
2336
2492
  const hash = assetHash(assetId);
2337
- if (hash) {
2338
- const persisted = getAssetMetadataStore()?.get(normaliseHash(hash));
2493
+ const storeKey = persistentCacheKey(assetId);
2494
+ if (storeKey) {
2495
+ const persisted = getAssetMetadataStore()?.get(storeKey);
2339
2496
  if (persisted) {
2340
2497
  this.cache.set(assetId, persisted);
2341
2498
  return this._wrap(assetId, persisted);
@@ -2349,7 +2506,7 @@ var AssetManager = class {
2349
2506
  );
2350
2507
  if (hash) {
2351
2508
  this.cache.set(assetId, data);
2352
- getAssetMetadataStore()?.put(normaliseHash(hash), data);
2509
+ if (storeKey) getAssetMetadataStore()?.put(storeKey, data);
2353
2510
  }
2354
2511
  return this._wrap(assetId, data);
2355
2512
  } catch (error) {
@@ -2583,12 +2740,58 @@ var OperationManager = class {
2583
2740
  };
2584
2741
 
2585
2742
  // src/WorkspaceManager.ts
2743
+ var SCOPED_NAMESPACES = ["t", "n", "c"];
2744
+ var SCOPE_UNEXPANDED = /Cannot use '[tnc]\/' prefix outside/;
2745
+ var MAX_PROJECT_FIELDS = 16;
2746
+ function scopedNamespaceOf(path) {
2747
+ const head = path.split("/", 1)[0];
2748
+ return SCOPED_NAMESPACES.includes(head) ? head : void 0;
2749
+ }
2750
+ function scopeParamsFor(ns, scope) {
2751
+ const { agent, task, session } = scope;
2752
+ switch (ns) {
2753
+ case "n":
2754
+ return { agent };
2755
+ case "c":
2756
+ if (!session) throw new CoviaError("A 'c/' scratch read needs a session in its scope");
2757
+ return { agent, session };
2758
+ case "t":
2759
+ if (!task) throw new CoviaError("A 't/' scratch read needs a task in its scope");
2760
+ return { agent, task };
2761
+ }
2762
+ }
2763
+ function expandScopedPath(path, ns, scope) {
2764
+ if (scope.agent.startsWith("did:")) {
2765
+ throw new CoviaError(
2766
+ "This venue cannot expand scoped scratch paths and the SDK cannot do it for a DID-qualified agent \u2014 pass a bare agent id, or upgrade the venue (covia#230)"
2767
+ );
2768
+ }
2769
+ const suffix = path.slice(ns.length);
2770
+ switch (ns) {
2771
+ case "n":
2772
+ return `g/${scope.agent}/n${suffix}`;
2773
+ case "c":
2774
+ return `g/${scope.agent}/sessions/${scope.session}/c${suffix}`;
2775
+ case "t":
2776
+ return `j/${scope.task}/temp${suffix}`;
2777
+ }
2778
+ }
2586
2779
  var WorkspaceManager = class {
2587
2780
  constructor(venue) {
2588
2781
  this.venue = venue;
2589
2782
  // Whether this venue serves GET /api/v1/values/* — flipped on the first 404
2590
2783
  // so pre-0.3 venues pay the probe once, not one failed GET per read.
2591
2784
  this.valuesSupported = true;
2785
+ // Whether this venue expands t/, n/ and c/ from explicit agent/task/session
2786
+ // selectors (covia#230) — flipped off the first time a venue answers a
2787
+ // selector-bearing read by complaining the shorthand has no execution scope,
2788
+ // after which scoped reads expand client-side instead.
2789
+ this.scopeExpansionSupported = true;
2790
+ // Whether this venue projects fields on `list` (covia#191). A venue without
2791
+ // it ignores the unknown `fields` param and answers a plain list, so the
2792
+ // probe is "asked for fields, got no values" — never a version check, which
2793
+ // covia-sdk#36 showed an embedded venue can misreport.
2794
+ this.fieldsSupported = true;
2592
2795
  }
2593
2796
  /** GET a job-free `/api/v1/values/{op}` read; omit undefined params. */
2594
2797
  _values(op, params) {
@@ -2622,6 +2825,52 @@ var WorkspaceManager = class {
2622
2825
  }
2623
2826
  return this.unsupported(`workspace ${op} reads`);
2624
2827
  }
2828
+ /**
2829
+ * A job-free Values read of an execution-scoped path, preferring the venue's
2830
+ * own selector expansion and falling back to the client-side one.
2831
+ *
2832
+ * The venue is preferred because it expands *before* the capability check, so
2833
+ * authorisation applies to the same canonical resource that is ultimately
2834
+ * read — a caller cannot substitute a sibling or foreign agent's scratch.
2835
+ * The fallback reaches the same resource but re-derives the layout here.
2836
+ */
2837
+ async scopedRead(op, params, scope) {
2838
+ const path = String(params.path ?? "");
2839
+ const ns = scopedNamespaceOf(path);
2840
+ if (!ns) return this.valuesRead(op, params);
2841
+ if (this.scopeExpansionSupported) {
2842
+ try {
2843
+ return await this.valuesRead(op, { ...params, ...scopeParamsFor(ns, scope) });
2844
+ } catch (e) {
2845
+ if (!(e instanceof Error) || !SCOPE_UNEXPANDED.test(e.message)) throw e;
2846
+ this.scopeExpansionSupported = false;
2847
+ }
2848
+ }
2849
+ scopeParamsFor(ns, scope);
2850
+ return this.valuesRead(op, { ...params, path: expandScopedPath(path, ns, scope) });
2851
+ }
2852
+ /**
2853
+ * Bind an execution scope, giving job-free reads of the `t/`, `n/` and `c/`
2854
+ * scratch shorthands that a bare GET cannot otherwise resolve (covia#177
2855
+ * forbids minting a Job for a read; the shorthands need context a GET lacks).
2856
+ *
2857
+ * ```ts
2858
+ * const scratch = venue.workspace.scoped({ agent: 'alice', task: taskId });
2859
+ * await scratch.read('t/snapshot');
2860
+ * await scratch.list('t/');
2861
+ * ```
2862
+ *
2863
+ * Ordinary paths still work through the handle, so a caller rendering a task
2864
+ * inspector can use one reader for both scratch and workspace. Capability
2865
+ * behaviour is unchanged: the caller needs read caps on the agent's subtree.
2866
+ */
2867
+ scoped(scope) {
2868
+ return new ScopedWorkspace(this, scope);
2869
+ }
2870
+ /** @internal — the scoped read used by {@link ScopedWorkspace}. */
2871
+ _scopedRead(op, params, scope) {
2872
+ return this.scopedRead(op, params, scope);
2873
+ }
2625
2874
  // ── job-free reads (#177) ───────────────────────────────────────────────────
2626
2875
  async read(path, maxSize, ucans) {
2627
2876
  if (ucans?.length) return this.unsupported("UCAN-authorised workspace reads");
@@ -2632,6 +2881,76 @@ var WorkspaceManager = class {
2632
2881
  if (ucans?.length) return this.unsupported("UCAN-authorised workspace listings");
2633
2882
  return this.valuesRead("list", { path, limit, offset });
2634
2883
  }
2884
+ /**
2885
+ * List a node's children *and* read named subpaths of each one, in a single
2886
+ * round trip (covia#191) — the standard partial-response / sparse-fieldset
2887
+ * pattern, and the cure for the collection-view N+1 that list-then-read-each
2888
+ * forces on every page.
2889
+ *
2890
+ * ```ts
2891
+ * const page = await venue.workspace.listFields('j', ['status', 'meta/updated'], { limit: 50 });
2892
+ * page.values['<jobid>']['status']; // → { exists: true, value: 'COMPLETE' }
2893
+ * ```
2894
+ *
2895
+ * Each field is defined as a `read` of `<path>/<key>/<field>` and carries
2896
+ * single-read semantics verbatim: stored null is present, absent is
2897
+ * `{exists:false}`, and a value past `maxSize` withholds `value` and sets
2898
+ * `truncated`. Projection applies *after* the `limit`/`offset` key page, so
2899
+ * work is bounded by `limit × fields.length`.
2900
+ *
2901
+ * Output shape only — no filtering, no ordering. Filter the projected
2902
+ * records locally; for a recency subset use key design plus `slice`.
2903
+ *
2904
+ * On a venue that predates projection this falls back to `list()` plus a
2905
+ * bounded `read()` per (key, field), returning the identical shape, and
2906
+ * remembers not to ask that venue again.
2907
+ *
2908
+ * @param path Parent node. Must be a keyed node (map/Index) — a sequence or
2909
+ * scalar is a venue 400.
2910
+ * @param fields Subpaths to project per key; may be nested (`meta/updated`).
2911
+ * At most 16, the venue's cap.
2912
+ */
2913
+ async listFields(path, fields, opts = {}) {
2914
+ const { limit, offset, maxSize } = opts;
2915
+ if (fields.length === 0) throw new CoviaError("listFields needs at least one field");
2916
+ if (fields.length > MAX_PROJECT_FIELDS) {
2917
+ throw new CoviaError(
2918
+ `A fields projection may name at most ${MAX_PROJECT_FIELDS} subpaths; got ${fields.length}`
2919
+ );
2920
+ }
2921
+ if (this.fieldsSupported) {
2922
+ const result = await this.valuesRead(
2923
+ "list",
2924
+ { path, limit, offset, fields: fields.join(","), maxSize }
2925
+ );
2926
+ if (result.values) return result;
2927
+ if (!result.exists || result.keys === void 0) return { ...result, values: {} };
2928
+ this.fieldsSupported = false;
2929
+ }
2930
+ return this.projectClientSide(path, fields, { limit, offset, maxSize });
2931
+ }
2932
+ /**
2933
+ * The pre-covia#191 fallback: one `list`, then a `read` per (key, field).
2934
+ * Fan-out is bounded by the page the list already returned, so this costs
2935
+ * `keys.length × fields.length` reads for the page — exactly the N+1 the
2936
+ * server-side projection exists to remove, which is why it is the fallback
2937
+ * and not the default.
2938
+ */
2939
+ async projectClientSide(path, fields, opts) {
2940
+ const base = await this.valuesRead(
2941
+ "list",
2942
+ { path, limit: opts.limit, offset: opts.offset }
2943
+ );
2944
+ const keys = base.keys ?? [];
2945
+ const parent = path.replace(/\/+$/, "");
2946
+ const rows = await Promise.all(keys.map(async (key) => {
2947
+ const projected = await Promise.all(fields.map(async (field) => {
2948
+ return [field, await this.read(`${parent}/${key}/${field}`, opts.maxSize)];
2949
+ }));
2950
+ return [key, Object.fromEntries(projected)];
2951
+ }));
2952
+ return { ...base, values: Object.fromEntries(rows) };
2953
+ }
2635
2954
  async slice(path, offset, limit, ucans) {
2636
2955
  if (ucans?.length) return this.unsupported("UCAN-authorised workspace slices");
2637
2956
  return this.valuesRead("slice", { path, offset, limit });
@@ -2682,6 +3001,30 @@ var WorkspaceManager = class {
2682
3001
  return this.venue.operations.run("v/ops/covia/copy", { from, to }, { ucans });
2683
3002
  }
2684
3003
  };
3004
+ var ScopedWorkspace = class {
3005
+ constructor(workspace, scope) {
3006
+ this.workspace = workspace;
3007
+ this.scope = scope;
3008
+ }
3009
+ read(path, maxSize) {
3010
+ return this.workspace._scopedRead("read", { path, maxSize }, this.scope);
3011
+ }
3012
+ list(path, limit, offset) {
3013
+ return this.workspace._scopedRead("list", { path, limit, offset }, this.scope);
3014
+ }
3015
+ slice(path, offset, limit) {
3016
+ return this.workspace._scopedRead("slice", { path, offset, limit }, this.scope);
3017
+ }
3018
+ inspect(path, budget, compact) {
3019
+ return this.workspace._scopedRead("inspect", { path, budget, compact }, this.scope);
3020
+ }
3021
+ count(path, opts = {}) {
3022
+ return this.workspace._scopedRead("count", { path, depth: opts.depth }, this.scope);
3023
+ }
3024
+ aggregate(path, opts = {}) {
3025
+ return this.workspace._scopedRead("aggregate", { path, depth: opts.depth, groupBy: opts.groupBy }, this.scope);
3026
+ }
3027
+ };
2685
3028
 
2686
3029
  // src/UCANManager.ts
2687
3030
  var UCANManager = class {
@@ -3004,6 +3347,185 @@ var A2AManager = class {
3004
3347
  }
3005
3348
  };
3006
3349
 
3350
+ // src/MCPManager.ts
3351
+ var TOOL_CALL_OP = "v/ops/mcp/tools-call";
3352
+ var DEFAULT_MCP_PATH = "/mcp";
3353
+ var HEADER_SESSION_ID = "Mcp-Session-Id";
3354
+ function isResponseFrame(value) {
3355
+ return typeof value === "object" && value !== null && ("result" in value || "error" in value);
3356
+ }
3357
+ var MCPManager = class {
3358
+ constructor(venue) {
3359
+ this.venue = venue;
3360
+ this.nextId = 1;
3361
+ }
3362
+ /** The venue's MCP discovery document (`GET /.well-known/mcp`). */
3363
+ discovery() {
3364
+ return this.venue.mcpDiscovery();
3365
+ }
3366
+ /**
3367
+ * List the tools this venue bridges, job-free.
3368
+ *
3369
+ * @param options.cursor Continue a previous page. MCP pagination is opaque:
3370
+ * pass back the `nextCursor` a page returned, and stop when it is absent.
3371
+ */
3372
+ async listTools(options = {}) {
3373
+ const params = options.cursor !== void 0 ? { cursor: options.cursor } : {};
3374
+ const result = await this.request(
3375
+ "tools/list",
3376
+ params,
3377
+ { signal: options.signal }
3378
+ );
3379
+ const page = { tools: result?.tools ?? [] };
3380
+ if (result?.nextCursor !== void 0) page.nextCursor = result.nextCursor;
3381
+ return page;
3382
+ }
3383
+ /**
3384
+ * Every tool the venue bridges, draining the cursor.
3385
+ *
3386
+ * A server that never stops handing back a cursor would loop forever, so a
3387
+ * repeated cursor is treated as the end rather than trusted.
3388
+ */
3389
+ async listAllTools(options = {}) {
3390
+ const tools = [];
3391
+ const seen = /* @__PURE__ */ new Set();
3392
+ let cursor;
3393
+ for (; ; ) {
3394
+ const page = await this.listTools({ cursor, signal: options.signal });
3395
+ tools.push(...page.tools);
3396
+ if (page.nextCursor === void 0 || seen.has(page.nextCursor)) return tools;
3397
+ seen.add(page.nextCursor);
3398
+ cursor = page.nextCursor;
3399
+ }
3400
+ }
3401
+ /**
3402
+ * Call a bridged tool natively and return its MCP result.
3403
+ *
3404
+ * This is the direct protocol call: the answer comes back in the response,
3405
+ * and the caller never holds a Job. The venue still runs the tool through
3406
+ * its job machinery and records it — "direct" describes what *this* call
3407
+ * hands back, not whether the venue keeps a record.
3408
+ *
3409
+ * Use {@link callToolTracked} when you want the `Job` itself: to stream it,
3410
+ * inspect its steps, link to its receipt, or continue it when it pauses.
3411
+ *
3412
+ * An MCP tool that fails *its own* execution answers with `isError: true`
3413
+ * and the failure in `content` — a successful protocol call reporting a
3414
+ * failed tool, which is not thrown. Only a protocol-level error throws.
3415
+ */
3416
+ async callTool(name, args = {}, options = {}) {
3417
+ return this.request(
3418
+ "tools/call",
3419
+ { name, arguments: args },
3420
+ { signal: options.signal }
3421
+ );
3422
+ }
3423
+ /**
3424
+ * Call a tool through the `v/ops/mcp/tools-call` operation, returning the
3425
+ * `Job` so the run is an inspectable, linkable record.
3426
+ *
3427
+ * The operation is the venue's bridge to *any* MCP server, so `server`
3428
+ * defaults to this venue's own base URL — pointing the bridge back at the
3429
+ * venue, which is how a local tool gets called with a Job in hand. Name a
3430
+ * different `server` (with a `token` if it needs one) to reach a third-party
3431
+ * MCP server through the venue instead, which the native endpoint cannot do.
3432
+ *
3433
+ * Unlike {@link callTool} this always persists a job, so it belongs to a
3434
+ * user-driven execution, never a page load.
3435
+ */
3436
+ async callToolTracked(name, args = {}, options = {}) {
3437
+ const { server, token, ...invokeOptions } = options;
3438
+ const input = {
3439
+ server: server ?? this.venue.baseUrl,
3440
+ toolName: name,
3441
+ arguments: args
3442
+ };
3443
+ if (token !== void 0) input.token = token;
3444
+ return this.venue.operations.invoke(TOOL_CALL_OP, input, invokeOptions);
3445
+ }
3446
+ /**
3447
+ * Send an arbitrary JSON-RPC method to the venue's MCP endpoint — the escape
3448
+ * hatch for methods this manager does not wrap (`resources/list`,
3449
+ * `prompts/list`, …).
3450
+ *
3451
+ * Request ids are minted and correlated here, so callers never supply one.
3452
+ */
3453
+ async request(method, params = {}, options = {}) {
3454
+ const id = this.nextId++;
3455
+ const endpoint = await this.endpoint();
3456
+ const headers = {
3457
+ // The venue may answer either way; advertise both and branch on what
3458
+ // comes back rather than assuming JSON (which is the bug in every
3459
+ // hand-rolled copy of this call).
3460
+ Accept: "application/json, text/event-stream"
3461
+ };
3462
+ if (this.sessionId) headers[HEADER_SESSION_ID] = this.sessionId;
3463
+ const response = await venueStream(this.venue, endpoint, {
3464
+ method: "POST",
3465
+ headers,
3466
+ body: JSON.stringify({ jsonrpc: "2.0", id, method, params }),
3467
+ signal: options.signal
3468
+ });
3469
+ const session = response.headers?.get?.(HEADER_SESSION_ID);
3470
+ if (session) this.sessionId = session;
3471
+ const frame = (response.headers?.get?.("Content-Type") ?? "").includes("text/event-stream") ? await this.readSSEFrame(response, id, options.signal) : await response.json();
3472
+ return this.unwrap(frame, method, id);
3473
+ }
3474
+ /** The resolved MCP endpoint path, from discovery, probed once. */
3475
+ endpoint() {
3476
+ return this.endpointPromise ?? (this.endpointPromise = this.resolveEndpoint());
3477
+ }
3478
+ /**
3479
+ * Discovery names the endpoint; `${baseUrl}/mcp` is the compatibility
3480
+ * default. Discovery failing is not fatal — a venue that serves `/mcp`
3481
+ * without advertising it still works, so a failed probe falls through to the
3482
+ * default rather than failing the call that triggered it.
3483
+ */
3484
+ async resolveEndpoint() {
3485
+ try {
3486
+ const doc = await this.discovery();
3487
+ const advertised = [doc?.server_url, typeof doc?.endpoint === "string" ? doc.endpoint : void 0].find((v) => typeof v === "string" && v.length > 0);
3488
+ if (advertised) {
3489
+ if (/^https?:\/\//i.test(advertised)) {
3490
+ return advertised.startsWith(this.venue.baseUrl) ? advertised.slice(this.venue.baseUrl.length) || DEFAULT_MCP_PATH : advertised;
3491
+ }
3492
+ return advertised.startsWith("/") ? advertised : `/${advertised}`;
3493
+ }
3494
+ } catch {
3495
+ }
3496
+ return DEFAULT_MCP_PATH;
3497
+ }
3498
+ /**
3499
+ * Read the response frame from an SSE-framed answer: the first event whose
3500
+ * JSON-RPC id matches the request. Notifications and unrelated frames are
3501
+ * skipped rather than mistaken for the answer.
3502
+ */
3503
+ async readSSEFrame(response, id, signal) {
3504
+ for await (const event of parseSSEStream(response, signal ? { signal } : void 0)) {
3505
+ let payload;
3506
+ try {
3507
+ payload = event.json();
3508
+ } catch {
3509
+ continue;
3510
+ }
3511
+ if (isResponseFrame(payload) && payload.id === id) return payload;
3512
+ }
3513
+ throw new CoviaError(
3514
+ `MCP stream ended without a response to request ${id}`
3515
+ );
3516
+ }
3517
+ /** JSON-RPC envelope → result, or a typed error. */
3518
+ unwrap(frame, method, id) {
3519
+ if (frame?.error) {
3520
+ throw new MCPError(frame.error.message || `MCP ${method} failed`, frame.error.code, frame.error.data);
3521
+ }
3522
+ if (!frame || !("result" in frame)) {
3523
+ throw new CoviaError(`MCP ${method} returned no result for request ${id}`);
3524
+ }
3525
+ return frame.result;
3526
+ }
3527
+ };
3528
+
3007
3529
  // src/Agent.ts
3008
3530
  var Agent = class _Agent {
3009
3531
  constructor(id, venue) {
@@ -3053,6 +3575,15 @@ var Agent = class _Agent {
3053
3575
  async getSession(sessionId) {
3054
3576
  return this._agents.getSession(this.id, sessionId);
3055
3577
  }
3578
+ /**
3579
+ * One session's transcript — its turns in order, with compacted segments
3580
+ * expanded back into the turns they archived.
3581
+ *
3582
+ * Shorthand for `getSession(sessionId)` when only the conversation is wanted.
3583
+ */
3584
+ async transcript(sessionId) {
3585
+ return (await this._agents.getSession(this.id, sessionId)).conversation;
3586
+ }
3056
3587
  /** Stream this agent's run-loop events. See `AgentManager.events`. */
3057
3588
  async *events(options) {
3058
3589
  yield* this._agents.events(this.id, options);
@@ -3212,6 +3743,9 @@ var Venue = class _Venue {
3212
3743
  get users() {
3213
3744
  return this._users ?? (this._users = new UserManager(this));
3214
3745
  }
3746
+ get mcp() {
3747
+ return this._mcp ?? (this._mcp = new MCPManager(this));
3748
+ }
3215
3749
  get a2a() {
3216
3750
  return this._a2a ?? (this._a2a = new A2AManager(this));
3217
3751
  }
@@ -3579,6 +4113,8 @@ exports.JobManager = JobManager;
3579
4113
  exports.JobNotFoundError = JobNotFoundError;
3580
4114
  exports.JobStatus = JobStatus;
3581
4115
  exports.KeyPairAuth = KeyPairAuth;
4116
+ exports.MCPError = MCPError;
4117
+ exports.MCPManager = MCPManager;
3582
4118
  exports.MemoryManager = MemoryManager;
3583
4119
  exports.Namespace = Namespace;
3584
4120
  exports.NoAuth = NoAuth;
@@ -3587,6 +4123,7 @@ exports.Operation = Operation;
3587
4123
  exports.OperationManager = OperationManager;
3588
4124
  exports.RateLimitError = RateLimitError;
3589
4125
  exports.RunStatus = RunStatus;
4126
+ exports.ScopedWorkspace = ScopedWorkspace;
3590
4127
  exports.SecretManager = SecretManager;
3591
4128
  exports.SkillManager = SkillManager;
3592
4129
  exports.UCANManager = UCANManager;