agent-accelerator 0.1.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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1220 -0
  3. package/SYSTEM_PROMPT.md +14 -0
  4. package/SYSTEM_PROMPT_AGENT.md +34 -0
  5. package/SYSTEM_PROMPT_TOOLS.md +9 -0
  6. package/bunfig.toml +2 -0
  7. package/package.json +59 -0
  8. package/src/agent/agent.ts +615 -0
  9. package/src/agent/context.ts +161 -0
  10. package/src/agent/delegation.ts +481 -0
  11. package/src/agent/loop.ts +569 -0
  12. package/src/agent/subagent.ts +83 -0
  13. package/src/ai-sdk/converters.ts +342 -0
  14. package/src/ai-sdk/errors.ts +122 -0
  15. package/src/ai-sdk/executor.ts +454 -0
  16. package/src/ai-sdk/index.ts +55 -0
  17. package/src/ai-sdk/model-provider.ts +303 -0
  18. package/src/ai-sdk/options.ts +306 -0
  19. package/src/ai-sdk/provider.ts +415 -0
  20. package/src/ai-sdk/registry.ts +416 -0
  21. package/src/data/README.md +84 -0
  22. package/src/index.ts +190 -0
  23. package/src/models/catalog-cache.ts +273 -0
  24. package/src/models/catalog.ts +503 -0
  25. package/src/streaming/event-stream.ts +211 -0
  26. package/src/streaming/sse-parser.ts +97 -0
  27. package/src/tokens/counter.ts +136 -0
  28. package/src/tools/executor.ts +365 -0
  29. package/src/tools/schema.ts +221 -0
  30. package/src/tools/tool.ts +101 -0
  31. package/src/types/agent.ts +87 -0
  32. package/src/types/core.ts +86 -0
  33. package/src/types/message.ts +115 -0
  34. package/src/types/model.ts +212 -0
  35. package/src/types/provider-payloads.ts +434 -0
  36. package/src/types/response.ts +158 -0
  37. package/src/types/tool.ts +61 -0
  38. package/src/utils/base64.ts +27 -0
  39. package/src/utils/cache.ts +146 -0
  40. package/src/utils/env.ts +78 -0
  41. package/src/utils/headers.ts +110 -0
  42. package/src/utils/media.ts +137 -0
  43. package/src/utils/serialization.ts +91 -0
  44. package/src/utils/session.ts +26 -0
  45. package/src/utils/thought-signature.ts +27 -0
  46. package/tsconfig.json +31 -0
@@ -0,0 +1,211 @@
1
+ import type { StreamEvent, AgentResponse } from "../types/response.ts";
2
+
3
+ /** Listener invoked for a matching stream event type or `*`. */
4
+ export type StreamEventListener = (event: StreamEvent) => void;
5
+
6
+ type Resolver = {
7
+ resolve: (value: IteratorResult<StreamEvent>) => void;
8
+ reject: (err: Error) => void;
9
+ };
10
+
11
+ /** Async-iterable event stream returned by Agent.stream and streaming Agent.run. */
12
+ export class AssistantMessageEventStream implements AsyncIterable<StreamEvent> {
13
+ private queue: StreamEvent[] = [];
14
+ private resolvers: Resolver[] = [];
15
+ private listeners: Map<string, Set<StreamEventListener>> = new Map();
16
+ private finished = false;
17
+ private error: Error | null = null;
18
+ private cancelled = false;
19
+ private cancelHandlers = new Set<() => void>();
20
+ private finalResultPromise: Promise<AgentResponse>;
21
+ private resolveFinalResult!: (result: AgentResponse) => void;
22
+ private rejectFinalResult!: (err: Error) => void;
23
+
24
+ /**
25
+ * Creates an empty event stream. `Agent.stream()` creates and completes
26
+ * streams automatically; this constructor is useful for custom adapters.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * const stream = new AssistantMessageEventStream();
31
+ * stream.on("text_delta", (event) => process.stdout.write(event.delta ?? ""));
32
+ * ```
33
+ */
34
+ constructor() {
35
+ this.finalResultPromise = new Promise<AgentResponse>((resolve, reject) => {
36
+ this.resolveFinalResult = resolve;
37
+ this.rejectFinalResult = reject;
38
+ });
39
+ // Prevent unhandled rejection when no one awaits result()
40
+ this.finalResultPromise.catch(() => {});
41
+ }
42
+
43
+ /** Publishes an event to listeners and async iterators. */
44
+ push(event: StreamEvent): void {
45
+ if (this.finished) return;
46
+
47
+ if (event.type === "error") {
48
+ this.error = (event.error instanceof Error ? event.error : new Error(String(event.error))) ?? new Error("Unknown error in stream");
49
+ // Reject final result but don't finish iterator yet — let iterators receive error event then fail
50
+ // Still deliver error event to listeners/consumers before marking finished? Keep consistent with agent-accel: error terminates stream
51
+ try { this.rejectFinalResult(this.error); } catch {}
52
+ }
53
+
54
+ // Trigger registered event listener callbacks
55
+ const specificListeners = this.listeners.get(event.type);
56
+ if (specificListeners) {
57
+ for (const listener of specificListeners) {
58
+ try { listener(event); } catch {}
59
+ }
60
+ }
61
+ const allListeners = this.listeners.get("*");
62
+ if (allListeners) {
63
+ for (const listener of allListeners) {
64
+ try { listener(event); } catch {}
65
+ }
66
+ }
67
+
68
+ if (event.type === "error") {
69
+ // Mark finished after delivering error event, reject pending resolvers
70
+ this.finished = true;
71
+ while (this.resolvers.length > 0) {
72
+ const r = this.resolvers.shift()!;
73
+ // First deliver error event if they were waiting, then reject next call
74
+ // For simplicity, reject immediately — consumer will see error via result() or next()
75
+ r.reject(this.error!);
76
+ }
77
+ // Also queue error event for later pulls if no resolver
78
+ if (this.queue.length === 0) this.queue.push(event);
79
+ else this.queue.push(event);
80
+ return;
81
+ }
82
+
83
+ if (this.resolvers.length > 0) {
84
+ const resolver = this.resolvers.shift()!;
85
+ resolver.resolve({ value: event, done: false });
86
+ } else {
87
+ this.queue.push(event);
88
+ }
89
+ }
90
+
91
+ /** Completes the stream and resolves result() with the final response. */
92
+ end(finalResponse?: AgentResponse): void {
93
+ if (this.finished) return;
94
+ this.finished = true;
95
+
96
+ if (finalResponse) {
97
+ this.resolveFinalResult(finalResponse);
98
+ } else {
99
+ // Resolve with empty if no response — should not happen, but prevent hanging
100
+ // Do not reject; just finish
101
+ }
102
+
103
+ // Flush any waiting resolvers as done
104
+ while (this.resolvers.length > 0) {
105
+ const resolver = this.resolvers.shift()!;
106
+ resolver.resolve({ value: undefined as any, done: true });
107
+ }
108
+ }
109
+
110
+ /** Fails the stream and rejects pending consumers. */
111
+ fail(error: Error): void {
112
+ if (this.finished) return;
113
+ // Delegate to push for unified error handling (listeners + queue + rejection)
114
+ this.push({ type: "error", error } as StreamEvent);
115
+ }
116
+
117
+ /** True once cancel() has been called. Background work should stop. */
118
+ isCancelled(): boolean {
119
+ return this.cancelled;
120
+ }
121
+
122
+ /** Registers a callback invoked once on cancel(). Returns an unsubscribe fn. */
123
+ onCancel(handler: () => void): () => void {
124
+ if (this.cancelled) {
125
+ try { handler(); } catch {}
126
+ return () => {};
127
+ }
128
+ this.cancelHandlers.add(handler);
129
+ return () => { this.cancelHandlers.delete(handler); };
130
+ }
131
+
132
+ /** Cancels the stream: stops background work and rejects result() with AbortError.
133
+ * Safe to call multiple times. Breaking out of for-await calls this automatically. */
134
+ cancel(reason?: unknown): void {
135
+ if (this.finished || this.cancelled) return;
136
+ this.cancelled = true;
137
+ for (const h of [...this.cancelHandlers]) {
138
+ try { h(); } catch {}
139
+ }
140
+ this.cancelHandlers.clear();
141
+ const err = reason instanceof Error ? reason : null;
142
+ const abortErr: Error =
143
+ err && (err.name === "AbortError" || /abort|cancel/i.test(err.message))
144
+ ? err
145
+ : Object.assign(new Error("Stream aborted"), { name: "AbortError" });
146
+ this.fail(abortErr);
147
+ }
148
+
149
+ /** Registers a listener for an event type or `*`. */
150
+ on(event: string, listener: StreamEventListener): this {
151
+ if (!this.listeners.has(event)) {
152
+ this.listeners.set(event, new Set());
153
+ }
154
+ this.listeners.get(event)!.add(listener);
155
+ return this;
156
+ }
157
+
158
+ /** Removes a previously registered listener. */
159
+ off(event: string, listener: StreamEventListener): this {
160
+ this.listeners.get(event)?.delete(listener);
161
+ return this;
162
+ }
163
+
164
+ /** Resolves when the stream finishes with the normalized AgentResponse. */
165
+ result(): Promise<AgentResponse> {
166
+ return this.finalResultPromise;
167
+ }
168
+
169
+ [Symbol.asyncIterator](): AsyncIterator<StreamEvent> {
170
+ let iteratorDone = false;
171
+ return {
172
+ return: (): Promise<IteratorResult<StreamEvent>> => {
173
+ if (!iteratorDone) {
174
+ iteratorDone = true;
175
+ this.cancel();
176
+ }
177
+ return Promise.resolve({ value: undefined as any, done: true });
178
+ },
179
+ throw: (err?: unknown): Promise<IteratorResult<StreamEvent>> => {
180
+ if (!iteratorDone) {
181
+ iteratorDone = true;
182
+ this.cancel(err);
183
+ }
184
+ return Promise.reject(err);
185
+ },
186
+ next: (): Promise<IteratorResult<StreamEvent>> => {
187
+ if (this.queue.length > 0) {
188
+ const value = this.queue.shift()!;
189
+ // If queued error, next call should reject, but deliver error event first?
190
+ // Deliver error event then on next call reject
191
+ if (value.type === "error" && this.error) {
192
+ // Return error event as value, next next() will reject
193
+ return Promise.resolve({ value, done: false });
194
+ }
195
+ return Promise.resolve({ value, done: false });
196
+ }
197
+
198
+ if (this.finished) {
199
+ if (this.error) {
200
+ return Promise.reject(this.error);
201
+ }
202
+ return Promise.resolve({ value: undefined as any, done: true });
203
+ }
204
+
205
+ return new Promise<IteratorResult<StreamEvent>>((resolve, reject) => {
206
+ this.resolvers.push({ resolve, reject });
207
+ });
208
+ },
209
+ };
210
+ }
211
+ }
@@ -0,0 +1,97 @@
1
+ /** Parsed Server-Sent Events message. */
2
+ export interface SSEMessage {
3
+ id?: string;
4
+ event?: string;
5
+ data: string;
6
+ }
7
+
8
+ /**
9
+ * Incremental SSE parser that preserves events split across network chunks.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const parser = new SSEParser();
14
+ * const events = parser.feed("event: message\\ndata: hello\\n\\n");
15
+ * ```
16
+ */
17
+ export class SSEParser {
18
+ private buffer = "";
19
+ // Persist across feed() calls — otherwise split SSE messages lose state (S1 fix)
20
+ private currentEvent: string | undefined = undefined;
21
+ private currentData: string[] = [];
22
+ private currentId: string | undefined = undefined;
23
+
24
+ /** Feeds a chunk and returns all complete messages found in it. */
25
+ feed(chunk: string): SSEMessage[] {
26
+ this.buffer += chunk;
27
+ const messages: SSEMessage[] = [];
28
+
29
+ let start = 0;
30
+ while (start < this.buffer.length) {
31
+ const newlineIdx = this.buffer.indexOf("\n", start);
32
+ if (newlineIdx === -1) {
33
+ break;
34
+ }
35
+
36
+ let lineEnd = newlineIdx;
37
+ if (lineEnd > start && this.buffer.charCodeAt(lineEnd - 1) === 13) {
38
+ lineEnd--;
39
+ }
40
+
41
+ const line = this.buffer.slice(start, lineEnd);
42
+ start = newlineIdx + 1;
43
+
44
+ if (line.length === 0) {
45
+ if (this.currentData.length > 0) {
46
+ messages.push({
47
+ id: this.currentId,
48
+ event: this.currentEvent,
49
+ data: this.currentData.join("\n"),
50
+ });
51
+ this.currentEvent = undefined;
52
+ this.currentData = [];
53
+ this.currentId = undefined;
54
+ }
55
+ } else if (line.charCodeAt(0) === 58) {
56
+ // Comment / ping
57
+ continue;
58
+ } else if (line.startsWith("data:")) {
59
+ const val = line.slice(5);
60
+ this.currentData.push(val.startsWith(" ") ? val.slice(1) : val.trimStart());
61
+ } else if (line.startsWith("event:")) {
62
+ const val = line.slice(6);
63
+ this.currentEvent = (val.startsWith(" ") ? val.slice(1) : val).trim();
64
+ } else if (line.startsWith("id:")) {
65
+ const val = line.slice(3);
66
+ this.currentId = (val.startsWith(" ") ? val.slice(1) : val).trim();
67
+ }
68
+ }
69
+
70
+ this.buffer = this.buffer.slice(start);
71
+ return messages;
72
+ }
73
+
74
+ /** Flushes any unterminated final event. */
75
+ flush(): SSEMessage[] {
76
+ const messages: SSEMessage[] = [];
77
+ // S7: flush pending currentData first, then treat remaining buffer as final lines (avoid duplication)
78
+ if (this.buffer.trim() !== "") {
79
+ // Feed remaining buffer as if terminated
80
+ const pending = this.feed(this.buffer + "\n\n");
81
+ messages.push(...pending);
82
+ this.buffer = "";
83
+ }
84
+ if (this.currentData.length > 0) {
85
+ messages.push({
86
+ id: this.currentId,
87
+ event: this.currentEvent,
88
+ data: this.currentData.join("\n"),
89
+ });
90
+ this.currentEvent = undefined;
91
+ this.currentData = [];
92
+ this.currentId = undefined;
93
+ }
94
+ this.buffer = "";
95
+ return messages;
96
+ }
97
+ }
@@ -0,0 +1,136 @@
1
+ import type { Message, ContentPart } from "../types/message.ts";
2
+ import type { ProviderContext } from "../types/message.ts";
3
+
4
+ /**
5
+ * High-speed token estimation utility.
6
+ * For English text, average is ~4 characters per token or ~0.75 words per token.
7
+ * Code and symbols average ~2-3 characters per token.
8
+ */
9
+ /**
10
+ * Estimates token count from text without contacting a provider tokenizer.
11
+ *
12
+ * @example `const tokens = estimateTokensFromText(prompt);`
13
+ */
14
+ export function estimateTokensFromText(text: string): number {
15
+ if (!text || text.length === 0) return 0;
16
+ const chars = text.length;
17
+ let words = 0;
18
+ let inWord = false;
19
+ let codeMarkers = 0;
20
+
21
+ for (let i = 0; i < chars; i++) {
22
+ const code = text.charCodeAt(i);
23
+ // Whitespace: space(32), tab(9), LF(10), CR(13)
24
+ if (code === 32 || code === 9 || code === 10 || code === 13) {
25
+ if (inWord) {
26
+ words++;
27
+ inWord = false;
28
+ }
29
+ } else {
30
+ inWord = true;
31
+ // Code markers: { (123), } (125), ; (59), = (61), < (60), > (62), / (47), \ (92)
32
+ if (code === 123 || code === 125 || code === 59 || code === 61 || code === 60 || code === 62 || code === 47 || code === 92) {
33
+ codeMarkers++;
34
+ }
35
+ }
36
+ }
37
+ if (inWord) words++;
38
+
39
+ const isCodeHeavy = codeMarkers > chars * 0.05;
40
+ const charDivisor = isCodeHeavy ? 3.0 : 3.8;
41
+ const wordFactor = isCodeHeavy ? 1.6 : 1.3;
42
+ return Math.ceil(Math.max(chars / charDivisor, words * wordFactor));
43
+ }
44
+
45
+ /**
46
+ * Estimates tokens for one multimodal, thinking, or tool content part.
47
+ *
48
+ * @example `const tokens = estimateTokensFromPart({ type: "text", text: prompt });`
49
+ */
50
+ export function estimateTokensFromPart(part: ContentPart): number {
51
+ switch (part.type) {
52
+ case "text":
53
+ return estimateTokensFromText(part.text);
54
+ case "thinking":
55
+ return estimateTokensFromText(part.thinking);
56
+ case "image":
57
+ // Standard vision token cost ~258 tokens per low-res / 1000-1600 per high-res tile
58
+ return 258;
59
+ case "audio":
60
+ // ~25 tokens per second of audio
61
+ return 150;
62
+ case "video":
63
+ // ~250 tokens per frame / minute
64
+ return 500;
65
+ case "file":
66
+ return 400;
67
+ case "tool_call":
68
+ return (
69
+ estimateTokensFromText(part.name) +
70
+ estimateTokensFromText(JSON.stringify(part.arguments)) +
71
+ 15
72
+ );
73
+ case "tool_result":
74
+ return (
75
+ estimateTokensFromText(part.name) +
76
+ estimateTokensFromText(
77
+ typeof part.result === "string" ? part.result : JSON.stringify(part.result)
78
+ ) +
79
+ 10
80
+ );
81
+ default:
82
+ return 0;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Estimates tokens for one normalized message including metadata overhead.
88
+ *
89
+ * @example `const tokens = estimateTokensFromMessage({ role: "user", content: "Hi" });`
90
+ */
91
+ export function estimateTokensFromMessage(message: Message): number {
92
+ let count = 4; // Message metadata overhead (role, markers)
93
+ if (typeof message.content === "string") {
94
+ count += estimateTokensFromText(message.content);
95
+ } else if (Array.isArray(message.content)) {
96
+ for (const part of message.content) {
97
+ count += estimateTokensFromPart(part);
98
+ }
99
+ }
100
+ return count;
101
+ }
102
+
103
+ /**
104
+ * Estimates tokens for text, messages, or a complete ProviderContext.
105
+ *
106
+ * @example `const tokens = countTokens({ messages });`
107
+ */
108
+ export function countTokens(
109
+ input: string | Message[] | ProviderContext
110
+ ): number {
111
+ if (typeof input === "string") {
112
+ return estimateTokensFromText(input);
113
+ }
114
+
115
+ if (Array.isArray(input)) {
116
+ let total = 0;
117
+ for (const msg of input) {
118
+ total += estimateTokensFromMessage(msg);
119
+ }
120
+ return total;
121
+ }
122
+
123
+ let total = 0;
124
+ if (input.systemPrompt) {
125
+ total += estimateTokensFromText(input.systemPrompt) + 5;
126
+ }
127
+ if (input.messages) {
128
+ for (const msg of input.messages) {
129
+ total += estimateTokensFromMessage(msg);
130
+ }
131
+ }
132
+ if (input.tools) {
133
+ total += estimateTokensFromText(JSON.stringify(input.tools)) + 20;
134
+ }
135
+ return total;
136
+ }