diff --git a/packages/agent-interface/CHANGELOG.md b/packages/agent-interface/CHANGELOG.md index f606c39..4d8e42d 100644 --- a/packages/agent-interface/CHANGELOG.md +++ b/packages/agent-interface/CHANGELOG.md @@ -1,5 +1,12 @@ # @tangle-network/agent-interface +## 2.6.1 + +### Patch Changes + +- Allow streamed event content and terminal response text up to the existing 1 MiB serialized UTF-8 contract limit. + Keep metadata, identifiers, and structural bounds unchanged, and export the shared content validator for providers. + ## 2.6.0 ### Minor Changes diff --git a/packages/agent-interface/package.json b/packages/agent-interface/package.json index bc096ed..d51b6bc 100644 --- a/packages/agent-interface/package.json +++ b/packages/agent-interface/package.json @@ -1,6 +1,6 @@ { "name": "@tangle-network/agent-interface", - "version": "2.6.0", + "version": "2.6.1", "type": "module", "sideEffects": false, "license": "MIT", diff --git a/packages/agent-interface/src/contract-limits.ts b/packages/agent-interface/src/contract-limits.ts index cc786b9..c62e98f 100644 --- a/packages/agent-interface/src/contract-limits.ts +++ b/packages/agent-interface/src/contract-limits.ts @@ -84,6 +84,142 @@ export function isBoundedJsonValue(value: unknown): boolean { return true; } +/** Exact serialized JSON UTF-8 byte count for one string scalar. */ +function serializedJsonStringBytes(value: string): number { + let bytes = 2; // Opening and closing quotes. + for (let index = 0; index < value.length; index += 1) { + const code = value.charCodeAt(index); + if (code === 0x22 || code === 0x5c || code === 0x08 || code === 0x0c || code === 0x0a || code === 0x0d || code === 0x09) { + bytes += 2; + } else if (code < 0x20) { + bytes += 6; + } else if (code >= 0xd800 && code <= 0xdbff && index + 1 < value.length) { + const next = value.charCodeAt(index + 1); + if (next >= 0xdc00 && next <= 0xdfff) { + bytes += 4; + index += 1; + } else { + bytes += 6; + } + } else if (code >= 0xd800 && code <= 0xdfff) { + bytes += 6; + } else if (code < 0x80) { + bytes += 1; + } else if (code < 0x800) { + bytes += 2; + } else { + bytes += 3; + } + if (bytes > CONTRACT_MAX_JSON_BYTES) return bytes; + } + return bytes; +} + +/** + * Validate provider event and terminal content. Unlike ordinary metadata, + * content may contain a single large transcript or tool result. It remains + * finite, plain JSON with the normal structural limits, and its complete JSON + * representation is capped exactly by UTF-8 bytes. + */ +export function isBoundedEventContentJson( + value: unknown, + { omitUndefinedObjectFields = false }: { omitUndefinedObjectFields?: boolean } = {}, +): boolean { + const pending: Array<{ + value: unknown; + depth: number; + leave?: boolean; + omitUndefined?: boolean; + }> = [ + { value, depth: 0 }, + ]; + const ancestors = new Set(); + let nodes = 0; + let bytes = 0; + const addBytes = (additional: number) => { + bytes += additional; + return bytes <= CONTRACT_MAX_JSON_BYTES; + }; + while (pending.length > 0) { + const item = pending.pop(); + if (!item) continue; + const current = item.value; + if (current === undefined && item.omitUndefined === true) continue; + nodes += 1; + if (nodes > CONTRACT_MAX_JSON_NODES) return false; + if (item.leave) { + ancestors.delete(current as object); + continue; + } + if (current === null) { + if (!addBytes(4)) return false; + continue; + } + if (typeof current === "boolean") { + if (!addBytes(current ? 4 : 5)) return false; + continue; + } + if (typeof current === "string") { + if (!addBytes(serializedJsonStringBytes(current))) return false; + continue; + } + if (typeof current === "number") { + if (!Number.isFinite(current)) return false; + if (!addBytes(JSON.stringify(current).length)) return false; + continue; + } + if (typeof current !== "object" || item.depth >= CONTRACT_MAX_JSON_DEPTH) { + return false; + } + if (ancestors.has(current)) return false; + ancestors.add(current); + pending.push({ value: current, depth: item.depth, leave: true }); + if (Array.isArray(current)) { + if (current.length > CONTRACT_MAX_ARRAY_LENGTH) return false; + if (!addBytes(2 + Math.max(current.length - 1, 0))) return false; + const prototype = Object.getPrototypeOf(current); + if (prototype !== Array.prototype && prototype !== null) return false; + const keys = Reflect.ownKeys(current); + const entryKeys = keys.filter((key) => key !== "length"); + if (entryKeys.length !== current.length) return false; + if (entryKeys.some((key) => { + if (typeof key !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(key)) return true; + const index = Number(key); + return !Number.isSafeInteger(index) || index < 0 || index >= current.length || index >= 4_294_967_295; + })) { + return false; + } + for (const key of entryKeys) { + const descriptor = Object.getOwnPropertyDescriptor(current, key); + if (descriptor === undefined || !descriptor.enumerable || !("value" in descriptor)) return false; + pending.push({ value: descriptor.value, depth: item.depth + 1 }); + } + continue; + } + const prototype = Object.getPrototypeOf(current); + if (prototype !== Object.prototype && prototype !== null) return false; + const entries: Array<[string, PropertyDescriptor]> = []; + for (const key of Reflect.ownKeys(current)) { + if (typeof key !== "string" || key.length > CONTRACT_MAX_IDENTIFIER_LENGTH) return false; + const descriptor = Object.getOwnPropertyDescriptor(current, key); + if (descriptor === undefined || !descriptor.enumerable || !("value" in descriptor)) return false; + if (descriptor.value === undefined && omitUndefinedObjectFields) continue; + entries.push([key, descriptor]); + if (entries.length > CONTRACT_MAX_MAP_ENTRIES) return false; + } + if (!addBytes(2 + Math.max(entries.length - 1, 0))) return false; + for (const [key, descriptor] of entries) { + if (!addBytes(serializedJsonStringBytes(key) + 1)) return false; + pending.push({ + value: descriptor.value, + depth: item.depth + 1, + omitUndefined: omitUndefinedObjectFields, + }); + } + } + return true; +} + /** Validate digest input while matching JSON's omission of undefined object fields. */ export function isBoundedJsonMaterial(value: unknown): boolean { const pending: Array<{ @@ -162,6 +298,26 @@ export const boundedJsonRecordSchema = z.custom>( { message: "metadata exceeds the contract bounds or is not a JSON object" }, ); +/** One provider event payload or terminal response, bounded as serialized UTF-8 JSON. */ +export const boundedEventContentJsonSchema = z.custom( + isBoundedEventContentJson, + { message: "event content exceeds its serialized byte bound or is not finite JSON" }, +); + +export const boundedEventContentRecordSchema = z.custom>( + (value) => + typeof value === "object" && + value !== null && + !Array.isArray(value) && + isBoundedEventContentJson(value), + { message: "event content exceeds its serialized byte bound or is not a JSON object" }, +); + +export const boundedEventContentStringSchema = z.custom( + (value) => typeof value === "string" && isBoundedEventContentJson(value), + { message: "event content exceeds its serialized byte bound or is not a string" }, +); + export function assertBoundedJson(value: unknown): void { if (!isBoundedJsonValue(value)) { throw new Error("value exceeds the contract bounds or is not finite JSON"); diff --git a/packages/agent-interface/src/environment-runtime.ts b/packages/agent-interface/src/environment-runtime.ts index f7355cc..82cdd91 100644 --- a/packages/agent-interface/src/environment-runtime.ts +++ b/packages/agent-interface/src/environment-runtime.ts @@ -18,7 +18,7 @@ import type { AgentWorkspaceBranchingProvider, } from "./workspace-branching.js"; import { AgentProfileCapabilitiesSchema } from "./environment-profile-capabilities.js"; -import { boundedIdentifierSchema, boundedJsonRecordSchema, boundedJsonSchema, boundedStringSchema, CONTRACT_MAX_ARRAY_LENGTH } from "./contract-limits.js"; +import { boundedEventContentJsonSchema, boundedEventContentRecordSchema, boundedEventContentStringSchema, boundedIdentifierSchema, boundedJsonRecordSchema, boundedStringSchema, CONTRACT_MAX_ARRAY_LENGTH } from "./contract-limits.js"; import { InputPartSchema } from "./portable-context-shared.js"; import { deepFreeze } from "./deep-freeze.js"; import type { AgentEnvironmentEgressMode, AgentEnvironmentEgressPolicy, AgentEnvironmentQuery, AgentEnvironmentStatus, AgentEnvironmentSummary, AgentProfileRef, AgentSessionStatus, CheckpointRef, CheckpointRequest, ExecRequest, ExecResult, ForkRequest, PlacementInfo, ResourceRequest, WorkspaceRequest } from "./environment-requests.js"; @@ -96,17 +96,17 @@ const TokenUsageSchema = z.strictObject({ const AgentEnvironmentEventSchema = z.strictObject({ type: boundedIdentifierSchema, - data: boundedJsonRecordSchema, + data: boundedEventContentRecordSchema, id: boundedIdentifierSchema.optional(), normalized: CanonicalStreamEventSchema.optional(), usage: TokenUsageSchema.optional(), usageMode: z.enum(["delta", "cumulative"]).optional(), - providerEvent: boundedJsonSchema.optional(), + providerEvent: boundedEventContentJsonSchema.optional(), }) satisfies z.ZodType; /** Runtime validator for a provider turn returned from durable continuation. */ export const AgentTurnResultSchema = z.strictObject({ - text: boundedStringSchema, + text: boundedEventContentStringSchema, success: z.boolean(), error: boundedStringSchema.optional(), sessionId: boundedIdentifierSchema.optional(), diff --git a/packages/agent-interface/src/index.ts b/packages/agent-interface/src/index.ts index 68b15cf..ae4aa88 100644 --- a/packages/agent-interface/src/index.ts +++ b/packages/agent-interface/src/index.ts @@ -120,6 +120,11 @@ export * from "./profile-security.js"; export * from "./sandbox-size.js"; export { CONTRACT_MAX_CONFIDENTIAL_ATTESTATION_QUOTE_LENGTH, + CONTRACT_MAX_JSON_BYTES, + boundedEventContentJsonSchema, + boundedEventContentRecordSchema, + boundedEventContentStringSchema, + isBoundedEventContentJson, } from "./contract-limits.js"; export { AgentEnvironmentEgressPolicySchema } from "./environment-requests.js"; diff --git a/packages/agent-interface/src/leaf-modules.test.ts b/packages/agent-interface/src/leaf-modules.test.ts index cb61bbc..fecb567 100644 --- a/packages/agent-interface/src/leaf-modules.test.ts +++ b/packages/agent-interface/src/leaf-modules.test.ts @@ -61,7 +61,7 @@ import { InputPartSchema, wireDigest as portableWireDigest, } from "./portable-context-shared.js"; -import { isBoundedJsonValue } from "./contract-limits.js"; +import { isBoundedEventContentJson, isBoundedJsonValue } from "./contract-limits.js"; import { ContextTransferReceiptSchema, ContextTransferRequestSchema, @@ -73,10 +73,12 @@ import { CONTRACT_MAX_CONFIDENTIAL_ATTESTATION_QUOTE_LENGTH, CONTRACT_MAX_JSON_BYTES, CONTRACT_MAX_STRING_LENGTH, + boundedEventContentStringSchema, boundedJsonSchema, boundedStringSchema, nullPrototypeRecord, } from "./contract-limits.js"; +import { CanonicalStreamEventSchema } from "./runtime-control.js"; import { WorkspaceCheckpointRefSchema, WorkspaceCheckpointRequestSchema, @@ -228,6 +230,73 @@ describe("interface split leaf modules", () => { expect(() => portableWireDigest(unicodeMaterial)).toThrow(/byte bound/); }); + it("accepts large event content up to its exact serialized UTF-8 bound", () => { + const maximum = "x".repeat(CONTRACT_MAX_JSON_BYTES - 2); + expect(isBoundedEventContentJson(maximum)).toBe(true); + expect(boundedEventContentStringSchema.parse(maximum)).toBe(maximum); + expect(isBoundedEventContentJson(`${maximum}x`)).toBe(false); + expect(boundedEventContentStringSchema.safeParse(`${maximum}x`).success).toBe(false); + expect(isBoundedEventContentJson({ unicode: "é".repeat(600_000) })).toBe(false); + expect(isBoundedEventContentJson({ ordinary: "x".repeat(CONTRACT_MAX_STRING_LENGTH + 1) })).toBe(true); + const altered = ["small"] as unknown as { toJSON: () => string }; + altered.toJSON = () => "x".repeat(CONTRACT_MAX_JSON_BYTES + 1); + expect(isBoundedEventContentJson(altered)).toBe(false); + const inheritedToJson = ["small"]; + Object.setPrototypeOf(inheritedToJson, { toJSON: () => "x".repeat(CONTRACT_MAX_JSON_BYTES + 1) }); + expect(isBoundedEventContentJson(inheritedToJson)).toBe(false); + const sparseWithNonIndex = new Array(1) as unknown as Record; + sparseWithNonIndex["4294967295"] = "ignored by JSON.stringify"; + expect(isBoundedEventContentJson(sparseWithNonIndex)).toBe(false); + for (const pattern of ["\ud800", "\"", "\\", "\u0000", "\b", "\n"]) { + const scalarBytes = Buffer.byteLength(JSON.stringify(pattern), "utf8") - 2; + const atLimit = pattern.repeat(Math.floor((CONTRACT_MAX_JSON_BYTES - 2) / scalarBytes)); + for (const value of [atLimit, `${atLimit}${pattern}`]) { + expect(isBoundedEventContentJson(value)).toBe( + Buffer.byteLength(JSON.stringify(value), "utf8") <= CONTRACT_MAX_JSON_BYTES, + ); + } + } + }); + + it("keeps long canonical stream and terminal response content without widening metadata", () => { + const content = "x".repeat(CONTRACT_MAX_STRING_LENGTH + 1); + const normalized = { + type: "message.part.updated" as const, + part: { + id: "part-1", + sessionID: "session-1", + messageID: "message-1", + type: "tool" as const, + tool: "shell", + state: { status: "completed" as const, input: {}, output: { content } }, + }, + delta: content, + }; + expect(CanonicalStreamEventSchema.parse(normalized)).toEqual(normalized); + expect(AgentTurnResultSchema.parse({ + text: content, + success: true, + events: [{ type: "provider-event", data: { content }, normalized, providerEvent: { content } }], + }).text).toBe(content); + expect(AgentTurnResultSchema.safeParse({ + text: "done", + success: true, + metadata: { content }, + }).success).toBe(false); + const withExplicitUndefined = { + type: "message.part.updated" as const, + part: { + id: "part-2", + sessionID: "session-1", + messageID: "message-1", + type: "text" as const, + text: "present", + }, + delta: undefined, + }; + expect(CanonicalStreamEventSchema.parse(withExplicitUndefined)).toEqual(withExplicitUndefined); + }); + it("exports the portable workspace cwd leaf contract", () => { expect(canonicalWorkspaceCwd({ base: "repository", path: "./packages//braid/." })).toEqual({ base: "repository", diff --git a/packages/agent-interface/src/runtime-control.ts b/packages/agent-interface/src/runtime-control.ts index c9ada83..21ba282 100644 --- a/packages/agent-interface/src/runtime-control.ts +++ b/packages/agent-interface/src/runtime-control.ts @@ -6,10 +6,13 @@ import { } from "./agent-candidate-schema-common.js"; import type { Sha256Digest } from "./agent-candidate.js"; import { + boundedEventContentJsonSchema, + boundedEventContentRecordSchema, + boundedEventContentStringSchema, boundedIdentifierSchema, boundedJsonRecordSchema, - boundedJsonSchema, boundedStringSchema, + isBoundedEventContentJson, } from "./contract-limits.js"; import { ModelUsageSchema } from "./environment-observation.js"; import { InteractionRequestSchema } from "./interaction.js"; @@ -341,20 +344,20 @@ const toolTimeSchema = z.strictObject({ const toolStateSchema = z.discriminatedUnion("status", [ z.strictObject({ status: z.literal("pending"), - input: unknownRecordSchema, - raw: boundedStringSchema.optional(), + input: boundedEventContentRecordSchema, + raw: boundedEventContentStringSchema.optional(), }), z.strictObject({ status: z.literal("running"), - input: unknownRecordSchema, + input: boundedEventContentRecordSchema, title: boundedStringSchema.optional(), metadata: unknownRecordSchema.optional(), time: z.strictObject({ start: z.number().finite() }).optional(), }), z.strictObject({ status: z.literal("completed"), - input: unknownRecordSchema, - output: boundedJsonSchema, + input: boundedEventContentRecordSchema, + output: boundedEventContentJsonSchema, title: boundedStringSchema.optional(), metadata: unknownRecordSchema.optional(), time: z.strictObject({ @@ -364,15 +367,15 @@ const toolStateSchema = z.discriminatedUnion("status", [ }), z.strictObject({ status: z.enum(["error", "failed"]), - input: unknownRecordSchema, + input: boundedEventContentRecordSchema, error: boundedStringSchema.optional(), - output: boundedJsonSchema.optional(), + output: boundedEventContentJsonSchema.optional(), metadata: unknownRecordSchema.optional(), time: toolTimeSchema.optional(), }), ]); const partSchema = z.discriminatedUnion("type", [ - z.strictObject({ ...partBase, type: z.literal("text"), text: boundedStringSchema }), + z.strictObject({ ...partBase, type: z.literal("text"), text: boundedEventContentStringSchema }), z.strictObject({ ...partBase, type: z.literal("tool"), @@ -384,7 +387,7 @@ const partSchema = z.discriminatedUnion("type", [ z.strictObject({ ...partBase, type: z.literal("reasoning"), - text: boundedStringSchema, + text: boundedEventContentStringSchema, }), z.strictObject({ ...partBase, @@ -469,12 +472,11 @@ const ChildTaskEventSchema = z }); /** Runtime validator for every member of the existing canonical event union. */ -export const CanonicalStreamEventSchema: z.ZodType = - z.discriminatedUnion("type", [ +const CanonicalStreamEventUnionSchema = z.discriminatedUnion("type", [ z.strictObject({ type: z.literal("message.part.updated"), part: partSchema, - delta: boundedStringSchema.optional(), + delta: boundedEventContentStringSchema.optional(), }), z.strictObject({ type: z.literal("tool-heartbeat"), @@ -514,7 +516,7 @@ export const CanonicalStreamEventSchema: z.ZodType = z.strictObject({ type: z.literal("raw"), backend: stableIdSchema, - event: boundedJsonSchema, + event: boundedEventContentJsonSchema, }), z.strictObject({ type: z.literal("session.updated"), @@ -538,8 +540,21 @@ export const CanonicalStreamEventSchema: z.ZodType = type: z.literal("plan.submitted"), plan: DurablePlanSchema, }), - ChildTaskEventSchema, - ]); + ChildTaskEventSchema, +]); + +export const CanonicalStreamEventSchema: z.ZodType = + CanonicalStreamEventUnionSchema.superRefine((event, refinement) => { + // Zod retains explicitly supplied optional `undefined` fields, whereas a + // JSON event omits them. Canonical events use that wire-equivalent omission + // without making raw provider records accept undefined. + if (!isBoundedEventContentJson(event, { omitUndefinedObjectFields: true })) { + refinement.addIssue({ + code: "custom", + message: "canonical stream event exceeds its serialized byte bound", + }); + } + }); /** Ordered, replayable envelope around the existing canonical event union. */ export interface RuntimeEventEnvelope { diff --git a/packages/agent-provider-tangle/CHANGELOG.md b/packages/agent-provider-tangle/CHANGELOG.md index fead40e..03a76b5 100644 --- a/packages/agent-provider-tangle/CHANGELOG.md +++ b/packages/agent-provider-tangle/CHANGELOG.md @@ -1,5 +1,15 @@ # @tangle-network/agent-provider-tangle +## 1.1.7 + +### Patch Changes + +- Accept complete Sandbox tool output and assistant text within the shared bounded event-content contract. + Validate the complete frame's serialized UTF-8 size while retaining identifier, usage metadata, and structural limits. + This fixes stream failures caused by applying a 16 KiB metadata string limit to backend content. +- Updated dependencies + - @tangle-network/agent-interface@2.6.1 + ## 1.1.6 ### Patch Changes diff --git a/packages/agent-provider-tangle/README.md b/packages/agent-provider-tangle/README.md index 8a78818..7423d31 100644 --- a/packages/agent-provider-tangle/README.md +++ b/packages/agent-provider-tangle/README.md @@ -102,6 +102,11 @@ Reconstruct an exact session with `environment.session(reference.id, { controlRe Result, replay, and cancel operations select that exact execution instead of whichever execution most recently changed the shared session. Session status with an exact control reference reports a state only when the payload names that execution; a payload bound to a different or unnamed execution reports `unknown`. +Stream frames can contain complete tool output and cumulative assistant text. +The adapter bounds each complete serialized frame to 1 MiB of UTF-8, including keys and JSON escaping. +It retains accepted content without truncation in the event data and original provider event. +Identifiers, usage metadata, and structural JSON limits retain their existing bounds. + ## Two capability documents Capabilities are derived in two stages, and the two stages answer different questions. diff --git a/packages/agent-provider-tangle/package.json b/packages/agent-provider-tangle/package.json index b6b986d..4ad1764 100644 --- a/packages/agent-provider-tangle/package.json +++ b/packages/agent-provider-tangle/package.json @@ -1,6 +1,6 @@ { "name": "@tangle-network/agent-provider-tangle", - "version": "1.1.6", + "version": "1.1.7", "description": "AgentEnvironmentProvider adapter for Tangle sandboxes", "type": "module", "license": "MIT", @@ -106,7 +106,7 @@ "test": "vitest run src" }, "dependencies": { - "@tangle-network/agent-interface": "^2.4.0" + "@tangle-network/agent-interface": "^2.6.1" }, "peerDependencies": { "@tangle-network/sandbox": ">=0.34.6 <1.0.0" diff --git a/packages/agent-provider-tangle/src/tangle-events.test.ts b/packages/agent-provider-tangle/src/tangle-events.test.ts new file mode 100644 index 0000000..8eb963d --- /dev/null +++ b/packages/agent-provider-tangle/src/tangle-events.test.ts @@ -0,0 +1,134 @@ +import { readFileSync } from "node:fs"; +import type { SandboxEvent } from "@tangle-network/sandbox"; +import { AgentTurnResultSchema, type AgentEnvironmentEvent } from "@tangle-network/agent-interface/environment-provider"; +import { describe, expect, it } from "vitest"; +import { createTangleProvider } from "./index.js"; +import { assertBoundedJson } from "./tangle-contract-safety.js"; +import { environmentEventFromSandboxEvent } from "./tangle-events.js"; +import { controlRefForTurn, retainedDeployment } from "./retained-control-test-helpers.js"; +import type { SandboxSessionLike } from "./index.js"; +import { validatedSandboxPromptResult } from "./tangle-prompt.js"; + +const recordedOutput = readFileSync( + new URL("../test/fixtures/codex-tool-output-redacted.txt", import.meta.url), + "utf8", +); + +function toolEvent(output: string): SandboxEvent { + return { + type: "message.part.updated", + id: "event-tool-completed", + data: { + executionId: "execution-1", + sessionId: "session-1", + part: { + id: "item-1", sessionID: "session-1", messageID: "message-1", + type: "tool", callID: "item-1", tool: "shell", + state: { + status: "completed", input: { command: "read documentation" }, + output, time: { start: 1, end: 2 }, + }, + }, + }, + }; +} + +const bound = { executionId: "execution-1", sessionId: "session-1" }; + +describe("Sandbox stream event content", () => { + it("preserves retained long Codex tool output through the provider stream", async () => { + expect(recordedOutput.length).toBe(17_511); + const event = toolEvent(recordedOutput); + const provider = createTangleProvider({ + client: { + async create() { + return { + id: "sandbox-fixture", status: "running", + async *streamPrompt() { + yield event; + yield { type: "done", id: "event-done", data: { status: "success", usage: { inputTokens: 21, outputTokens: 3 } } }; + }, + }; + }, + }, + }); + const environment = await provider.create({ profile: { name: "fixture" } }); + const events: AgentEnvironmentEvent[] = []; + for await (const entry of environment.stream({ prompt: "inspect documentation" })) events.push(entry); + expect(events).toHaveLength(2); + expect(events[0]).toMatchObject({ id: event.id, data: event.data, normalized: { type: event.type, part: event.data.part } }); + expect(events[0]?.providerEvent).toEqual(event); + expect(events[1]?.usage).toEqual({ inputTokens: 21, outputTokens: 3 }); + expect(() => AgentTurnResultSchema.parse({ text: "done", success: true, events })).not.toThrow(); + }); + + it("preserves cumulative assistant text and terminal content", () => { + const part = { id: "text-1", sessionID: "session-1", messageID: "message-1", type: "text", text: recordedOutput }; + const textEvent = environmentEventFromSandboxEvent({ type: "message.part.updated", data: { part } }); + expect(textEvent.normalized).toEqual({ type: "message.part.updated", part }); + const terminal = environmentEventFromSandboxEvent({ type: "done", data: { finalText: recordedOutput } }); + expect(terminal.data.finalText).toBe(recordedOutput); + expect(terminal.usage).toBeUndefined(); + expect(() => AgentTurnResultSchema.parse({ text: recordedOutput, success: true, events: [textEvent, terminal] })).not.toThrow(); + }); + + it("retains a long final response through exact-session result readback", async () => { + const nativeSession: SandboxSessionLike = { + id: "session-result", + async status() { return { status: "completed" }; }, + async *events() {}, + async prompt() { throw new Error("readback must not start a turn"); }, + async interrupt() { throw new Error("readback must not interrupt a turn"); }, + async result(options) { + return { + success: true, status: "success", executionId: options?.executionId, + response: recordedOutput, durationMs: 1, + usage: { inputTokens: 21, outputTokens: 3 }, + }; + }, + }; + const box = retainedDeployment({ + id: "sandbox-result", async *streamPrompt() {}, session: () => nativeSession, + }); + const provider = createTangleProvider({ client: { async create() { return box; } } }); + const environment = await provider.create({ profile: { name: "fixture" } }); + const controlRef = controlRefForTurn({ prompt: "research", turnId: "turn-result" }, box.id, nativeSession.id); + const session = environment.session!(nativeSession.id, { controlRef }); + const result = await session.result(); + expect(result.text).toBe(recordedOutput); + expect(result.usage).toEqual({ inputTokens: 21, outputTokens: 3 }); + expect(() => AgentTurnResultSchema.parse(result)).not.toThrow(); + }); + + it("counts the complete serialized UTF-8 frame, including escaping and keys", () => { + const empty = toolEvent(""); + const remaining = 1024 * 1024 - Buffer.byteLength(JSON.stringify(empty), "utf8"); + expect(() => environmentEventFromSandboxEvent(toolEvent("x".repeat(remaining)), bound)).not.toThrow(); + expect(() => environmentEventFromSandboxEvent(toolEvent("x".repeat(remaining + 1)), bound)).toThrow(/JSON content bound/); + for (const value of ["😀", "\u0000"]) { + const escapedBytes = Buffer.byteLength(JSON.stringify(value), "utf8") - 2; + const fits = value.repeat(Math.floor(remaining / escapedBytes)); + expect(() => environmentEventFromSandboxEvent(toolEvent(fits), bound)).not.toThrow(); + expect(() => environmentEventFromSandboxEvent(toolEvent(fits + value), bound)).toThrow(/JSON content bound/); + } + const manyStrings = { type: "raw", data: { first: "x".repeat(600_000), second: "x".repeat(600_000) } }; + expect(() => environmentEventFromSandboxEvent(manyStrings)).toThrow(/JSON content bound/); + }); + + it("retains identity, JSON-shape, and metadata restrictions", () => { + expect(() => environmentEventFromSandboxEvent(toolEvent(recordedOutput), { ...bound, sessionId: "foreign" })).toThrow(/different sessionId/); + expect(() => environmentEventFromSandboxEvent({ type: "raw", data: { value: Infinity } })).toThrow(/JSON content bound/); + expect(() => environmentEventFromSandboxEvent({ type: "raw", data: { value: new Array(1025).fill(null) } })).toThrow(/JSON content bound/); + const cyclic: Record = {}; + cyclic.self = cyclic; + expect(() => environmentEventFromSandboxEvent({ type: "raw", data: cyclic })).toThrow(/JSON content bound/); + expect(() => environmentEventFromSandboxEvent({ type: "raw", data: { value: new Date() } })).toThrow(/JSON content bound/); + expect(() => assertBoundedJson(recordedOutput)).toThrow(/JSON bound/); + expect(() => environmentEventFromSandboxEvent({ type: "raw", data: { usage: { inputTokens: 2, outputTokens: 3, note: recordedOutput } } })).toThrow(/JSON bound/); + expect(() => environmentEventFromSandboxEvent({ type: "raw", data: { usage: { inputTokens: 2, outputTokens: 3 }, costUsd: -1 } })).toThrow(/result cost/); + const result = { success: true, status: "success" as const, durationMs: 1, response: recordedOutput }; + expect(() => validatedSandboxPromptResult({ ...result, response: "x".repeat(1024 * 1024) })).toThrow(/JSON bound/); + expect(() => validatedSandboxPromptResult({ ...result, traceId: recordedOutput })).toThrow(/JSON bound/); + expect(() => validatedSandboxPromptResult({ ...result, usage: { inputTokens: 2, outputTokens: -1 } })).toThrow(/output token count/); + }); +}); diff --git a/packages/agent-provider-tangle/src/tangle-events.ts b/packages/agent-provider-tangle/src/tangle-events.ts index 511095a..d932a0e 100644 --- a/packages/agent-provider-tangle/src/tangle-events.ts +++ b/packages/agent-provider-tangle/src/tangle-events.ts @@ -1,11 +1,11 @@ import type { SandboxEvent } from "@tangle-network/sandbox"; import { CanonicalStreamEventSchema, + boundedEventContentRecordSchema, type StreamEvent, type StreamStatus, } from "@tangle-network/agent-interface"; import type { AgentEnvironmentEvent } from "@tangle-network/agent-interface/environment-provider"; -import { assertBoundedJson } from "./tangle-contract-safety.js"; import { optionalNonEmptyString } from "./tangle-environment-values.js"; import { tokenUsageFromData } from "./tangle-result-values.js"; @@ -217,8 +217,7 @@ export function environmentEventFromSandboxEvent( throw new Error("Tangle Sandbox event contained an invalid event id"); } const data = record.data as Record; - assertBoundedRecord(data); - assertBoundedJson(record); + assertBoundedRecord(record); if (Object.prototype.hasOwnProperty.call(data, "contextTransferReceipt")) { throw new Error( "Tangle Sandbox emitted an unsolicited context transfer receipt", @@ -439,54 +438,8 @@ function detailFromSandboxData( } function assertBoundedRecord(value: Record): void { - if (Object.keys(value).length > 256) { - throw new Error("Tangle Sandbox event data has too many fields"); - } - const pending: Array<{ value: unknown; depth: number; leave?: boolean }> = [ - { value, depth: 0 }, - ]; - const ancestors = new Set(); - let nodes = 0; - while (pending.length > 0) { - const item = pending.pop(); - if (!item) continue; - nodes += 1; - if (nodes > 8_192) throw new Error("Tangle Sandbox event data has too many JSON nodes"); - const current = item.value; - if (item.leave) { - ancestors.delete(current as object); - continue; - } - if (current === null || typeof current === "boolean") continue; - if (typeof current === "string" || typeof current === "number") { - if (typeof current === "string" && current.length > 16_384) { - throw new Error("Tangle Sandbox event data exceeded its string bound"); - } - if (typeof current === "number" && !Number.isFinite(current)) { - throw new Error("Tangle Sandbox event data contained a non-finite number"); - } - continue; - } - if (typeof current !== "object" || item.depth >= 16 || ancestors.has(current)) { - throw new Error("Tangle Sandbox event data exceeded its JSON bound"); - } - ancestors.add(current); - pending.push({ value: current, depth: item.depth, leave: true }); - if (Array.isArray(current)) { - if (current.length > 1_024) { - throw new Error("Tangle Sandbox event data has too many array entries"); - } - for (const entry of current) pending.push({ value: entry, depth: item.depth + 1 }); - continue; - } - if (Object.getPrototypeOf(current) !== Object.prototype && Object.getPrototypeOf(current) !== null) { - throw new Error("Tangle Sandbox event data must be plain JSON"); - } - const keys = Object.keys(current); - if (keys.length > 256) throw new Error("Tangle Sandbox event map is too large"); - for (const key of keys) { - if (key.length > 512) throw new Error("Tangle Sandbox event key is too long"); - pending.push({ value: (current as Record)[key], depth: item.depth + 1 }); - } + const parsed = boundedEventContentRecordSchema.safeParse(value); + if (!parsed.success) { + throw new Error("Tangle Sandbox event exceeded its JSON content bound", { cause: parsed.error }); } } diff --git a/packages/agent-provider-tangle/src/tangle-prompt.ts b/packages/agent-provider-tangle/src/tangle-prompt.ts index 31f524b..e2461d5 100644 --- a/packages/agent-provider-tangle/src/tangle-prompt.ts +++ b/packages/agent-provider-tangle/src/tangle-prompt.ts @@ -13,6 +13,7 @@ import type { } from "@tangle-network/agent-interface"; import { agentProfileSchema, + boundedEventContentRecordSchema, AgentExactRunControlRefSchema, AgentTurnInputSchema, ContextTransferReceiptSchema, @@ -577,7 +578,15 @@ export function validatedSandboxPromptResult( value !== undefined || !SANDBOX_OPTIONAL_RESULT_FIELDS.has(field), ), ); - assertBoundedJson(record); + const content = boundedEventContentRecordSchema.safeParse(record); + if (!content.success) { + throw new Error("Tangle prompt result exceeded its JSON bound", { cause: content.error }); + } + // Response text is content; all other fields retain their metadata limits. + assertBoundedJson(Object.fromEntries( + Object.entries(record).filter(([field]) => + field !== "response" && field !== "text" && field !== "finalText"), + )); if (typeof record.success !== "boolean") { throw new Error("Tangle prompt result omitted its success status"); } diff --git a/packages/agent-provider-tangle/src/tangle-result-values.ts b/packages/agent-provider-tangle/src/tangle-result-values.ts index a99b17c..afc83b2 100644 --- a/packages/agent-provider-tangle/src/tangle-result-values.ts +++ b/packages/agent-provider-tangle/src/tangle-result-values.ts @@ -25,7 +25,13 @@ export function execResultFromSandboxExecResult(result: SandboxExecResult | unde }; } export function tokenUsageFromData(data: Record): TokenUsage | undefined { - assertBoundedJson(data); + // The enclosing event or result has its own content bound. + // Apply metadata limits only to fields used to compute usage. + assertBoundedJson(Object.fromEntries( + ["usage", "tokenUsage", "costUsd", "totalCostUsd"] + .filter((field) => Object.hasOwn(data, field)) + .map((field) => [field, data[field]]), + )); if ( data.usage !== undefined && (!data.usage || typeof data.usage !== "object" || Array.isArray(data.usage)) diff --git a/packages/agent-provider-tangle/test/fixtures/codex-tool-output-redacted.md b/packages/agent-provider-tangle/test/fixtures/codex-tool-output-redacted.md new file mode 100644 index 0000000..2384dfa --- /dev/null +++ b/packages/agent-provider-tangle/test/fixtures/codex-tool-output-redacted.md @@ -0,0 +1,15 @@ +# Long tool-output regression + +This fixture reconstructs a Sandbox tool-part event from retained Codex output. +It is not the unavailable original failing cloud frame. + +The native output was recorded on 2026-09-09 at 19:48:58.765Z during capability inspection. +Its original JSONL record SHA-256 is `1cab0085af487dc6d68b891253dd4763f10fb67752366f06e903646efe069138`. +Every non-whitespace code point was replaced with `x`; whitespace positions remain unchanged. +The sanitized output contains 17511 characters. +No prompts, paths, source content, credentials, or native identity are retained. + +The test wraps this output in the completed `message.part.updated` tool shape. +That shape is emitted by `sdk-provider-codex/src/app-server-items.ts` in agent-dev-container. +The Discovery failure record reports the same adapter string-bound error. +Its exact Sandbox frame was not retained, and the later sandbox lookup returned null. diff --git a/packages/agent-provider-tangle/test/fixtures/codex-tool-output-redacted.txt b/packages/agent-provider-tangle/test/fixtures/codex-tool-output-redacted.txt new file mode 100644 index 0000000..fc744e2 --- /dev/null +++ b/packages/agent-provider-tangle/test/fixtures/codex-tool-output-redacted.txt @@ -0,0 +1,253 @@ +xxxxx xxx xxxxxx +xxxx xxxxx xxxxxx xxxxxxx +xxxxxxx xxxxxx xxxx xxxx x +xxxxxxxx xxxxx xxxxxx xxxx +xxxxxxx +xxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xx xxx xxxxx xxx xxxx xxxxxxx xxxxxxxxxxxxxxx xxxxxxxx xxxx xxxx xxx +xxxx xxx xxxxxxxx xxxxxxx xxxxxxx xxxxxx xxxxx xxxxxxxxxxx xxxxxx xxxxxxx xxx xxxxxxx xxxxxxx xxxxxx xxxxxxxx xxxxx xxxxxxxx xxxxxx xxxxxxxxx xxx xxxxxxxx xxxxxxxx xxxx xxxxxxxx xxxx xxxxxxxxxx xxxxxxxxxxxx + +xxxxxxx xxx xxxxxx xxxxxxx xxx xxxxxxxx xxxxxxxxxxxx xxxxxx xxxxxxxxxxx xxxxxx xxxxxxxx xxxxxx xxxxxx xxxxxxxxxxx xxxxxxx xxxxxxxxx xxxxxxxxxxx xxxxxxxx xxx xxxxxxxx xx xxxxxxxxxxx xxxxxxx xxxxxxxxxxxx xxx xxxx xxxxxxxxxxx + +xxxxxx xxx xxxxx xxxxxxxx xxxxx xx xxxxxx xxxxxxxxx xxxx xxxxxxxx xxxxxxxxxxx xxxxx xxxxxxxxxxxxx xxxxxxxx xxxxxxxxxxx xxx xxxxxxxx xxxxxxxxx xxx xxxxxx xx xxxxxxxxx xxxxxx xxxxxxxx xxxxxxx xxx xxxxxxxx xxx xxxxxxxxx xxx xxxxx xxx xxxxxxxx xxxxxxxx xx xxxx xxxxxxxx + +xxxxxx xxxxx xxxxx xxxxxxxx xxxx xxxx xxx xxxxxxxxxxx xxxxx xxxxxxxxxxx xxxxx xxxx xxxxxxxxxxxx xxxx x xxxxx xxxx xxx xx xxxx xxxxxxxxxxxxxx xxxxxxxxx xxx xxxxx xxxxxxxxxxxxxxxxx xxx xxxxxxxxxxxxxxxxxxxx xxxxxx xx xxxxx xxxxxxxxxxxxx xxxxxx + +xxx xxx xxxxxxxxxxxxxxxxxxx xxxxx xxxx xxxxxx xxxxxxxx xxx xxxxxx xxx xxxxxxxxx xxxxx xxx xxxxxxx xx xxxxxx xx xxxxxxx xxxxxxxx xxxxx xxx xx xxxxxxxx + +xxxxx xxxxxxxx xxxx xxxxx xxxxxxx xx xxxxxxxxx xx xxxxxxxxxx xxxxxx xxxxxxxxxxxx xxx x xxxxxxxxxx xxxxxxxxxx xxxxxxxxx xx xxxxxxxxxxxxxxx xxxxx xxxxxx xxxxxxxxxxx xxxxx xx xxxxxxxxxxx xxxxxxx xxxxxxxxx xx xxxxxxx xxxxx + +xxxxxxxx xxxxxxxx xxxx xxx xxxxxx xxxxxxxxx xxxxxxxx xxxxxxx xxxxx xxxxxxx xxxxxxxxxx xxxxxxxxxxxxx xxxxxxxxxxxx xxxxxxxxxx xxxxxxxxxx xxx xxxxx xxxx xxxxxxx xxxxxxx xxxxxx xxxxxxx xxxxxxx xxx xxxxxxxx xxxxxxx xxxxxx xxxxxxxx + +xxxxx xxxxxxxxxxxxx xxxxx xxxxxx xxxxxx xxxxxxx xx xxxx xxxx xxxxxxxx xxxxxxx xxxxxxxx xxxxxx xxxxxxxx xxxxxxxx xxx xxxxxxxxxx xxxxxxx xxxx xxxx xx xx xxxxxxxxxxxxx xxxxxxxxxx xxxxxxx x xxxxxxxx xxx xxxx xxxxxxx xxx xxxx xxxxxxxxx xx x xxxxxxxxxxxx xxxxxxxx xx xxxxxxxxxx xxxxxxxxx + +xx xxxxxxxxx xx +xxx xx +xxxxx xx +xxxxxxxxxxx x xxxx xxxxx xxx xxx x xxxxx x +xxxxxxxxxxx xxx xxxx xxxxx xxxxx xxx x xxxxx xx +xxxxxxxxxxx x xxxx xxxxx xx xxx x xxxxx xxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx x xxxxx xxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxxxx xxx x xxxxx xxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx x xxxxx xxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx x xxxxx xxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx x xxxxx xxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx x xxxxx xxxxxxxxx +xxx xxxxx +xxxxx x +xxxxxxxxxxx xxx xxxx xxxxx xxxxx xxx x xxxxx x +xxxxxxxxxxx xx xxxx xxxxx xxxx xxx x xxxxx xx +xxxxxxxxxxx x xxxx xxxxx xxx xxx x xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx x xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx x xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx x xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx x xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx x xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx x xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx x xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx x xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxx xxx xxxx xxxxx xxxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxx +xxx xxxxxxxx +xxxxx xxx +xxxxxxxxxxx xx xxxx xxxxx xxxx xxx x xxxxx x +xxxxxxxxxx xxx xxxx xxxxx xxxx xxx x xxxxx xx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx x xxxxx xxxxxx +xxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xx xxx x xxxxx xxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx x xxxxx xxxx +xxxxxxxxxxx x xxxx xxxxx xxxx xxx x xxxxx xxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xx xxx x xxxxx xxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxxx xxx x xxxxx xxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxxx xxx xx xxxxx xxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxxx xxx x xxxxx xxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxxxx xxx x xxxxx xxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xx xxx x xxxxx xxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxxx xxx x xxxxx xxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxxxx xxx x xxxxx xxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxxx xxx x xxxxx xxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxxx xxx x xxxxx xxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xx xxx xx xxxxx xxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx x xxxxx xxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxxx xxx xx xxxxx xxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xx xxx x xxxxx xxx +xxxxxxxxxxx x xxxx xxxxx xx xxx x xxxxx xxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx x xxxxx xxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx x xxxxx xxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx x xxxxx xxxxxxxx +xxxxxxxxxx xxx xxxx xxxxx xxxx xxx xx xxxxx xxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx x xxxxx xxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx x xxxxx xxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxx +xxxxxxxxxxx xxx xxxx xxxxx xxxx xxx x xxxxx xxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx x xxxxx xx +xxxxxxxxxxx x xxxx xxxxx xxx xxx x xxxxx xxxxxx +xxxxxxxxxxx x xxxx xxxxx xx xxx xx xxxxx xxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxxx xxx x xxxxx xxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx x xxxxx xxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx x xxxxx xxx +xxxxxxxxxxx xxxx xxxx xxxxx xxxxx xxx x xxxxx xxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx x xxxxx xxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxxxx xxx x xxxxx xxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx x xxxxx xxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xxx xxxx xxxxx xxxx xxx x xxxxx xxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxxx xxx x xxxxx xxxxxxxx +xxxxxxxxxxx xxx xxxx xxxxx xxxxx xxx x xxxxx xxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxxx xxx x xxxxx xxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx x xxxxx xxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx x xxxxx xxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxxx xxx x xxxxx xxxx +xxxxxxxxxxx x xxxx xxxxx xx xxx xx xxxxx xxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx x xxxxx xxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx x xxxxx xxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xx xxx xx xxxxx xxxxxxxxxxxxxxx +xxx xxxxxxxxxxx +xxxxx xxx +xxxxxxxxxx xxx xxxx xxxxx xxxx xxx x xxxxx x +xxxxxxxxxxx xxx xxxx xxxxx xxxx xxx x xxxxx xx +xxxxxxxxxxx x xxxx xxxxx xxxxx xxx x xxxxx xxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xx xxx xx xxxxx xxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx x xxxxx xxxxxxx +xxxxxxxxxxx x xxxx xxxxx xx xxx xx xxxxx xxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xx xxx x xxxx xxxxxxxxx +xxxxxxxxxx xx xxxx xxxxx xxx xxx x xxxx xxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx x xxxxx xxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxxxx xxx x xxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxxx xxx x xxxxx xxxxxxxx +xxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxx +xxxxxxxxxx xx xxxx xxxxx xxx xxx x xxxxx xxx +xxxxxxxxxxx x xxxx xxxxx xx xxx x xxxx xxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxxx xxx x xxxxx xxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxxx xxx x xxxxx xxxxxxxxx +xxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxx xxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxxx xxx x xxxxx xxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxxx xxx x xxxxx xxxxxxxxxxxxxxxxxxx +xxxxxxxxxx xx xxxx xxxxx xxx xxx x xxxxx xxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxxx xxx x xxxxx xxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx x xxxxx xxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx x xxxxx xxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx x xxxxx xxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx x xxxxx xxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx x xxxxx xxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx x xxxxx xxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxxx xxx x xxxxx xxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx x xxxxx xxxxxxxxx +xxxxxxxxxx xx xxxx xxxxx xxx xxx x xxxx xxxxxxxx +xxxxxxxxxx xx xxxx xxxxx xxx xxx x xxxx xxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxxx xxx x xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxx +xxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxx xxxxxxxxxxxxxxxxxxx +xxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxx +xxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxx xxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxx xxx +xxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxx xxxxxxxxxxxx +xxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxx xx +xxxxxxxxxx xx xxxx xxxxx xxxx xxx xx xxxxx xxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxx xxxxxxxxxxxxxx +xxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxx xxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx x xxxx xxxxxxxxxxxxxxxx +xxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxxx xxx x xxxxx xxxxxxxxx +xxxxxxxxxx xx xxxx xxxxx xxxx xxx x xxxxx xxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxx xx xxxx xxxxx xxxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx x xxxx xxxxx xxx xxx x xxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxx xxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx x xxxxx xxxxx +xxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxx xx xxxx xxxxx xxxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxx +xxxxxxxxxx xx xxxx xxxxx xxxx xxx xx xxxx xxxxxxxxxxxxx +xxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxx xxxxx +xxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxx xxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxx xxxxxxxxxxxxxxxxxxx +xxxxxxxxxx x xxxx xxxxx xxx xxx xx xxxx xxxxxxxxxxxxxxx +xxxxxxxxxx x xxxx xxxxx xx xxx x xxxx xxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx x xxxx xxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxxx xxx x xxxxx xxxxxxxxxxxxxx +xxxxxxxxxx xx xxxx xxxxx xxxx xxx x xxxxx xxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx x xxxx xxxxxxxxxxxxx +xxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx x xxxxx xxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxxx xxx x xxxxx xxxxxxxxxxxxx +xxxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxx xx xxxx xxxxx xxx xxx xx xxxxx xxxxxxxxxxxxxxxxxxxxxxxxx