@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 +11 -0
- package/dist/conformance/index.d.mts +79 -0
- package/dist/conformance/index.mjs +192 -0
- package/dist/index.d.mts +1249 -0
- package/dist/index.mjs +4561 -0
- package/dist/prompt-input-BHn_1z69.d.mts +73 -0
- package/package.json +48 -3
- package/README.md +0 -3
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
|