@tangle-network/sdk-provider-cli-base 0.0.0-stage → 0.13.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/LICENSE ADDED
@@ -0,0 +1,11 @@
1
+ Copyright (c) 2025 Tangle Network
2
+
3
+ All rights reserved.
4
+
5
+ This software and associated documentation files (the "Software") are proprietary
6
+ and confidential. No part of this Software may be reproduced, distributed, or
7
+ transmitted in any form or by any means, including photocopying, recording, or
8
+ other electronic or mechanical methods, without the prior written permission of
9
+ Tangle Network.
10
+
11
+ For licensing inquiries, contact: hello@tangle.tools
@@ -0,0 +1,79 @@
1
+ import { i as PreparedAttachment } from "../prompt-input-BHn_1z69.mjs";
2
+ import { StreamEvent } from "@tangle-network/agent-interface";
3
+
4
+ //#region src/conformance/attachments.d.ts
5
+ /**
6
+ * Asserts an adapter delivered `attachment` as a native input part: one of
7
+ * its `path`, `base64`, or `url` appears verbatim in argv or stdin, AND the
8
+ * fallback marker appears in neither — a native delivery that also leaves
9
+ * the marker behind would describe the same image to the model twice, once
10
+ * as a real input part and once as a dangling text reference.
11
+ */
12
+ declare function assertImageForwardedNatively(input: {
13
+ args: string[];
14
+ stdin?: string;
15
+ attachment: PreparedAttachment;
16
+ }): void;
17
+ /**
18
+ * Asserts an adapter left `attachment` on the marker-only fallback path: its
19
+ * `[Image: <ref>]` / `[File: <ref>]` marker text appears in argv or stdin —
20
+ * i.e. it locks that the adapter actually DELIVERS the marker-bearing prompt
21
+ * to the CLI, not merely that the marker exists somewhere upstream of
22
+ * `buildExecArgs`. This is the contract for every adapter that does not opt
23
+ * the attachment's kind into `nativeAttachmentKinds()`: a future native
24
+ * opt-in that suppresses the marker without ever wiring up native delivery
25
+ * reds this instead of silently downgrading to an undelivered fallback.
26
+ */
27
+ declare function assertImageMarkerFallback(input: {
28
+ args: string[];
29
+ stdin?: string;
30
+ attachment: PreparedAttachment;
31
+ }): void;
32
+ //#endregion
33
+ //#region src/conformance/scenarios.d.ts
34
+ type ScenarioName = /** One reasoning block, delivered however the provider natively does. */"reasoning-single-block" /** Two distinct reasoning blocks → two part ids, correct texts, in order. */ | "reasoning-two-blocks" /** One block streamed as increments → one id, append-only deltas. */ | "reasoning-streamed-deltas" /** Same content delivered twice (stream + final message) → no duplicate parts. */ | "reasoning-snapshot-replay" /** Plain text output → one text part id, correct final text. */ | "text-single-part" /** Text and reasoning interleaved → ids never collide across types. */ | "text-interleaved-reasoning" /** A tool call → tool part with callID and full-snapshot input. */ | "tool-call-basic" /** A write-tool call surfaces a file part with a stable id and URL. */ | "file-part-from-write-tool";
35
+ interface ScenarioExpectation {
36
+ /** Final text of each distinct reasoning block, in emission order. */
37
+ reasoningBlockTexts?: string[];
38
+ /**
39
+ * Every reasoning update must carry a delta (default true). Set false only
40
+ * for adapters whose provider genuinely re-snapshots without increments.
41
+ */
42
+ reasoningDeltasRequired?: boolean;
43
+ /** Final text-part content. */
44
+ finalText?: string;
45
+ /** Tool calls that must appear, with their final full-snapshot input. */
46
+ toolCalls?: Array<{
47
+ callID: string;
48
+ input: unknown;
49
+ }>;
50
+ /** File parts that must appear, keyed by final {url, mediaType}. */
51
+ fileParts?: Array<{
52
+ url: string;
53
+ mediaType?: string;
54
+ }>;
55
+ }
56
+ interface ScenarioResult {
57
+ events: StreamEvent[];
58
+ expected: ScenarioExpectation;
59
+ }
60
+ /** Scenario-level checks on top of `assertStreamInvariants`. */
61
+ declare function assertScenarioExpectations(name: ScenarioName, result: ScenarioResult): void;
62
+ //#endregion
63
+ //#region src/conformance/binding.d.ts
64
+ type ScenarioRunner = () => ScenarioResult | Promise<ScenarioResult>;
65
+ interface ConformanceBinding {
66
+ /** Adapter name, for test titles and failure messages. */
67
+ adapter: string;
68
+ /** Only the scenarios this provider's event shape supports. */
69
+ scenarios: Partial<Record<ScenarioName, ScenarioRunner>>;
70
+ }
71
+ /** Typed entries of a binding's declared scenarios. */
72
+ declare function conformanceScenarioEntries(binding: ConformanceBinding): Array<[ScenarioName, ScenarioRunner]>;
73
+ //#endregion
74
+ //#region src/conformance/invariants.d.ts
75
+ /** Replays a trace and throws listing every invariant violation found. */
76
+ declare function assertStreamInvariants(events: StreamEvent[]): void;
77
+ //#endregion
78
+ export { type ConformanceBinding, type ScenarioExpectation, type ScenarioName, type ScenarioResult, type ScenarioRunner, assertImageForwardedNatively, assertImageMarkerFallback, assertScenarioExpectations, assertStreamInvariants, conformanceScenarioEntries };
79
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1,192 @@
1
+ //#region src/conformance/attachments.ts
2
+ /** True when `needle` appears verbatim in one argv element or in stdin.
3
+ * Checked per-element (never joined) so a match can never straddle two
4
+ * unrelated argv entries. */
5
+ function appearsInArgsOrStdin(input, needle) {
6
+ if (input.args.some((arg) => arg.includes(needle))) return true;
7
+ return (input.stdin ?? "").includes(needle);
8
+ }
9
+ /**
10
+ * Asserts an adapter delivered `attachment` as a native input part: one of
11
+ * its `path`, `base64`, or `url` appears verbatim in argv or stdin, AND the
12
+ * fallback marker appears in neither — a native delivery that also leaves
13
+ * the marker behind would describe the same image to the model twice, once
14
+ * as a real input part and once as a dangling text reference.
15
+ */
16
+ function assertImageForwardedNatively(input) {
17
+ const { attachment } = input;
18
+ const candidates = [
19
+ attachment.path,
20
+ attachment.base64,
21
+ attachment.url
22
+ ].filter((value) => value !== void 0);
23
+ if (candidates.length === 0) throw new Error("assertImageForwardedNatively: attachment carries none of path/base64/url — nothing to check native delivery against");
24
+ if (!candidates.some((candidate) => appearsInArgsOrStdin(input, candidate))) throw new Error(`assertImageForwardedNatively: none of the attachment's path/base64/url (${candidates.length} candidate(s)) appear in argv or stdin — the attachment was not forwarded natively`);
25
+ if (appearsInArgsOrStdin(input, attachment.marker)) throw new Error(`assertImageForwardedNatively: fallback marker ${JSON.stringify(attachment.marker)} found in argv or stdin alongside the native delivery — a native path must not also leave the text marker behind`);
26
+ }
27
+ /**
28
+ * Asserts an adapter left `attachment` on the marker-only fallback path: its
29
+ * `[Image: <ref>]` / `[File: <ref>]` marker text appears in argv or stdin —
30
+ * i.e. it locks that the adapter actually DELIVERS the marker-bearing prompt
31
+ * to the CLI, not merely that the marker exists somewhere upstream of
32
+ * `buildExecArgs`. This is the contract for every adapter that does not opt
33
+ * the attachment's kind into `nativeAttachmentKinds()`: a future native
34
+ * opt-in that suppresses the marker without ever wiring up native delivery
35
+ * reds this instead of silently downgrading to an undelivered fallback.
36
+ */
37
+ function assertImageMarkerFallback(input) {
38
+ const { attachment } = input;
39
+ if (!appearsInArgsOrStdin(input, attachment.marker)) throw new Error(`assertImageMarkerFallback: fallback marker ${JSON.stringify(attachment.marker)} not found in argv or stdin`);
40
+ }
41
+ //#endregion
42
+ //#region src/conformance/binding.ts
43
+ /** Typed entries of a binding's declared scenarios. */
44
+ function conformanceScenarioEntries(binding) {
45
+ return Object.entries(binding.scenarios).filter((entry) => entry[1] !== void 0);
46
+ }
47
+ //#endregion
48
+ //#region src/conformance/invariants.ts
49
+ const STATUS_RANK = {
50
+ pending: 0,
51
+ running: 1,
52
+ completed: 2,
53
+ failed: 2,
54
+ error: 2
55
+ };
56
+ function isPartUpdate(event) {
57
+ return event.type === "message.part.updated";
58
+ }
59
+ /** Replays a trace and throws listing every invariant violation found. */
60
+ function assertStreamInvariants(events) {
61
+ const errors = [];
62
+ const tracks = /* @__PURE__ */ new Map();
63
+ const textIdsByMessage = /* @__PURE__ */ new Map();
64
+ events.forEach((event, index) => {
65
+ if (!isPartUpdate(event)) return;
66
+ const part = event.part;
67
+ const at = `event[${index}] part "${part.id}"`;
68
+ if (!part.id) {
69
+ errors.push(`event[${index}]: part id is empty`);
70
+ return;
71
+ }
72
+ const prev = tracks.get(part.id);
73
+ if (prev && prev.type !== part.type) {
74
+ errors.push(`${at}: type changed "${prev.type}" -> "${part.type}"`);
75
+ return;
76
+ }
77
+ if (prev && prev.messageID !== part.messageID) errors.push(`${at}: messageID changed "${prev.messageID}" -> "${part.messageID}"`);
78
+ const track = prev ?? {
79
+ type: part.type,
80
+ messageID: part.messageID,
81
+ text: "",
82
+ sawDelta: false
83
+ };
84
+ if (part.type === "text" || part.type === "reasoning") {
85
+ const text = part.text;
86
+ if (event.delta !== void 0) {
87
+ if (track.text + event.delta !== text) errors.push(`${at}: delta is not an append-only suffix (prev ${JSON.stringify(track.text)} + delta ${JSON.stringify(event.delta)} !== text ${JSON.stringify(text)})`);
88
+ track.sawDelta = true;
89
+ } else {
90
+ if (track.sawDelta && text !== track.text) errors.push(`${at}: mixed update modes — earlier updates carried a delta, this one changes text without one`);
91
+ if (!text.startsWith(track.text)) errors.push(`${at}: snapshot rewrites earlier content (prev ${JSON.stringify(track.text)}, got ${JSON.stringify(text)})`);
92
+ }
93
+ track.text = text;
94
+ if (part.type === "text") {
95
+ const ids = textIdsByMessage.get(part.messageID) ?? /* @__PURE__ */ new Set();
96
+ ids.add(part.id);
97
+ textIdsByMessage.set(part.messageID, ids);
98
+ if (ids.size > 1) errors.push(`${at}: message "${part.messageID}" has ${ids.size} text part ids (expected one)`);
99
+ }
100
+ }
101
+ if (part.type === "tool") {
102
+ if (!part.callID) errors.push(`${at}: tool part is missing callID`);
103
+ else if (track.callID !== void 0 && track.callID !== part.callID) errors.push(`${at}: callID changed "${track.callID}" -> "${part.callID}"`);
104
+ track.callID = part.callID;
105
+ if (part.state?.input === void 0) errors.push(`${at}: tool update is missing the full input snapshot`);
106
+ const rank = STATUS_RANK[part.state?.status];
107
+ if (rank === void 0) errors.push(`${at}: unknown tool status "${part.state?.status}"`);
108
+ else {
109
+ if (track.statusRank !== void 0 && rank < track.statusRank) errors.push(`${at}: tool status regressed to "${part.state.status}"`);
110
+ track.statusRank = rank;
111
+ }
112
+ }
113
+ if (part.type === "file") {
114
+ if (event.delta !== void 0) errors.push(`${at}: file part carries a delta (file parts are snapshot-only)`);
115
+ if (!part.url) errors.push(`${at}: file part is missing a non-empty url`);
116
+ else if (track.fileUrl !== void 0 && track.fileUrl !== part.url) errors.push(`${at}: file url changed "${track.fileUrl}" -> "${part.url}"`);
117
+ if (part.url) track.fileUrl = part.url;
118
+ }
119
+ tracks.set(part.id, track);
120
+ });
121
+ if (errors.length > 0) throw new Error(`stream invariants violated (${errors.length}):\n- ${errors.join("\n- ")}`);
122
+ }
123
+ //#endregion
124
+ //#region src/conformance/scenarios.ts
125
+ function partUpdates(events) {
126
+ return events.filter((e) => e.type === "message.part.updated");
127
+ }
128
+ /** Final text per part id of the given type, in first-appearance order. */
129
+ function finalTextsByPartId(events, type) {
130
+ const texts = /* @__PURE__ */ new Map();
131
+ for (const event of partUpdates(events)) {
132
+ if (event.part.type !== type) continue;
133
+ texts.set(event.part.id, event.part.text);
134
+ }
135
+ return texts;
136
+ }
137
+ /** Scenario-level checks on top of `assertStreamInvariants`. */
138
+ function assertScenarioExpectations(name, result) {
139
+ const errors = [];
140
+ const { events, expected } = result;
141
+ if (expected.reasoningBlockTexts) {
142
+ const finals = finalTextsByPartId(events, "reasoning");
143
+ const got = [...finals.values()];
144
+ if (got.length !== expected.reasoningBlockTexts.length) errors.push(`expected ${expected.reasoningBlockTexts.length} distinct reasoning part id(s), got ${got.length} (${JSON.stringify([...finals.keys()])})`);
145
+ else expected.reasoningBlockTexts.forEach((text, i) => {
146
+ if (got[i] !== text) errors.push(`reasoning block ${i}: expected ${JSON.stringify(text)}, got ${JSON.stringify(got[i])}`);
147
+ });
148
+ }
149
+ if (expected.reasoningDeltasRequired ?? true) partUpdates(events).forEach((event, i) => {
150
+ if (event.part.type === "reasoning" && event.delta === void 0) errors.push(`reasoning update ${i} for part "${event.part.id}" carries no delta`);
151
+ });
152
+ if (expected.finalText !== void 0) {
153
+ const finals = [...finalTextsByPartId(events, "text").values()];
154
+ if (finals.length !== 1 || finals[0] !== expected.finalText) errors.push(`expected one text part with final text ${JSON.stringify(expected.finalText)}, got ${JSON.stringify(finals)}`);
155
+ }
156
+ if (expected.toolCalls) for (const call of expected.toolCalls) {
157
+ const updates = partUpdates(events).filter((e) => e.part.type === "tool" && e.part.callID === call.callID);
158
+ const last = updates[updates.length - 1];
159
+ if (!last || last.part.type !== "tool") {
160
+ errors.push(`no tool part with callID "${call.callID}"`);
161
+ continue;
162
+ }
163
+ const gotInput = JSON.stringify(last.part.state.input);
164
+ const wantInput = JSON.stringify(call.input);
165
+ if (gotInput !== wantInput) errors.push(`tool "${call.callID}": final input ${gotInput} !== expected ${wantInput}`);
166
+ }
167
+ if (expected.fileParts) {
168
+ const fileUpdates = partUpdates(events).filter((e) => e.part.type === "file");
169
+ const idsByUrl = /* @__PURE__ */ new Map();
170
+ const finalByPartId = /* @__PURE__ */ new Map();
171
+ for (const event of fileUpdates) {
172
+ if (event.part.type !== "file") continue;
173
+ const url = event.part.url ?? "";
174
+ const ids = idsByUrl.get(url) ?? /* @__PURE__ */ new Set();
175
+ ids.add(event.part.id);
176
+ idsByUrl.set(url, ids);
177
+ finalByPartId.set(event.part.id, {
178
+ url,
179
+ mediaType: event.part.mediaType
180
+ });
181
+ }
182
+ for (const [url, ids] of idsByUrl) if (ids.size > 1) errors.push(`file part url ${JSON.stringify(url)} has ${ids.size} distinct part ids (expected exactly one — dedupe failed)`);
183
+ const finals = [...finalByPartId.values()];
184
+ if (finals.length !== expected.fileParts.length) errors.push(`expected ${expected.fileParts.length} distinct file part id(s), got ${finals.length} (${JSON.stringify(finals)})`);
185
+ else for (const want of expected.fileParts) if (!finals.some((got) => got.url === want.url && got.mediaType === want.mediaType)) errors.push(`no file part with final state ${JSON.stringify(want)} (got ${JSON.stringify(finals)})`);
186
+ }
187
+ if (errors.length > 0) throw new Error(`scenario "${name}" expectations failed (${errors.length}):\n- ${errors.join("\n- ")}`);
188
+ }
189
+ //#endregion
190
+ export { assertImageForwardedNatively, assertImageMarkerFallback, assertScenarioExpectations, assertStreamInvariants, conformanceScenarioEntries };
191
+
192
+ //# sourceMappingURL=index.mjs.map