@agentica/core 0.32.5 → 0.32.7

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.
@@ -0,0 +1,104 @@
1
+ import { JsonUtil } from "./JsonUtil";
2
+
3
+ describe("JsonUtil", () => {
4
+ describe("parse", () => {
5
+ it("should handle string that starts with '{}'", () => {
6
+ const jsonString = '{}{"name": "test", "value": 42}';
7
+ const result = JsonUtil.parse(jsonString);
8
+
9
+ expect(result).toEqual({ name: "test", value: 42 });
10
+ });
11
+
12
+ it("should handle array with '{}' prefix", () => {
13
+ const jsonString = '{}[1, 2, 3, "test"]';
14
+ const result = JsonUtil.parse(jsonString);
15
+
16
+ expect(result).toEqual([1, 2, 3, "test"]);
17
+ });
18
+
19
+ it("should handle nested object with '{}' prefix", () => {
20
+ const jsonString = '{}{"user": {"id": 1, "name": "John"}}';
21
+ const result = JsonUtil.parse(jsonString);
22
+
23
+ expect(result).toEqual({
24
+ user: { id: 1, name: "John" }
25
+ });
26
+ });
27
+
28
+ it("should handle primitive values with '{}' prefix", () => {
29
+ expect(JsonUtil.parse('{}42')).toBe(42);
30
+ expect(JsonUtil.parse('{}"hello"')).toBe("hello");
31
+ expect(JsonUtil.parse('{}true')).toBe(true);
32
+ expect(JsonUtil.parse('{}null')).toBeNull();
33
+ });
34
+
35
+ it("should not modify string that doesn't start with '{}'", () => {
36
+ const jsonString = '{"normal": "json"}';
37
+ const result = JsonUtil.parse(jsonString);
38
+
39
+ expect(result).toEqual({ normal: "json" });
40
+ });
41
+
42
+ // 마지막 괄호 누락 보정 테스트 (예상 기능)
43
+ it("should handle missing closing brace in object", () => {
44
+ const jsonString = '{"name": "test", "value": 42';
45
+ const result = JsonUtil.parse(jsonString);
46
+
47
+ expect(result).toEqual({ name: "test", value: 42 });
48
+ });
49
+
50
+ it("should handle missing closing bracket in array", () => {
51
+ const jsonString = '[1, 2, 3, "test"';
52
+ const result = JsonUtil.parse(jsonString);
53
+
54
+ expect(result).toEqual([1, 2, 3, "test"]);
55
+ });
56
+
57
+ it("should handle nested object with missing closing brace", () => {
58
+ const jsonString = '{"user": {"id": 1, "name": "John"}';
59
+ const result = JsonUtil.parse(jsonString);
60
+
61
+ expect(result).toEqual({
62
+ user: { id: 1, name: "John" }
63
+ });
64
+ });
65
+
66
+ it("should handle complex nested structure with missing closing brace", () => {
67
+ const jsonString = '{"users": [{"id": 1, "name": "John"}, {"id": 2, "name": "Jane"}], "count": 2';
68
+ const result = JsonUtil.parse(jsonString);
69
+
70
+ expect(result).toEqual({
71
+ users: [
72
+ { id: 1, name: "John" },
73
+ { id: 2, name: "Jane" }
74
+ ],
75
+ count: 2
76
+ });
77
+ });
78
+
79
+ it("should handle both '{}' prefix and missing closing brace", () => {
80
+ const jsonString = '{}{"name": "test", "value": 42';
81
+ const result = JsonUtil.parse(jsonString);
82
+
83
+ expect(result).toEqual({ name: "test", value: 42 });
84
+ });
85
+
86
+ it("should handle both '{}' prefix and missing closing bracket in array", () => {
87
+ const jsonString = '{}[1, 2, 3, "test"';
88
+ const result = JsonUtil.parse(jsonString);
89
+
90
+ expect(result).toEqual([1, 2, 3, "test"]);
91
+ });
92
+
93
+ // 에러 케이스 (보정할 수 없는 경우)
94
+ it("should throw error for completely invalid JSON", () => {
95
+ const invalidJson = '{invalid: json without quotes}';
96
+
97
+ expect(() => JsonUtil.parse(invalidJson)).toThrow();
98
+ });
99
+
100
+ it("should throw error for empty string", () => {
101
+ expect(() => JsonUtil.parse("")).toThrow();
102
+ });
103
+ });
104
+ });
@@ -0,0 +1,150 @@
1
+ export const JsonUtil = {
2
+ parse,
3
+ };
4
+
5
+ function parse(str: string) {
6
+ const corrected = pipe(stripFirstBrace, correctMissingLastBrace)(str);
7
+ return JSON.parse(corrected);
8
+ }
9
+
10
+ const pipe = (...fns: ((str: string) => string)[]) => (str: string) => fns.reduce((acc, fn) => fn(acc), str);
11
+
12
+ function stripFirstBrace(str: string) {
13
+ if(RegExp("^{}.").test(str) === true) {
14
+ return str.substring(2);
15
+ }
16
+ return str;
17
+ }
18
+
19
+ export function correctMissingLastBrace(input: string): string {
20
+ const initial: ParseState = { s: "OUT", stack: [], line: 1, col: 0, edits: [] };
21
+
22
+ const scanned = Array.from(input).reduce<ParseState>((ps, ch, i) => {
23
+ const updated = ch === "\n"
24
+ ? { ...ps, line: ps.line + 1, col: 0 }
25
+ : { ...ps, col: ps.col + 1 };
26
+
27
+ const tok = categorize(ch);
28
+ const trans = (table[updated.s] as Record<Token, Transition>)?.[tok];
29
+ return trans ? trans(updated, ch, i) : updated;
30
+ }, initial);
31
+
32
+ // Return original string if string is not closed (do not modify)
33
+ if (scanned.s !== "OUT") return input;
34
+
35
+ // Insert closing braces at the end for remaining open braces (LIFO)
36
+ const withTail = scanned.stack.length === 0
37
+ ? scanned
38
+ : ((): ParseState => {
39
+ const closers = scanned.stack.slice().reverse().map(e => closeOf[e.type]).join("");
40
+ return {
41
+ ...scanned,
42
+ edits: [...scanned.edits, { op: "insert", index: input.length, text: closers }],
43
+ stack: [],
44
+ };
45
+ })();
46
+
47
+ return applyEditsImmutable(input, withTail.edits);
48
+ }
49
+
50
+ // Apply edits immutably
51
+ function applyEditsImmutable(src: string, edits: ReadonlyArray<Edit>): string {
52
+ const sorted = [...edits].sort((a, b) => a.index - b.index);
53
+
54
+ type Build = { out: string; cursor: number };
55
+ const built = sorted.reduce<Build>((acc, e) => {
56
+ const prefix = src.slice(acc.cursor, e.index);
57
+ const acc1 = { out: acc.out + prefix, cursor: e.index };
58
+ return e.op === "delete"
59
+ ? { out: acc1.out, cursor: acc1.cursor + 1 }
60
+ : e.op === "replace"
61
+ ? { out: acc1.out + e.text, cursor: acc1.cursor + 1 }
62
+ : /* insert */ { out: acc1.out + e.text, cursor: acc1.cursor };
63
+ }, { out: "", cursor: 0 });
64
+
65
+ return built.out + src.slice(built.cursor);
66
+ }
67
+
68
+ const openOf = Object.freeze<Readonly<Record<BraceClose, BraceOpen>>>({ "}": "{", "]": "[" });
69
+ const closeOf = Object.freeze<Readonly<Record<BraceOpen, BraceClose>>>({ "{": "}", "[": "]" });
70
+
71
+ const categorize = (ch: string): Token => {
72
+ switch (ch) {
73
+ case '"': return "DQUOTE";
74
+ case "\\": return "BSLASH";
75
+ case "{": return "OCB";
76
+ case "[": return "OSB";
77
+ case "}": return "CCB";
78
+ case "]": return "CSB";
79
+ case "\n": return "NEWLINE";
80
+ default: return "CHAR";
81
+ }
82
+ }
83
+
84
+ type Transition = (ps: ParseState, ch: string, i: number) => ParseState;
85
+ type Table = Readonly<Partial<Record<StateName, Partial<Record<Token, Transition>>>>>;
86
+
87
+ const push = (ps: ParseState, type: BraceOpen, index: number): ParseState =>
88
+ ({ ...ps, stack: [...ps.stack, { type, index }] });
89
+
90
+ const withEdit = (ps: ParseState, edit: Edit): ParseState =>
91
+ ({ ...ps, edits: [...ps.edits, edit] });
92
+
93
+ const popOrFix = (ps: ParseState, closer: BraceClose, idx: number): ParseState =>
94
+ ((): ParseState => {
95
+ if (ps.stack.length === 0) {
96
+ // Extra closing brace → delete
97
+ return withEdit(ps, { op: "delete", index: idx });
98
+ }
99
+ const top = ps.stack[ps.stack.length - 1];
100
+ if (top !== undefined && top.type !== openOf[closer]) {
101
+ // Type mismatch → replace with expected value + pop
102
+ const expected = closeOf[top.type];
103
+ return withEdit({ ...ps, stack: ps.stack.slice(0, -1) }, { op: "replace", index: idx, text: expected });
104
+ }
105
+ // Normal matching → pop
106
+ return { ...ps, stack: ps.stack.slice(0, -1) };
107
+ })();
108
+
109
+ const table: Table = {
110
+ OUT: {
111
+ DQUOTE: (ps) => ({ ...ps, s: "IN" }),
112
+ OCB: (ps, _ch, i) => push(ps, "{", i),
113
+ OSB: (ps, _ch, i) => push(ps, "[", i),
114
+ CCB: (ps, _ch, i) => popOrFix(ps, "}", i),
115
+ CSB: (ps, _ch, i) => popOrFix(ps, "]", i),
116
+ },
117
+ IN: {
118
+ BSLASH: (ps) => ({ ...ps, s: "ESC" }),
119
+ DQUOTE: (ps) => ({ ...ps, s: "OUT" }),
120
+ },
121
+ ESC: {
122
+ DQUOTE: (ps) => ({ ...ps, s: "IN" }),
123
+ BSLASH: (ps) => ({ ...ps, s: "IN" }),
124
+ OCB: (ps) => ({ ...ps, s: "IN" }),
125
+ OSB: (ps) => ({ ...ps, s: "IN" }),
126
+ CCB: (ps) => ({ ...ps, s: "IN" }),
127
+ CSB: (ps) => ({ ...ps, s: "IN" }),
128
+ CHAR: (ps) => ({ ...ps, s: "IN" }),
129
+ NEWLINE:(ps) => ({ ...ps, s: "IN" }),
130
+ },
131
+ };
132
+
133
+ type StateName = "OUT" | "IN" | "ESC";
134
+ type BraceOpen = "{" | "[";
135
+ type BraceClose = "}" | "]";
136
+ type Token = "DQUOTE" | "BSLASH" | "OCB" | "OSB" | "CCB" | "CSB" | "NEWLINE" | "CHAR";
137
+
138
+ type StackEntry = { type: BraceOpen; index: number };
139
+ type Edit =
140
+ | { op: "delete"; index: number }
141
+ | { op: "replace"; index: number; text: string }
142
+ | { op: "insert"; index: number; text: string };
143
+
144
+ type ParseState = {
145
+ s: StateName;
146
+ stack: ReadonlyArray<StackEntry>;
147
+ line: number;
148
+ col: number;
149
+ edits: ReadonlyArray<Edit>;
150
+ };
@@ -188,17 +188,6 @@ describe("streamUtil", () => {
188
188
  expect(emptyResult).toBe("initial value");
189
189
  });
190
190
 
191
- it("should return null for empty stream without initial value", async () => {
192
- const emptyNoInitialStream = createEmptyStream<number>();
193
- const emptyNoInitialResult = await StreamUtil.reduce<number>(
194
- emptyNoInitialStream,
195
- (acc, cur) => acc + cur,
196
- { initial: 0 },
197
- );
198
-
199
- expect(emptyNoInitialResult).toBeNull();
200
- });
201
-
202
191
  it("should work with async generated stream", async () => {
203
192
  const stringStream = await createDelayedNumberStream(1, 3, 10);
204
193
  const stringResult = await StreamUtil.reduce<number, string>(