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