@dex-ai/sdk 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +308 -0
- package/dist/agent.d.ts +181 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +41 -0
- package/dist/agent.js.map +1 -0
- package/dist/context.d.ts +68 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +8 -0
- package/dist/context.js.map +1 -0
- package/dist/create-agent.d.ts +7 -0
- package/dist/create-agent.d.ts.map +1 -0
- package/dist/create-agent.js +205 -0
- package/dist/create-agent.js.map +1 -0
- package/dist/extension.d.ts +162 -0
- package/dist/extension.d.ts.map +1 -0
- package/dist/extension.js +20 -0
- package/dist/extension.js.map +1 -0
- package/dist/generate.d.ts +10 -0
- package/dist/generate.d.ts.map +1 -0
- package/dist/generate.js +839 -0
- package/dist/generate.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/message.d.ts +89 -0
- package/dist/message.d.ts.map +1 -0
- package/dist/message.js +17 -0
- package/dist/message.js.map +1 -0
- package/dist/messages.d.ts +98 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +339 -0
- package/dist/messages.js.map +1 -0
- package/dist/model.d.ts +39 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +11 -0
- package/dist/model.js.map +1 -0
- package/dist/provider.d.ts +157 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +39 -0
- package/dist/provider.js.map +1 -0
- package/dist/resolve-schema.d.ts +44 -0
- package/dist/resolve-schema.d.ts.map +1 -0
- package/dist/resolve-schema.js +367 -0
- package/dist/resolve-schema.js.map +1 -0
- package/dist/schema.d.ts +80 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +90 -0
- package/dist/schema.js.map +1 -0
- package/dist/tool-dispatch.d.ts +24 -0
- package/dist/tool-dispatch.d.ts.map +1 -0
- package/dist/tool-dispatch.js +120 -0
- package/dist/tool-dispatch.js.map +1 -0
- package/dist/tool-result-cache.d.ts +43 -0
- package/dist/tool-result-cache.d.ts.map +1 -0
- package/dist/tool-result-cache.js +118 -0
- package/dist/tool-result-cache.js.map +1 -0
- package/dist/tool.d.ts +96 -0
- package/dist/tool.d.ts.map +1 -0
- package/dist/tool.js +29 -0
- package/dist/tool.js.map +1 -0
- package/dist/util.d.ts +26 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +104 -0
- package/dist/util.js.map +1 -0
- package/package.json +41 -0
- package/src/agent.ts +235 -0
- package/src/context.ts +82 -0
- package/src/create-agent.ts +237 -0
- package/src/extension.ts +244 -0
- package/src/generate.ts +943 -0
- package/src/index.ts +113 -0
- package/src/message.ts +114 -0
- package/src/messages.test.ts +299 -0
- package/src/messages.ts +423 -0
- package/src/model.ts +43 -0
- package/src/provider.ts +187 -0
- package/src/resolve-schema.test.ts +351 -0
- package/src/resolve-schema.ts +426 -0
- package/src/schema.ts +131 -0
- package/src/tool-dispatch.ts +166 -0
- package/src/tool-result-cache.test.ts +182 -0
- package/src/tool-result-cache.ts +164 -0
- package/src/tool.ts +110 -0
- package/src/util.ts +110 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @dex-ai/sdk — public barrel.
|
|
3
|
+
*
|
|
4
|
+
* Core types: Extension, Model, Tool, Message, Content, Agent.
|
|
5
|
+
* Extensions register event handlers; the loop emits events.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Messages & content
|
|
9
|
+
export type {
|
|
10
|
+
Role,
|
|
11
|
+
Content,
|
|
12
|
+
ContentPayload,
|
|
13
|
+
TextContent,
|
|
14
|
+
CacheControl,
|
|
15
|
+
ImageContent,
|
|
16
|
+
FileContent,
|
|
17
|
+
ReasoningContent,
|
|
18
|
+
ToolCallContent,
|
|
19
|
+
ToolResultContent,
|
|
20
|
+
ToolOutput,
|
|
21
|
+
Message,
|
|
22
|
+
MessageType,
|
|
23
|
+
} from "./message";
|
|
24
|
+
|
|
25
|
+
// Schema
|
|
26
|
+
export type { StandardSchemaV1, InferInput, InferOutput } from "./schema";
|
|
27
|
+
export { Schema } from "./schema";
|
|
28
|
+
export { resolveJsonSchema, validateToolSchemas } from "./resolve-schema";
|
|
29
|
+
export type { JsonSchemaObject, ToolSchemaDiagnostic } from "./resolve-schema";
|
|
30
|
+
|
|
31
|
+
// Tool
|
|
32
|
+
export type {
|
|
33
|
+
ToolCall,
|
|
34
|
+
ToolResult,
|
|
35
|
+
ExecuteResult,
|
|
36
|
+
AnyTool,
|
|
37
|
+
ToolStreamPart,
|
|
38
|
+
} from "./tool";
|
|
39
|
+
export { Tool } from "./tool";
|
|
40
|
+
|
|
41
|
+
// Provider types (StreamPart, ModelRequest, etc. — still used by Model.stream)
|
|
42
|
+
export type {
|
|
43
|
+
ModelRequest,
|
|
44
|
+
ModelResponse,
|
|
45
|
+
ResponseMeta,
|
|
46
|
+
FinishReason,
|
|
47
|
+
Usage,
|
|
48
|
+
ToolChoice,
|
|
49
|
+
StreamPart,
|
|
50
|
+
ResponseStartPart,
|
|
51
|
+
ResponseStopPart,
|
|
52
|
+
MessageStartPart,
|
|
53
|
+
MessageStopPart,
|
|
54
|
+
TextDeltaPart,
|
|
55
|
+
ReasoningDeltaPart,
|
|
56
|
+
ToolCallDeltaPart,
|
|
57
|
+
ToolCallPart,
|
|
58
|
+
FinishPart,
|
|
59
|
+
RawChunkPart,
|
|
60
|
+
ErrorPart,
|
|
61
|
+
AbortPart,
|
|
62
|
+
} from "./provider";
|
|
63
|
+
// Provider type kept for extensions that need to wrap legacy providers
|
|
64
|
+
export type { Provider } from "./provider";
|
|
65
|
+
|
|
66
|
+
// Model
|
|
67
|
+
export type { Model, ThinkingLevel } from "./model";
|
|
68
|
+
|
|
69
|
+
// Extension
|
|
70
|
+
export type {
|
|
71
|
+
ExtensionEvents,
|
|
72
|
+
MaybePromise,
|
|
73
|
+
ErrorSource,
|
|
74
|
+
Skill,
|
|
75
|
+
ConfigField,
|
|
76
|
+
ConfigSchema,
|
|
77
|
+
ProviderConfigField,
|
|
78
|
+
ProviderDescriptor,
|
|
79
|
+
} from "./extension";
|
|
80
|
+
export { Extension } from "./extension";
|
|
81
|
+
|
|
82
|
+
// Context
|
|
83
|
+
export type { AgentContext, GenerateContext } from "./context";
|
|
84
|
+
|
|
85
|
+
// Messages
|
|
86
|
+
export { Messages, validateMessages } from "./messages";
|
|
87
|
+
export type { ValidationError } from "./messages";
|
|
88
|
+
|
|
89
|
+
// Agent
|
|
90
|
+
export type {
|
|
91
|
+
AgentStream,
|
|
92
|
+
AgentStreamPart,
|
|
93
|
+
LoopStreamPart,
|
|
94
|
+
CreateAgent,
|
|
95
|
+
CreateAgentOptions,
|
|
96
|
+
GenerateOptions,
|
|
97
|
+
GenerateResult,
|
|
98
|
+
GenerateStartPart,
|
|
99
|
+
GenerateFinishPart,
|
|
100
|
+
IterationStartPart,
|
|
101
|
+
IterationFinishPart,
|
|
102
|
+
ToolExecuteStartPart,
|
|
103
|
+
ToolExecuteFinishPart,
|
|
104
|
+
MessageCommittedPart,
|
|
105
|
+
ExtensionInfoPart,
|
|
106
|
+
} from "./agent";
|
|
107
|
+
export { Agent } from "./agent";
|
|
108
|
+
|
|
109
|
+
// Tool Result Cache
|
|
110
|
+
export type { ToolResultCacheConfig } from "./tool-result-cache";
|
|
111
|
+
|
|
112
|
+
// Utilities
|
|
113
|
+
export { formatError } from "./util";
|
package/src/message.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message & Content model.
|
|
3
|
+
*
|
|
4
|
+
* Content is a flat discriminated union aligned with ai-sdk:
|
|
5
|
+
* text, image, file, reasoning, tool-call, tool-result.
|
|
6
|
+
*
|
|
7
|
+
* Messages carry Content[]. ToolResult (see tool.ts) uses tagged-variant
|
|
8
|
+
* output rather than Content[], because tool outputs span more shapes
|
|
9
|
+
* (rich content, structured JSON, plain text, error text, error JSON).
|
|
10
|
+
*
|
|
11
|
+
* Conversation state is kept well-typed end to end:
|
|
12
|
+
* AgentContext.messages : Message[] (persistent)
|
|
13
|
+
* GenerateContext.content : Content[] (in-flight output)
|
|
14
|
+
* Tool output : ToolOutput (tagged) (see tool.ts)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export type Role = "system" | "user" | "assistant" | "tool";
|
|
18
|
+
|
|
19
|
+
/* ------------------------------------------------------------------ */
|
|
20
|
+
/* Content — flat union (ai-sdk aligned) */
|
|
21
|
+
/* ------------------------------------------------------------------ */
|
|
22
|
+
|
|
23
|
+
export interface TextContent {
|
|
24
|
+
readonly type: "text";
|
|
25
|
+
readonly text: string;
|
|
26
|
+
/** Cache control hint for providers that support prompt caching (e.g. Anthropic). */
|
|
27
|
+
readonly cacheControl?: CacheControl | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Cache control — instructs the provider to cache content up to this point. */
|
|
31
|
+
export interface CacheControl {
|
|
32
|
+
/** Cache strategy. 'ephemeral' = cache for the provider's default TTL. */
|
|
33
|
+
readonly type: "ephemeral";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ImageContent {
|
|
37
|
+
readonly type: "image";
|
|
38
|
+
readonly image: string | Uint8Array | URL;
|
|
39
|
+
readonly mediaType?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Binary file: PDF, spreadsheet, arbitrary blob. ai-sdk-aligned name. */
|
|
43
|
+
export interface FileContent {
|
|
44
|
+
readonly type: "file";
|
|
45
|
+
readonly data: string | Uint8Array | URL;
|
|
46
|
+
readonly mediaType: string;
|
|
47
|
+
readonly name?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Assistant chain-of-thought / reasoning trace. */
|
|
51
|
+
export interface ReasoningContent {
|
|
52
|
+
readonly type: "reasoning";
|
|
53
|
+
readonly text: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Assistant requests a tool. */
|
|
57
|
+
export interface ToolCallContent {
|
|
58
|
+
readonly type: "tool-call";
|
|
59
|
+
readonly toolCallId: string;
|
|
60
|
+
readonly toolName: string;
|
|
61
|
+
readonly input: unknown;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Tool response content part. See tool.ts for the ToolOutput variant shape. */
|
|
65
|
+
export interface ToolResultContent {
|
|
66
|
+
readonly type: "tool-result";
|
|
67
|
+
readonly toolCallId: string;
|
|
68
|
+
readonly toolName: string;
|
|
69
|
+
readonly output: ToolOutput;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* ToolOutput — tagged variant (ai-sdk v5 aligned). */
|
|
73
|
+
export type ToolOutput =
|
|
74
|
+
| { readonly type: "content"; readonly value: ReadonlyArray<ContentPayload> }
|
|
75
|
+
| { readonly type: "json"; readonly value: unknown }
|
|
76
|
+
| { readonly type: "text"; readonly value: string }
|
|
77
|
+
| { readonly type: "error-text"; readonly value: string }
|
|
78
|
+
| { readonly type: "error-json"; readonly value: unknown };
|
|
79
|
+
|
|
80
|
+
/** What a ToolOutput of type "content" carries. Text | Image | File only — not tool-call/tool-result. */
|
|
81
|
+
export type ContentPayload = TextContent | ImageContent | FileContent;
|
|
82
|
+
|
|
83
|
+
/* ------------------------------------------------------------------ */
|
|
84
|
+
/* Content union & Message */
|
|
85
|
+
/* ------------------------------------------------------------------ */
|
|
86
|
+
|
|
87
|
+
export type Content =
|
|
88
|
+
| TextContent
|
|
89
|
+
| ImageContent
|
|
90
|
+
| FileContent
|
|
91
|
+
| ReasoningContent
|
|
92
|
+
| ToolCallContent
|
|
93
|
+
| ToolResultContent;
|
|
94
|
+
|
|
95
|
+
export type MessageType =
|
|
96
|
+
| "user"
|
|
97
|
+
| "assistant"
|
|
98
|
+
| "tool-result"
|
|
99
|
+
| "system"
|
|
100
|
+
| "context-session"
|
|
101
|
+
| "context-skills"
|
|
102
|
+
| "context-turn"
|
|
103
|
+
| "info";
|
|
104
|
+
|
|
105
|
+
export interface Message {
|
|
106
|
+
readonly role: Role;
|
|
107
|
+
readonly content: ReadonlyArray<Content>;
|
|
108
|
+
/** Semantic type — controls persistence, display, and LLM routing. Defaults to role. */
|
|
109
|
+
readonly type?: MessageType;
|
|
110
|
+
readonly name?: string;
|
|
111
|
+
readonly id?: string;
|
|
112
|
+
/** Identifies the generate() call that produced this message. All messages committed during a single generate() share the same generateId. */
|
|
113
|
+
readonly generateId?: string;
|
|
114
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { Messages, validateMessages } from "./messages";
|
|
3
|
+
import type { Message } from "./message";
|
|
4
|
+
|
|
5
|
+
/* ------------------------------------------------------------------ */
|
|
6
|
+
/* Helpers */
|
|
7
|
+
/* ------------------------------------------------------------------ */
|
|
8
|
+
|
|
9
|
+
function systemMsg(text = "system"): Message {
|
|
10
|
+
return { role: "system", content: [{ type: "text", text }], id: "sys-1" };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function userMsg(text = "hello"): Message {
|
|
14
|
+
return { role: "user", content: [{ type: "text", text }], id: "user-1" };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function assistantMsg(text = "hi"): Message {
|
|
18
|
+
return { role: "assistant", content: [{ type: "text", text }], id: "asst-1" };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function assistantWithToolCall(
|
|
22
|
+
toolCallId = "tc-1",
|
|
23
|
+
toolName = "my_tool",
|
|
24
|
+
): Message {
|
|
25
|
+
return {
|
|
26
|
+
role: "assistant",
|
|
27
|
+
content: [
|
|
28
|
+
{ type: "tool-call", toolCallId, toolName, input: {} },
|
|
29
|
+
],
|
|
30
|
+
id: "asst-tc-1",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function toolResultMsg(
|
|
35
|
+
toolCallId = "tc-1",
|
|
36
|
+
toolName = "my_tool",
|
|
37
|
+
): Message {
|
|
38
|
+
return {
|
|
39
|
+
role: "tool",
|
|
40
|
+
content: [
|
|
41
|
+
{
|
|
42
|
+
type: "tool-result",
|
|
43
|
+
toolCallId,
|
|
44
|
+
toolName,
|
|
45
|
+
output: { type: "text", value: "done" },
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
id: "tool-1",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* ------------------------------------------------------------------ */
|
|
53
|
+
/* validateMessages */
|
|
54
|
+
/* ------------------------------------------------------------------ */
|
|
55
|
+
|
|
56
|
+
describe("validateMessages", () => {
|
|
57
|
+
it("accepts empty array", () => {
|
|
58
|
+
expect(validateMessages([])).toEqual([]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("accepts valid system → user → assistant sequence", () => {
|
|
62
|
+
const msgs = [systemMsg(), userMsg(), assistantMsg()];
|
|
63
|
+
expect(validateMessages(msgs)).toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("accepts valid tool-call → tool-result pairing", () => {
|
|
67
|
+
const msgs = [
|
|
68
|
+
systemMsg(),
|
|
69
|
+
userMsg(),
|
|
70
|
+
assistantWithToolCall("tc-1"),
|
|
71
|
+
toolResultMsg("tc-1"),
|
|
72
|
+
];
|
|
73
|
+
expect(validateMessages(msgs)).toEqual([]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("rejects system message after non-system", () => {
|
|
77
|
+
const msgs = [userMsg(), systemMsg()];
|
|
78
|
+
const errors = validateMessages(msgs);
|
|
79
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
80
|
+
expect(errors.some((e) => e.message.includes("System message"))).toBe(
|
|
81
|
+
true,
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("rejects first non-system message being assistant", () => {
|
|
86
|
+
const msgs = [systemMsg(), assistantMsg()];
|
|
87
|
+
const errors = validateMessages(msgs);
|
|
88
|
+
expect(errors.some((e) => e.message.includes('must be "user"'))).toBe(
|
|
89
|
+
true,
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("rejects orphaned tool-call without tool-result", () => {
|
|
94
|
+
const msgs = [
|
|
95
|
+
systemMsg(),
|
|
96
|
+
userMsg(),
|
|
97
|
+
assistantWithToolCall("tc-1"),
|
|
98
|
+
// no tool-result
|
|
99
|
+
userMsg(),
|
|
100
|
+
];
|
|
101
|
+
const errors = validateMessages(msgs);
|
|
102
|
+
expect(
|
|
103
|
+
errors.some((e) => e.message.includes("Orphaned tool-call")),
|
|
104
|
+
).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("rejects orphaned tool-result without tool-call", () => {
|
|
108
|
+
const msgs = [systemMsg(), userMsg(), assistantMsg(), toolResultMsg("tc-orphan")];
|
|
109
|
+
const errors = validateMessages(msgs);
|
|
110
|
+
expect(
|
|
111
|
+
errors.some((e) => e.message.includes("Orphaned tool-result")),
|
|
112
|
+
).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("rejects user following user", () => {
|
|
116
|
+
const msgs: Message[] = [
|
|
117
|
+
{ role: "user", content: [{ type: "text", text: "a" }], id: "u1" },
|
|
118
|
+
{ role: "user", content: [{ type: "text", text: "b" }], id: "u2" },
|
|
119
|
+
];
|
|
120
|
+
const errors = validateMessages(msgs);
|
|
121
|
+
expect(
|
|
122
|
+
errors.some((e) => e.message.includes('"user" message follows "user"')),
|
|
123
|
+
).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("rejects assistant following assistant", () => {
|
|
127
|
+
const msgs: Message[] = [
|
|
128
|
+
userMsg(),
|
|
129
|
+
{ role: "assistant", content: [{ type: "text", text: "a" }], id: "a1" },
|
|
130
|
+
{ role: "assistant", content: [{ type: "text", text: "b" }], id: "a2" },
|
|
131
|
+
];
|
|
132
|
+
const errors = validateMessages(msgs);
|
|
133
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
/* ------------------------------------------------------------------ */
|
|
138
|
+
/* Messages class — Proxy trapping */
|
|
139
|
+
/* ------------------------------------------------------------------ */
|
|
140
|
+
|
|
141
|
+
describe("Messages proxy", () => {
|
|
142
|
+
it("blocks direct .push()", () => {
|
|
143
|
+
const msgs = new Messages();
|
|
144
|
+
// Start with valid state to test proxy blocking
|
|
145
|
+
msgs.append(userMsg());
|
|
146
|
+
const arr = msgs.array as any;
|
|
147
|
+
expect(() => arr.push(assistantMsg())).toThrow("Direct .push()");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("blocks direct .splice()", () => {
|
|
151
|
+
const msgs = new Messages();
|
|
152
|
+
msgs.append(userMsg());
|
|
153
|
+
const arr = msgs.array as any;
|
|
154
|
+
expect(() => arr.splice(0, 1)).toThrow("Direct .splice()");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("blocks index assignment", () => {
|
|
158
|
+
const msgs = new Messages();
|
|
159
|
+
msgs.append(userMsg());
|
|
160
|
+
const arr = msgs.array as any;
|
|
161
|
+
expect(() => {
|
|
162
|
+
arr[0] = assistantMsg();
|
|
163
|
+
}).toThrow("Direct index assignment");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("blocks length assignment", () => {
|
|
167
|
+
const msgs = new Messages();
|
|
168
|
+
msgs.append(userMsg());
|
|
169
|
+
const arr = msgs.array as any;
|
|
170
|
+
expect(() => {
|
|
171
|
+
arr.length = 0;
|
|
172
|
+
}).toThrow("Direct array mutation");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("allows read operations (indexing, iteration, length)", () => {
|
|
176
|
+
const msgs = new Messages();
|
|
177
|
+
msgs.append(userMsg());
|
|
178
|
+
msgs.append(assistantMsg());
|
|
179
|
+
const arr = msgs.array;
|
|
180
|
+
expect(arr.length).toBe(2);
|
|
181
|
+
expect(arr[0]!.role).toBe("user");
|
|
182
|
+
expect([...arr].length).toBe(2);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
/* ------------------------------------------------------------------ */
|
|
187
|
+
/* Messages class — controlled mutations */
|
|
188
|
+
/* ------------------------------------------------------------------ */
|
|
189
|
+
|
|
190
|
+
describe("Messages.append", () => {
|
|
191
|
+
it("appends a valid message", () => {
|
|
192
|
+
const msgs = new Messages();
|
|
193
|
+
msgs.append(userMsg());
|
|
194
|
+
expect(msgs.length).toBe(1);
|
|
195
|
+
expect(msgs.array[0]!.role).toBe("user");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("assigns ID if missing", () => {
|
|
199
|
+
const msgs = new Messages();
|
|
200
|
+
const noId: Message = { role: "user", content: [{ type: "text", text: "hi" }] };
|
|
201
|
+
msgs.append(noId);
|
|
202
|
+
expect(msgs.array[0]!.id).toBeDefined();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("throws on invalid append (e.g. tool after user)", () => {
|
|
206
|
+
const msgs = new Messages();
|
|
207
|
+
msgs.append(userMsg());
|
|
208
|
+
expect(() =>
|
|
209
|
+
msgs.append(toolResultMsg("tc-1")),
|
|
210
|
+
).toThrow("Invalid message sequence");
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe("Messages.splice", () => {
|
|
215
|
+
it("removes and inserts messages with validation", () => {
|
|
216
|
+
const msgs = new Messages();
|
|
217
|
+
msgs.append(userMsg());
|
|
218
|
+
msgs.append(assistantMsg());
|
|
219
|
+
// Replace assistant with a new one
|
|
220
|
+
const removed = msgs.splice(1, 1, {
|
|
221
|
+
role: "assistant",
|
|
222
|
+
content: [{ type: "text", text: "replaced" }],
|
|
223
|
+
id: "a-new",
|
|
224
|
+
});
|
|
225
|
+
expect(removed.length).toBe(1);
|
|
226
|
+
expect(msgs.array[1]!.content[0]!.type).toBe("text");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("throws if splice leaves invalid state", () => {
|
|
230
|
+
const msgs = new Messages();
|
|
231
|
+
msgs.append(userMsg());
|
|
232
|
+
msgs.append(assistantMsg());
|
|
233
|
+
// Remove the user message → assistant is first non-system = invalid
|
|
234
|
+
expect(() => msgs.splice(0, 1)).toThrow("Invalid message sequence");
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe("Messages.batch", () => {
|
|
239
|
+
it("defers validation during batch", () => {
|
|
240
|
+
const msgs = new Messages();
|
|
241
|
+
msgs.append(userMsg());
|
|
242
|
+
msgs.append(assistantMsg());
|
|
243
|
+
|
|
244
|
+
msgs.batch();
|
|
245
|
+
// This would be invalid mid-way but we're batching
|
|
246
|
+
msgs.splice(0, 2); // empty — no first-user check since empty is valid
|
|
247
|
+
msgs.append(userMsg());
|
|
248
|
+
msgs.append(assistantMsg());
|
|
249
|
+
msgs.commit(); // should not throw
|
|
250
|
+
expect(msgs.length).toBe(2);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("throws on commit if batch leaves invalid state", () => {
|
|
254
|
+
const msgs = new Messages();
|
|
255
|
+
msgs.batch();
|
|
256
|
+
msgs.append(assistantMsg()); // invalid: first non-system must be user
|
|
257
|
+
expect(() => msgs.commit()).toThrow("Invalid message sequence");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("transaction auto-commits on success", () => {
|
|
261
|
+
const msgs = new Messages();
|
|
262
|
+
msgs.transaction(() => {
|
|
263
|
+
msgs.append(userMsg());
|
|
264
|
+
msgs.append(assistantMsg());
|
|
265
|
+
});
|
|
266
|
+
expect(msgs.length).toBe(2);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("transaction does not validate on throw", () => {
|
|
270
|
+
const msgs = new Messages();
|
|
271
|
+
expect(() =>
|
|
272
|
+
msgs.transaction(() => {
|
|
273
|
+
msgs.append(userMsg());
|
|
274
|
+
throw new Error("bail");
|
|
275
|
+
}),
|
|
276
|
+
).toThrow("bail");
|
|
277
|
+
// Messages still contains the user msg (no rollback, but no validation either)
|
|
278
|
+
expect(msgs.length).toBe(1);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe("Messages.replace", () => {
|
|
283
|
+
it("replaces a message at index", () => {
|
|
284
|
+
const msgs = new Messages();
|
|
285
|
+
msgs.append(userMsg());
|
|
286
|
+
msgs.append(assistantMsg());
|
|
287
|
+
msgs.replace(1, {
|
|
288
|
+
role: "assistant",
|
|
289
|
+
content: [{ type: "text", text: "new" }],
|
|
290
|
+
id: "a-replaced",
|
|
291
|
+
});
|
|
292
|
+
expect((msgs.array[1]!.content[0] as any).text).toBe("new");
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("throws on out-of-bounds index", () => {
|
|
296
|
+
const msgs = new Messages();
|
|
297
|
+
expect(() => msgs.replace(0, userMsg())).toThrow("out of bounds");
|
|
298
|
+
});
|
|
299
|
+
});
|