@tangle-network/agent-provider-tangle 1.1.8 → 1.1.10
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 +19 -4
- package/dist/tangle-prompt.d.ts +7 -0
- package/dist/tangle-prompt.js +114 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,10 +11,16 @@ import { Sandbox } from "@tangle-network/sandbox";
|
|
|
11
11
|
import { createTangleProvider } from "@tangle-network/agent-provider-tangle";
|
|
12
12
|
|
|
13
13
|
const provider = createTangleProvider({
|
|
14
|
-
client: new Sandbox({
|
|
14
|
+
client: new Sandbox({
|
|
15
|
+
apiKey: process.env.TANGLE_API_KEY,
|
|
16
|
+
baseUrl: process.env.TANGLE_SANDBOX_URL || "https://sandbox.tangle.tools",
|
|
17
|
+
}),
|
|
15
18
|
});
|
|
16
19
|
```
|
|
17
20
|
|
|
21
|
+
`Sandbox` requires an explicit `baseUrl`.
|
|
22
|
+
Set `TANGLE_SANDBOX_URL` to use another deployment.
|
|
23
|
+
|
|
18
24
|
## `create()` returns a ready environment
|
|
19
25
|
|
|
20
26
|
`provider.create()` does not return until the sandbox reports `running`.
|
|
@@ -32,7 +38,10 @@ Composing an environment also reads the sandbox's deployment capability document
|
|
|
32
38
|
|
|
33
39
|
```ts
|
|
34
40
|
const provider = createTangleProvider({
|
|
35
|
-
client: new Sandbox({
|
|
41
|
+
client: new Sandbox({
|
|
42
|
+
apiKey: process.env.TANGLE_API_KEY,
|
|
43
|
+
baseUrl: process.env.TANGLE_SANDBOX_URL || "https://sandbox.tangle.tools",
|
|
44
|
+
}),
|
|
36
45
|
readyTimeoutMs: 180_000,
|
|
37
46
|
});
|
|
38
47
|
|
|
@@ -280,7 +289,10 @@ The adapter does not trust the requested nonce, child metadata, or any legacy
|
|
|
280
289
|
|
|
281
290
|
```ts
|
|
282
291
|
const provider = createTangleProvider({
|
|
283
|
-
client: new Sandbox({
|
|
292
|
+
client: new Sandbox({
|
|
293
|
+
apiKey: process.env.TANGLE_API_KEY,
|
|
294
|
+
baseUrl: process.env.TANGLE_SANDBOX_URL || "https://sandbox.tangle.tools",
|
|
295
|
+
}),
|
|
284
296
|
confidentialAttestationVerifier: async ({ report, attestation }) =>
|
|
285
297
|
(await verifyTangleQuote({ report, attestation })) ?? null,
|
|
286
298
|
});
|
|
@@ -360,7 +372,10 @@ Set `teamId` inside `exactProcess` to scope create, lookup, and recovery to one
|
|
|
360
372
|
|
|
361
373
|
```ts
|
|
362
374
|
const provider = createTangleProvider({
|
|
363
|
-
client: new Sandbox({
|
|
375
|
+
client: new Sandbox({
|
|
376
|
+
apiKey: process.env.TANGLE_API_KEY,
|
|
377
|
+
baseUrl: process.env.TANGLE_SANDBOX_URL || "https://sandbox.tangle.tools",
|
|
378
|
+
}),
|
|
364
379
|
exactProcess: {},
|
|
365
380
|
});
|
|
366
381
|
|
package/dist/tangle-prompt.d.ts
CHANGED
|
@@ -80,6 +80,13 @@ type ValidatedSandboxPromptResult = Record<string, unknown> & {
|
|
|
80
80
|
durationMs: number;
|
|
81
81
|
executionId?: string;
|
|
82
82
|
};
|
|
83
|
+
/**
|
|
84
|
+
* The marker left in place of a tool result's discarded tail.
|
|
85
|
+
*
|
|
86
|
+
* It names the byte counts so a reader can tell a truncated result from a tool that genuinely
|
|
87
|
+
* returned little, and so a caller can decide to re-fetch rather than reason from a partial page.
|
|
88
|
+
*/
|
|
89
|
+
export declare function toolOutputTruncationMarker(keptBytes: number, originalBytes: number): string;
|
|
83
90
|
export declare function validatedSandboxPromptResult(result: PromptResult): ValidatedSandboxPromptResult;
|
|
84
91
|
export declare function agentTurnResultFromPromptRecord(record: ValidatedSandboxPromptResult, options?: {
|
|
85
92
|
contextTransferRequested?: boolean;
|
package/dist/tangle-prompt.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { agentProfileSchema, boundedEventContentRecordSchema, AgentExactRunControlRefSchema, AgentTurnInputSchema, ContextTransferReceiptSchema, contextTransferResultMatchesRequest, } from "@tangle-network/agent-interface";
|
|
1
|
+
import { agentProfileSchema, CONTRACT_MAX_JSON_BYTES, boundedEventContentRecordSchema, isBoundedEventContentJson, AgentExactRunControlRefSchema, AgentTurnInputSchema, ContextTransferReceiptSchema, contextTransferResultMatchesRequest, } from "@tangle-network/agent-interface";
|
|
2
2
|
import { tokenUsageFromData } from "./tangle-result-values.js";
|
|
3
3
|
import { assertBoundedJson, boundedString } from "./tangle-contract-safety.js";
|
|
4
4
|
export function promptFromTurnInput(input) {
|
|
@@ -359,6 +359,74 @@ const SANDBOX_OPTIONAL_RESULT_FIELDS = new Set([
|
|
|
359
359
|
"usage",
|
|
360
360
|
"costUsd",
|
|
361
361
|
]);
|
|
362
|
+
/**
|
|
363
|
+
* The marker left in place of a tool result's discarded tail.
|
|
364
|
+
*
|
|
365
|
+
* It names the byte counts so a reader can tell a truncated result from a tool that genuinely
|
|
366
|
+
* returned little, and so a caller can decide to re-fetch rather than reason from a partial page.
|
|
367
|
+
*/
|
|
368
|
+
export function toolOutputTruncationMarker(keptBytes, originalBytes) {
|
|
369
|
+
return `\n\n[truncated by the Tangle provider: kept ${keptBytes} of ${originalBytes} bytes to stay inside the ${CONTRACT_MAX_JSON_BYTES}-byte record content bound]`;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Shrink oversized tool output until the whole record fits the content bound.
|
|
373
|
+
*
|
|
374
|
+
* Only `toolInvocations[].result` is touched, because it is the one field that carries arbitrary
|
|
375
|
+
* fetched material rather than identity, accounting, or control state. The largest result is cut
|
|
376
|
+
* first and the loop repeats, so a record with several big results converges instead of destroying
|
|
377
|
+
* the first one it meets.
|
|
378
|
+
*
|
|
379
|
+
* Returns the record unchanged when it already fits, so the common path allocates nothing.
|
|
380
|
+
*/
|
|
381
|
+
function withTruncatedToolOutput(record) {
|
|
382
|
+
if (isBoundedEventContentJson(record))
|
|
383
|
+
return { record, truncated: 0 };
|
|
384
|
+
const invocations = record.toolInvocations;
|
|
385
|
+
if (!Array.isArray(invocations))
|
|
386
|
+
return { record, truncated: 0 };
|
|
387
|
+
const next = invocations.map((entry) => entry && typeof entry === "object" && !Array.isArray(entry)
|
|
388
|
+
? { ...entry }
|
|
389
|
+
: entry);
|
|
390
|
+
const originalLengths = new Map();
|
|
391
|
+
let truncated = 0;
|
|
392
|
+
// Each pass cuts the current widest result by the record's measured overflow, so a single huge
|
|
393
|
+
// value converges immediately and several large ones shrink in turn rather than the first being
|
|
394
|
+
// destroyed. The cap is a backstop: `isBoundedEventContentJson` also enforces node, depth, and
|
|
395
|
+
// array limits that trimming a string cannot satisfy, and those must fall through to the refusal
|
|
396
|
+
// rather than spin here.
|
|
397
|
+
for (let pass = 0; pass < next.length * 4 + 8; pass += 1) {
|
|
398
|
+
const candidate = { ...record, toolInvocations: next };
|
|
399
|
+
if (isBoundedEventContentJson(candidate))
|
|
400
|
+
return { record: candidate, truncated };
|
|
401
|
+
let widest = -1;
|
|
402
|
+
let widestLength = 0;
|
|
403
|
+
for (const [index, entry] of next.entries()) {
|
|
404
|
+
const value = entry?.result;
|
|
405
|
+
if (typeof value === "string" && value.length > widestLength) {
|
|
406
|
+
widest = index;
|
|
407
|
+
widestLength = value.length;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
// Nothing left to shrink: the overflow is elsewhere, and the caller refuses rather than
|
|
411
|
+
// silently altering a field that is not tool output.
|
|
412
|
+
if (widest < 0 || widestLength === 0)
|
|
413
|
+
return { record, truncated };
|
|
414
|
+
const entry = next[widest];
|
|
415
|
+
const current = entry.result;
|
|
416
|
+
const original = originalLengths.get(widest) ?? current.length;
|
|
417
|
+
originalLengths.set(widest, original);
|
|
418
|
+
// Cut by what the record is actually over, plus room for the marker and for JSON escaping,
|
|
419
|
+
// which can widen a byte count well past the character count.
|
|
420
|
+
const serialized = Buffer.byteLength(JSON.stringify(candidate) ?? "", "utf8");
|
|
421
|
+
const overflow = Math.max(0, serialized - CONTRACT_MAX_JSON_BYTES);
|
|
422
|
+
const marker = toolOutputTruncationMarker(0, original).length;
|
|
423
|
+
const cut = Math.max(1, overflow + marker + 1024);
|
|
424
|
+
const kept = Math.max(0, current.length - cut);
|
|
425
|
+
entry.result = current.slice(0, kept) + toolOutputTruncationMarker(kept, original);
|
|
426
|
+
truncated += 1;
|
|
427
|
+
}
|
|
428
|
+
return { record, truncated };
|
|
429
|
+
}
|
|
362
430
|
export function validatedSandboxPromptResult(result) {
|
|
363
431
|
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
364
432
|
throw new Error("Tangle prompt returned no result object");
|
|
@@ -371,13 +439,54 @@ export function validatedSandboxPromptResult(result) {
|
|
|
371
439
|
// The Sandbox SDK materializes absent optional response fields as
|
|
372
440
|
// `undefined`. They were absent on the JSON wire and must stay absent in the
|
|
373
441
|
// provider-neutral result before the strict JSON check runs.
|
|
374
|
-
|
|
375
|
-
|
|
442
|
+
let record = Object.fromEntries(Object.entries(source).filter(([field, value]) => value !== undefined || !SANDBOX_OPTIONAL_RESULT_FIELDS.has(field)));
|
|
443
|
+
// Oversized tool output is TRUNCATED, never thrown.
|
|
444
|
+
//
|
|
445
|
+
// This validator runs inside the terminal result read — after the live stream has drained and
|
|
446
|
+
// after the usage receipt has been credited — so a throw here cannot prevent the work or the
|
|
447
|
+
// charge. It can only discard a finished, fully paid turn, which a supervisor then reports as a
|
|
448
|
+
// child that did nothing at all. That failure mode cost one Discovery Lab 143 of 199 children
|
|
449
|
+
// across 16 pursuits, and its six-stage sourcing graph blocked in 24 of 24 invocations.
|
|
450
|
+
//
|
|
451
|
+
// Widening the bound alone does not remove it. The Sandbox SDK serializes each tool value up to
|
|
452
|
+
// MAX_SERIALIZED_TOOL_VALUE_BYTES (4 MiB) while this bound is CONTRACT_MAX_JSON_BYTES (1 MiB)
|
|
453
|
+
// across the WHOLE record, so a single 2 MiB fetch still dies, and so do two 0.6 MiB fetches
|
|
454
|
+
// together. Any bound that throws on this path is one large page away from discarding a paid
|
|
455
|
+
// turn again.
|
|
456
|
+
//
|
|
457
|
+
// So the bound is enforced by replacing what does not fit, and saying so in the record. The
|
|
458
|
+
// caller still receives the turn, its response text, its usage, and every tool call it made;
|
|
459
|
+
// what it loses is the tail of an oversized tool result, marked where it was cut. Only tool
|
|
460
|
+
// output is truncatable: every other field is identity, accounting, or control material where a
|
|
461
|
+
// silently shortened value would be worse than a refusal, so those still refuse below.
|
|
462
|
+
const bounded = withTruncatedToolOutput(record);
|
|
463
|
+
const content = boundedEventContentRecordSchema.safeParse(bounded.record);
|
|
376
464
|
if (!content.success) {
|
|
377
465
|
throw new Error("Tangle prompt result exceeded its JSON bound", { cause: content.error });
|
|
378
466
|
}
|
|
379
|
-
|
|
380
|
-
|
|
467
|
+
record = bounded.record;
|
|
468
|
+
// What the agent produced is content and is bounded as content by the check above; the fields
|
|
469
|
+
// that describe the turn keep their metadata limits.
|
|
470
|
+
//
|
|
471
|
+
// `toolInvocations` belongs with the response text, not with the metadata. It carries whatever a
|
|
472
|
+
// tool returned, and one `webfetch` of a paper or an API page is routinely tens or hundreds of
|
|
473
|
+
// kilobytes, while the metadata bound is CONTRACT_MAX_STRING_LENGTH — 16,384 characters per
|
|
474
|
+
// string. `isBoundedEventContentJson` was written for exactly this material ("content may contain
|
|
475
|
+
// a single large transcript or tool result") and the whole record has already passed it at
|
|
476
|
+
// CONTRACT_MAX_JSON_BYTES.
|
|
477
|
+
//
|
|
478
|
+
// Holding tool output to the metadata bound rejected a turn the Sandbox SDK had already accepted:
|
|
479
|
+
// it serializes each tool value up to MAX_SERIALIZED_TOOL_VALUE_BYTES (4 MiB), a 256x mismatch.
|
|
480
|
+
// Because this validator runs inside the terminal result read, AFTER the live stream has drained
|
|
481
|
+
// and the usage receipt has been credited, the rejection did not fail the tool call — it
|
|
482
|
+
// converted a finished, fully paid turn into an unreconcilable retained execution that a
|
|
483
|
+
// supervisor reports as a child that did no work at all. Measured 2026-09-11 in one Discovery Lab
|
|
484
|
+
// worktree: 143 of 199 children across 16 pursuits, every one at `iterations: 0`, and the
|
|
485
|
+
// enumerate and extract stages that fetch papers were the ones that died.
|
|
486
|
+
assertBoundedJson(Object.fromEntries(Object.entries(record).filter(([field]) => field !== "response" &&
|
|
487
|
+
field !== "text" &&
|
|
488
|
+
field !== "finalText" &&
|
|
489
|
+
field !== "toolInvocations")));
|
|
381
490
|
if (typeof record.success !== "boolean") {
|
|
382
491
|
throw new Error("Tangle prompt result omitted its success status");
|
|
383
492
|
}
|