@atoms-agent/testing 0.3.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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 atoms_agent contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # @atoms-agent/testing
2
+
3
+ Offline test helpers for `atoms_agent`: ScriptedModel, fake clock, and
4
+ deterministic IDs.
5
+
6
+ **Dev/test dependency only.** Production application runtimes should depend on
7
+ `@atoms-agent/core` and `@atoms-agent/llm` only. Do not ship this package in
8
+ application runtime installs unless you intentionally run offline demos.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install -D @atoms-agent/testing
14
+ ```
15
+
16
+ Local tarball (publish-ready verification):
17
+
18
+ ```bash
19
+ npm install ./atoms-agent-llm-0.3.0.tgz ./atoms-agent-core-0.3.0.tgz ./atoms-agent-testing-0.3.0.tgz
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ```typescript
25
+ import { createScriptedModel } from "@atoms-agent/testing";
26
+ import { createAgent } from "@atoms-agent/core";
27
+
28
+ const model = createScriptedModel({
29
+ script: [{ kind: "text", deltas: ["hello from scripted model"] }],
30
+ });
31
+
32
+ const agent = createAgent({
33
+ model,
34
+ systemPrompt: "Reply briefly.",
35
+ });
36
+ ```
37
+
38
+ Only import from the package root. Subpath / deep imports are not supported.
39
+
40
+ ## Requirements
41
+
42
+ - Node.js >= 22.13
43
+ - ESM
44
+
45
+ ## License
46
+
47
+ MIT
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Fake Clock(任务 B002):让 timeout/retry 测试确定化,
3
+ * 默认测试不依赖真实时间流逝。
4
+ */
5
+ export interface FakeClock {
6
+ now(): number;
7
+ sleep(ms: number, options?: {
8
+ signal?: AbortSignal;
9
+ }): Promise<void>;
10
+ advance(ms: number): Promise<void>;
11
+ }
12
+ export declare function createFakeClock(startMs?: number): FakeClock;
13
+ //# sourceMappingURL=clock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clock.d.ts","sourceRoot":"","sources":["../src/clock.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,WAAW,SAAS;IACxB,GAAG,IAAI,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrE,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC;AAUD,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAwE3D"}
package/dist/clock.js ADDED
@@ -0,0 +1,78 @@
1
+ import { AtomsAgentError } from "@atoms-agent/llm";
2
+ export function createFakeClock(startMs) {
3
+ let currentMs = startMs ?? 0;
4
+ let nextOrder = 0;
5
+ let pendingSleeps = [];
6
+ if (!Number.isFinite(currentMs)) {
7
+ throw new RangeError("Fake clock start time must be finite.");
8
+ }
9
+ return {
10
+ now() {
11
+ return currentMs;
12
+ },
13
+ sleep(ms, options) {
14
+ assertDuration(ms);
15
+ const deadlineMs = currentMs + ms;
16
+ if (!Number.isFinite(deadlineMs)) {
17
+ throw new RangeError("Fake clock sleep deadline must be finite.");
18
+ }
19
+ if (options?.signal?.aborted === true) {
20
+ return Promise.reject(clockCancellationError(options.signal));
21
+ }
22
+ if (ms === 0) {
23
+ return Promise.resolve();
24
+ }
25
+ return new Promise((resolve, reject) => {
26
+ const pending = {
27
+ deadlineMs,
28
+ order: nextOrder,
29
+ resolve,
30
+ signal: options?.signal,
31
+ };
32
+ nextOrder += 1;
33
+ if (options?.signal !== undefined) {
34
+ pending.onAbort = () => {
35
+ const index = pendingSleeps.indexOf(pending);
36
+ if (index < 0) {
37
+ return;
38
+ }
39
+ pendingSleeps.splice(index, 1);
40
+ reject(clockCancellationError(options.signal));
41
+ };
42
+ options.signal.addEventListener("abort", pending.onAbort, { once: true });
43
+ }
44
+ pendingSleeps.push(pending);
45
+ });
46
+ },
47
+ async advance(ms) {
48
+ assertDuration(ms);
49
+ const nextMs = currentMs + ms;
50
+ if (!Number.isFinite(nextMs)) {
51
+ throw new RangeError("Fake clock time must remain finite.");
52
+ }
53
+ currentMs = nextMs;
54
+ const ready = pendingSleeps
55
+ .filter((pending) => pending.deadlineMs <= currentMs)
56
+ .sort((left, right) => left.deadlineMs - right.deadlineMs || left.order - right.order);
57
+ pendingSleeps = pendingSleeps.filter((pending) => pending.deadlineMs > currentMs);
58
+ for (const pending of ready) {
59
+ if (pending.signal !== undefined && pending.onAbort !== undefined) {
60
+ pending.signal.removeEventListener("abort", pending.onAbort);
61
+ }
62
+ pending.resolve();
63
+ }
64
+ await Promise.resolve();
65
+ },
66
+ };
67
+ }
68
+ function assertDuration(ms) {
69
+ if (!Number.isFinite(ms) || ms < 0) {
70
+ throw new RangeError("Fake clock durations must be finite and non-negative.");
71
+ }
72
+ }
73
+ function clockCancellationError(signal) {
74
+ return new AtomsAgentError("CANCELLED", "Fake clock sleep was cancelled.", {
75
+ cause: signal.reason,
76
+ });
77
+ }
78
+ //# sourceMappingURL=clock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clock.js","sourceRoot":"","sources":["../src/clock.ts"],"names":[],"mappings":"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"}
package/dist/ids.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Deterministic ID 工厂(任务 B002):相同前缀产生 `prefix-1`、`prefix-2`…,
3
+ * 让 Event 快照测试可重复。
4
+ */
5
+ export type IdFactory = (prefix: string) => string;
6
+ export declare function createDeterministicIds(): IdFactory;
7
+ //# sourceMappingURL=ids.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ids.d.ts","sourceRoot":"","sources":["../src/ids.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;AAEnD,wBAAgB,sBAAsB,IAAI,SAAS,CAQlD"}
package/dist/ids.js ADDED
@@ -0,0 +1,9 @@
1
+ export function createDeterministicIds() {
2
+ const counters = new Map();
3
+ return (prefix) => {
4
+ const next = (counters.get(prefix) ?? 0) + 1;
5
+ counters.set(prefix, next);
6
+ return `${prefix}-${next}`;
7
+ };
8
+ }
9
+ //# sourceMappingURL=ids.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ids.js","sourceRoot":"","sources":["../src/ids.ts"],"names":[],"mappings":"AAMA,MAAM,UAAU,sBAAsB;IACpC,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;IAE3C,OAAO,CAAC,MAAc,EAAU,EAAE;QAChC,MAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;QAC7C,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAC3B,OAAO,GAAG,MAAM,IAAI,IAAI,EAAE,CAAC;IAC7B,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,7 @@
1
+ export { createScriptedModel } from "./scripted-model.js";
2
+ export type { ScriptedModel, ScriptedModelOptions, ScriptedToolCall, ScriptedTurn, } from "./scripted-model.js";
3
+ export { createFakeClock } from "./clock.js";
4
+ export type { FakeClock } from "./clock.js";
5
+ export { createDeterministicIds } from "./ids.js";
6
+ export type { IdFactory } from "./ids.js";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,YAAY,EACV,aAAa,EACb,oBAAoB,EACpB,gBAAgB,EAChB,YAAY,GACb,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAClD,YAAY,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { createScriptedModel } from "./scripted-model.js";
2
+ export { createFakeClock } from "./clock.js";
3
+ export { createDeterministicIds } from "./ids.js";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAO1D,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE7C,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC"}
@@ -0,0 +1,58 @@
1
+ import type { AssistantMessage, FinishReason, JsonValue, ModelAdapter, ModelCapabilities, ModelRequest, Usage } from "@atoms-agent/llm";
2
+ /**
3
+ * ScriptedModel —— 离线、确定性的 ModelAdapter(PRD TEST-001,任务 B002)。
4
+ *
5
+ * 每次 stream() 调用消费脚本中的下一段 turn,按声明产出事件。
6
+ * 相同脚本必须产生相同 Event 序列。
7
+ */
8
+ export type ScriptedToolCall = {
9
+ /** 缺省时由 Harness 生成稳定的 `tool-call-N`。 */
10
+ toolCallId?: string;
11
+ toolName: string;
12
+ input: JsonValue;
13
+ };
14
+ type ScriptedMessageResult = {
15
+ usage?: Usage;
16
+ finishReason?: FinishReason;
17
+ };
18
+ export type ScriptedTurn = ({
19
+ kind: "text";
20
+ /** 按 delta 逐段吐出,message_end 拼接为完整文本。 */
21
+ deltas: readonly string[];
22
+ } & ScriptedMessageResult) | ({
23
+ kind: "tool_calls";
24
+ calls: readonly ScriptedToolCall[];
25
+ /** 模型在 Tool Call 前产生的可选文本。 */
26
+ deltas?: readonly string[];
27
+ } & ScriptedMessageResult) | ({
28
+ kind: "message";
29
+ /** 直接产出完整 Canonical Assistant Message。 */
30
+ message: AssistantMessage;
31
+ /** 若提供,拼接结果必须与 message 中全部文本块一致。 */
32
+ deltas?: readonly string[];
33
+ } & ScriptedMessageResult) | {
34
+ kind: "provider_error";
35
+ /** 用于测试有限 Retry:retryable=true 的错误按策略重试。 */
36
+ retryable: boolean;
37
+ retryAfterMs?: number;
38
+ message: string;
39
+ } | {
40
+ kind: "hang";
41
+ };
42
+ export type ScriptedModelOptions = {
43
+ script: readonly ScriptedTurn[];
44
+ modelId?: string;
45
+ /** 用于 Capability Missing 场景;缺省声明 Streaming + Parallel Tool Call。 */
46
+ capabilities?: Partial<ModelCapabilities>;
47
+ };
48
+ /**
49
+ * ModelAdapter 之外暴露只读观测数据,供 Loop Fixture 核对 Tool Result,
50
+ * 包括 `isError=true` 的 Validation/Authorization/Execution Error 回填。
51
+ */
52
+ export interface ScriptedModel extends ModelAdapter {
53
+ readonly requests: readonly ModelRequest[];
54
+ readonly remainingTurns: number;
55
+ }
56
+ export declare function createScriptedModel(options: ScriptedModelOptions): ScriptedModel;
57
+ export {};
58
+ //# sourceMappingURL=scripted-model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scripted-model.d.ts","sourceRoot":"","sources":["../src/scripted-model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,YAAY,EACZ,SAAS,EACT,YAAY,EACZ,iBAAiB,EACjB,YAAY,EAGZ,KAAK,EACN,MAAM,kBAAkB,CAAC;AAK1B;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,wCAAwC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,SAAS,CAAC;CAClB,CAAC;AAEF,KAAK,qBAAqB,GAAG;IAC3B,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,YAAY,GACpB,CAAC;IACC,IAAI,EAAE,MAAM,CAAC;IACb,wCAAwC;IACxC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3B,GAAG,qBAAqB,CAAC,GAC1B,CAAC;IACC,IAAI,EAAE,YAAY,CAAC;IACnB,KAAK,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACnC,8BAA8B;IAC9B,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5B,GAAG,qBAAqB,CAAC,GAC1B,CAAC;IACC,IAAI,EAAE,SAAS,CAAC;IAChB,0CAA0C;IAC1C,OAAO,EAAE,gBAAgB,CAAC;IAC1B,oCAAoC;IACpC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5B,GAAG,qBAAqB,CAAC,GAC1B;IACE,IAAI,EAAE,gBAAgB,CAAC;IACvB,4CAA4C;IAC5C,SAAS,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;CACjB,GACD;IACE,IAAI,EAAE,MAAM,CAAC;CAEd,CAAC;AAEN,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,SAAS,YAAY,EAAE,CAAC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,YAAY,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,CAAC;CAC3C,CAAC;AAEF;;;GAGG;AACH,MAAM,WAAW,aAAc,SAAQ,YAAY;IACjD,QAAQ,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,CAAC;IAC3C,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAOD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,aAAa,CAmEhF"}
@@ -0,0 +1,158 @@
1
+ import { assertModelRequestCapabilities, AtomsAgentError } from "@atoms-agent/llm";
2
+ import { createDeterministicIds } from "./ids.js";
3
+ const EMPTY_USAGE = {
4
+ inputTokens: 0,
5
+ outputTokens: 0,
6
+ };
7
+ export function createScriptedModel(options) {
8
+ const script = structuredClone(options.script);
9
+ const requests = [];
10
+ const nextId = createDeterministicIds();
11
+ const modelId = options.modelId ?? "scripted:model";
12
+ const capabilities = Object.freeze({
13
+ streaming: true,
14
+ toolCalls: "parallel",
15
+ structuredOutput: "none",
16
+ ...options.capabilities,
17
+ });
18
+ let cursor = 0;
19
+ if (modelId.length === 0) {
20
+ throw new AtomsAgentError("CONFIG_ERROR", "Scripted modelId must not be empty.");
21
+ }
22
+ return {
23
+ modelId,
24
+ capabilities,
25
+ get requests() {
26
+ return structuredClone(requests);
27
+ },
28
+ get remainingTurns() {
29
+ return script.length - cursor;
30
+ },
31
+ stream(request, streamOptions) {
32
+ assertModelRequestCapabilities({ modelId, capabilities }, request);
33
+ return runTurn(request, streamOptions);
34
+ },
35
+ };
36
+ async function* runTurn(request, streamOptions) {
37
+ throwIfCancelled(streamOptions?.signal);
38
+ const turn = script[cursor];
39
+ if (turn === undefined) {
40
+ throw new AtomsAgentError("CONFIG_ERROR", `Scripted model has no turn configured for request ${cursor + 1}.`);
41
+ }
42
+ cursor += 1;
43
+ requests.push(structuredClone(request));
44
+ if (turn.kind === "provider_error") {
45
+ throw new AtomsAgentError("PROVIDER_ERROR", turn.message, {
46
+ retryable: turn.retryable,
47
+ retryAfterMs: turn.retryAfterMs,
48
+ });
49
+ }
50
+ if (turn.kind === "hang") {
51
+ await waitForCancellation(streamOptions?.signal);
52
+ return;
53
+ }
54
+ const events = buildMessageEvents(turn, nextId);
55
+ for (const event of events) {
56
+ throwIfCancelled(streamOptions?.signal);
57
+ yield event;
58
+ }
59
+ }
60
+ }
61
+ function buildMessageEvents(turn, nextId) {
62
+ if (turn.kind === "text") {
63
+ const text = turn.deltas.join("");
64
+ const message = {
65
+ role: "assistant",
66
+ content: [{ type: "text", text }],
67
+ };
68
+ return messageEvents(message, turn.deltas, turn.usage, turn.finishReason ?? "stop");
69
+ }
70
+ if (turn.kind === "tool_calls") {
71
+ if (turn.calls.length === 0) {
72
+ throw new AtomsAgentError("CONFIG_ERROR", "A scripted tool_calls turn requires a call.");
73
+ }
74
+ const deltas = turn.deltas ?? [];
75
+ const text = deltas.join("");
76
+ const message = {
77
+ role: "assistant",
78
+ content: [
79
+ ...(text.length > 0 ? [{ type: "text", text }] : []),
80
+ ...turn.calls.map((call) => ({
81
+ type: "tool_call",
82
+ toolCallId: call.toolCallId ?? nextId("tool-call"),
83
+ toolName: call.toolName,
84
+ input: structuredClone(call.input),
85
+ })),
86
+ ],
87
+ };
88
+ assertUniqueToolCallIds(message);
89
+ return messageEvents(message, deltas, turn.usage, turn.finishReason ?? "tool_calls");
90
+ }
91
+ const message = structuredClone(turn.message);
92
+ const deltas = turn.deltas ?? [];
93
+ if (turn.deltas !== undefined && deltas.join("") !== messageText(message)) {
94
+ throw new AtomsAgentError("CONFIG_ERROR", "Scripted message deltas do not match the complete assistant message.");
95
+ }
96
+ assertUniqueToolCallIds(message);
97
+ const hasToolCalls = message.content.some((block) => block.type === "tool_call");
98
+ return messageEvents(message, deltas, turn.usage, turn.finishReason ?? (hasToolCalls ? "tool_calls" : "stop"));
99
+ }
100
+ function messageEvents(message, deltas, usage, finishReason) {
101
+ const toolCalls = message.content
102
+ .filter((block) => block.type === "tool_call")
103
+ .map((block) => ({
104
+ type: "tool_call",
105
+ toolCallId: block.toolCallId,
106
+ toolName: block.toolName,
107
+ input: structuredClone(block.input),
108
+ }));
109
+ return [
110
+ { type: "message_start" },
111
+ ...deltas.map((delta) => ({ type: "text_delta", delta })),
112
+ ...toolCalls,
113
+ {
114
+ type: "message_end",
115
+ message: structuredClone(message),
116
+ usage: structuredClone(usage ?? EMPTY_USAGE),
117
+ finishReason,
118
+ },
119
+ ];
120
+ }
121
+ function messageText(message) {
122
+ return message.content
123
+ .filter((block) => block.type === "text")
124
+ .map((block) => block.text)
125
+ .join("");
126
+ }
127
+ function assertUniqueToolCallIds(message) {
128
+ const seen = new Set();
129
+ for (const block of message.content) {
130
+ if (block.type !== "tool_call") {
131
+ continue;
132
+ }
133
+ if (block.toolCallId.length === 0 || seen.has(block.toolCallId)) {
134
+ throw new AtomsAgentError("CONFIG_ERROR", "Scripted assistant messages require unique, non-empty Tool Call IDs.");
135
+ }
136
+ seen.add(block.toolCallId);
137
+ }
138
+ }
139
+ function throwIfCancelled(signal) {
140
+ if (signal?.aborted === true) {
141
+ throw cancellationError(signal);
142
+ }
143
+ }
144
+ async function waitForCancellation(signal) {
145
+ if (signal === undefined) {
146
+ return new Promise(() => undefined);
147
+ }
148
+ throwIfCancelled(signal);
149
+ return new Promise((_resolve, reject) => {
150
+ signal.addEventListener("abort", () => reject(cancellationError(signal)), { once: true });
151
+ });
152
+ }
153
+ function cancellationError(signal) {
154
+ return new AtomsAgentError("CANCELLED", "Scripted model stream was cancelled.", {
155
+ cause: signal.reason,
156
+ });
157
+ }
158
+ //# sourceMappingURL=scripted-model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scripted-model.js","sourceRoot":"","sources":["../src/scripted-model.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@atoms-agent/testing",
3
+ "version": "0.3.0",
4
+ "type": "module",
5
+ "description": "ScriptedModel, fake clock, deterministic ids and loop fixtures for offline agent testing",
6
+ "license": "MIT",
7
+ "author": "atoms_agent contributors",
8
+ "homepage": "https://github.com/Fufu-0101/atoms_agent#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/Fufu-0101/atoms_agent.git",
12
+ "directory": "packages/testing"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/Fufu-0101/atoms_agent/issues"
16
+ },
17
+ "keywords": [
18
+ "agent",
19
+ "testing",
20
+ "scripted-model",
21
+ "fixtures",
22
+ "typescript"
23
+ ],
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "default": "./dist/index.js"
31
+ }
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "engines": {
39
+ "node": ">=22.13"
40
+ },
41
+ "dependencies": {
42
+ "@atoms-agent/core": "0.3.0",
43
+ "@atoms-agent/llm": "0.3.0"
44
+ },
45
+ "scripts": {
46
+ "test": "vitest run",
47
+ "build": "tsc -b"
48
+ }
49
+ }