@aparte/engine 0.13.1 → 0.15.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/dist/index.js CHANGED
@@ -1,13 +1,3 @@
1
- const CLOSE_TAG = "</artifact>";
2
- const OPEN_TAG = "<artifact";
3
- function partialOpenTagLength(text) {
4
- const max = Math.min(text.length, OPEN_TAG.length - 1);
5
- for (let k = max; k > 0; k--) {
6
- if (text.endsWith(OPEN_TAG.slice(0, k))) return k;
7
- }
8
- return 0;
9
- }
10
- const INLINE_MAX_LINES = 15;
11
1
  function deriveArtifactKind(mimeType, fallback = "unknown") {
12
2
  const m = (mimeType || "").toLowerCase().trim();
13
3
  const ant = m.match(/^application\/vnd\.ant\.([a-z0-9-]+)/);
@@ -30,119 +20,6 @@ function deriveArtifactKind(mimeType, fallback = "unknown") {
30
20
  if (m.includes("markdown")) return "markdown";
31
21
  return fallback;
32
22
  }
33
- class ArtifactXmlStateMachine {
34
- constructor(hint, idGen) {
35
- this.hint = hint;
36
- this.idGen = idGen ?? (() => `artifact-xml-${this.seq++}`);
37
- }
38
- state = "normal";
39
- /** Buffers the opening tag until its `>` arrives (may span deltas). */
40
- scanBuf = "";
41
- /** Buffers the tail that might be the start of a split `</artifact>`. */
42
- closeBuf = "";
43
- segId = null;
44
- content = "";
45
- mime = "";
46
- kind = "";
47
- title = "";
48
- seq = 0;
49
- idGen;
50
- /** Feed one text delta; returns the ordered micro-events it produced. */
51
- feed(delta) {
52
- const out = [];
53
- let remaining = delta;
54
- while (remaining.length > 0) {
55
- if (this.state === "normal") {
56
- const tagStart = remaining.indexOf(OPEN_TAG);
57
- if (tagStart === -1) {
58
- const held = partialOpenTagLength(remaining);
59
- if (held > 0) {
60
- const before = remaining.slice(0, remaining.length - held);
61
- if (before) out.push({ type: "chat-text", text: before, reduced: true });
62
- this.scanBuf = remaining.slice(remaining.length - held);
63
- this.state = "scanning";
64
- } else {
65
- out.push({ type: "chat-text", text: remaining });
66
- }
67
- remaining = "";
68
- } else {
69
- const before = remaining.slice(0, tagStart);
70
- if (before) out.push({ type: "chat-text", text: before, reduced: true });
71
- this.scanBuf = "";
72
- remaining = remaining.slice(tagStart);
73
- this.state = "scanning";
74
- }
75
- } else if (this.state === "scanning") {
76
- this.scanBuf += remaining;
77
- remaining = "";
78
- const cmp = Math.min(this.scanBuf.length, OPEN_TAG.length);
79
- if (this.scanBuf.slice(0, cmp) !== OPEN_TAG.slice(0, cmp)) {
80
- out.push({ type: "chat-text", text: this.scanBuf });
81
- this.scanBuf = "";
82
- this.state = "normal";
83
- continue;
84
- }
85
- const gtIdx = this.scanBuf.indexOf(">");
86
- if (gtIdx !== -1) {
87
- const tag = this.scanBuf.slice(0, gtIdx + 1);
88
- this.mime = /mimeType=['"]([^'"]+)['"]/.exec(tag)?.[1] ?? this.hint.mimeType;
89
- this.title = /title=['"]([^'"]+)['"]/.exec(tag)?.[1] ?? this.hint.kind;
90
- this.kind = deriveArtifactKind(this.mime, this.hint.kind);
91
- this.segId = this.idGen();
92
- this.content = "";
93
- out.push({ type: "artifact-open", id: this.segId, mimeType: this.mime, kind: this.kind, title: this.title });
94
- this.state = "in-artifact";
95
- remaining = this.scanBuf.slice(gtIdx + 1);
96
- this.scanBuf = "";
97
- }
98
- } else {
99
- const combined = this.closeBuf + remaining;
100
- const closeIdx = combined.indexOf(CLOSE_TAG);
101
- if (closeIdx !== -1) {
102
- this.content += combined.slice(0, closeIdx);
103
- const inline = this.content.split("\n").length < INLINE_MAX_LINES;
104
- out.push({ type: "artifact-close", id: this.segId, content: this.content, inline });
105
- this.state = "normal";
106
- this.closeBuf = "";
107
- remaining = combined.slice(closeIdx + CLOSE_TAG.length);
108
- } else {
109
- const safeLen = Math.max(0, combined.length - CLOSE_TAG.length + 1);
110
- this.content += combined.slice(0, safeLen);
111
- this.closeBuf = combined.slice(safeLen);
112
- remaining = "";
113
- if (this.segId) out.push({ type: "artifact-chunk", id: this.segId, content: this.content });
114
- }
115
- }
116
- }
117
- return out;
118
- }
119
- /**
120
- * Flush a truncated artifact: if the stream ended mid-body (model cut off
121
- * before `</artifact>`), emit a close with whatever was buffered. Mirrors the
122
- * `XML artifact finalize` block in `AparteClient._streamLoop`.
123
- */
124
- finalize() {
125
- if (this.state === "scanning") {
126
- const text = this.scanBuf;
127
- this.scanBuf = "";
128
- this.state = "normal";
129
- return text ? [{ type: "chat-text", text }] : [];
130
- }
131
- if (this.state === "in-artifact" && this.segId) {
132
- this.content += this.closeBuf;
133
- const inline = this.content.split("\n").length < INLINE_MAX_LINES;
134
- const ev = { type: "artifact-close", id: this.segId, content: this.content, inline };
135
- this.state = "normal";
136
- this.closeBuf = "";
137
- return [ev];
138
- }
139
- return [];
140
- }
141
- /** Current parser state (for the adapter to decide finalize routing). */
142
- get currentState() {
143
- return this.state;
144
- }
145
- }
146
23
  const DEFAULT_TOOL_TIMEOUT_MS = 5 * 60 * 1e3;
147
24
  const DEFAULT_MAX_TURNS = 10;
148
25
  const warnedUnknownEvents = /* @__PURE__ */ new Set();
@@ -173,8 +50,6 @@ async function runStreamAgent(opts) {
173
50
  messages.push(message);
174
51
  opts.onHistoryAppend?.(message);
175
52
  };
176
- const pipeline = baseRequest["_meta"]?.["pipeline"];
177
- let pipelineIndex = 0;
178
53
  let continueLoop = true;
179
54
  let turns = 0;
180
55
  let lastUsage;
@@ -211,21 +86,7 @@ async function runStreamAgent(opts) {
211
86
  append({ role: "tool_result", content: outcome.content, toolCallId: syntheticId });
212
87
  baseRequest = { ...baseRequest, toolChoice: "none", tools: void 0 };
213
88
  }
214
- let phaseMessages = messages;
215
- let phaseMeta = baseRequest["_meta"];
216
- if (pipeline && pipelineIndex < pipeline.length) {
217
- const phase = pipeline[pipelineIndex];
218
- phaseMessages = [{ role: "system", content: phase.system }, ...messages];
219
- if (phase.mode === "artifact") {
220
- phaseMeta = { ...phaseMeta, artifactRaw: { mimeType: phase.mimeType, kind: phase.kind } };
221
- } else {
222
- const rest = { ...phaseMeta ?? {} };
223
- delete rest["artifactRaw"];
224
- delete rest["pipeline"];
225
- phaseMeta = rest;
226
- }
227
- }
228
- const request = { ...baseRequest, messages: phaseMessages, _meta: phaseMeta };
89
+ const request = { ...baseRequest, messages };
229
90
  let response;
230
91
  try {
231
92
  response = await transportCall(request);
@@ -243,25 +104,14 @@ async function runStreamAgent(opts) {
243
104
  break;
244
105
  }
245
106
  emitter({ type: "turn-start" });
246
- const rawHint = request["_meta"]?.["artifactRaw"];
247
- let rawSegId = null;
248
- let rawContent = "";
249
- if (rawHint) {
250
- rawSegId = idGen("artifact-raw");
251
- emitter({ type: "artifact-open", id: rawSegId, mimeType: rawHint.mimeType, kind: rawHint.kind, title: rawHint.kind });
252
- }
253
- const xmlHint = request["_meta"]?.["artifactXml"];
254
- const xmlMachine = xmlHint && !rawHint ? new ArtifactXmlStateMachine(xmlHint, () => idGen("artifact-xml")) : null;
255
- const emitXml = (events) => {
256
- for (const ev of events) {
257
- if (ev.type === "chat-text") emitter({ type: "text-delta", delta: ev.text, ...ev.reduced ? { reduced: true } : {} });
258
- else if (ev.type === "artifact-open") emitter({ type: "artifact-open", id: ev.id, mimeType: ev.mimeType, kind: ev.kind, title: ev.title });
259
- else if (ev.type === "artifact-chunk") emitter({ type: "artifact-chunk", id: ev.id, content: ev.content });
260
- else emitter({ type: "artifact-close", id: ev.id, content: ev.content, inline: ev.inline });
261
- }
262
- };
263
107
  let precedingText = "";
264
108
  const toolCallsThisTurn = [];
109
+ let envelope = null;
110
+ const ensureEnvelope = () => {
111
+ if (envelope) return;
112
+ envelope = { role: "tool_call", content: "", toolCalls: toolCallsThisTurn, precedingText: precedingText.trim() || void 0 };
113
+ append(envelope);
114
+ };
265
115
  const iterator = response[Symbol.asyncIterator]();
266
116
  const bailOnAbort = async () => {
267
117
  if (!signal.aborted) return false;
@@ -283,15 +133,6 @@ async function runStreamAgent(opts) {
283
133
  }
284
134
  if (event.type === "text") {
285
135
  precedingText += event.delta;
286
- if (rawSegId) {
287
- rawContent += event.delta;
288
- emitter({ type: "artifact-chunk", id: rawSegId, content: rawContent });
289
- continue;
290
- }
291
- if (xmlMachine) {
292
- emitXml(xmlMachine.feed(event.delta));
293
- continue;
294
- }
295
136
  emitter({ type: "text-delta", delta: event.delta });
296
137
  continue;
297
138
  }
@@ -312,7 +153,7 @@ async function runStreamAgent(opts) {
312
153
  const mimeType = input.mimeType ?? "text/plain";
313
154
  const kind = deriveArtifactKind(mimeType, "text");
314
155
  emitter({ type: "artifact-ready", id: `artifact-${event.id}`, mimeType, kind, title: input.title ?? kind, content: input.content ?? "" });
315
- append({ role: "tool_call", content: "", toolCalls: [{ id: event.id, name: event.name, input: event.input }] });
156
+ ensureEnvelope();
316
157
  append({ role: "tool_result", content: "Artifact created successfully.", toolCallId: event.id });
317
158
  continue;
318
159
  }
@@ -338,11 +179,18 @@ async function runStreamAgent(opts) {
338
179
  continueLoop = false;
339
180
  break;
340
181
  }
341
- const decision = await approvalResolver(
342
- { id: event.id, name: event.name, input: event.input },
343
- signal
344
- );
345
- if (signal.aborted) {
182
+ let decision;
183
+ if (!signal.aborted) {
184
+ try {
185
+ decision = await approvalResolver(
186
+ { id: event.id, name: event.name, input: event.input },
187
+ signal
188
+ );
189
+ } catch (err) {
190
+ if (!signal.aborted && err?.name !== "AbortError") throw err;
191
+ }
192
+ }
193
+ if (signal.aborted || !decision) {
346
194
  emitter({ type: "tool-aborted", toolCallId: event.id });
347
195
  continueLoop = false;
348
196
  break;
@@ -350,7 +198,7 @@ async function runStreamAgent(opts) {
350
198
  if (!decision.approved) {
351
199
  const rejection = decision.instruction ? `The user rejected this tool call and said: ${decision.instruction}` : "Tool execution was rejected by the user.";
352
200
  emitter({ type: "tool-rejected", toolCallId: event.id, reason: rejection });
353
- pushToolCallEnvelope(messages, append, toolCallsThisTurn, precedingText);
201
+ ensureEnvelope();
354
202
  append({ role: "tool_result", content: rejection, toolCallId: event.id });
355
203
  break;
356
204
  }
@@ -371,47 +219,20 @@ async function runStreamAgent(opts) {
371
219
  break;
372
220
  } else {
373
221
  emitter({ type: "tool-resolved", toolCallId: event.id, result: outcome.content });
374
- pushToolCallEnvelope(messages, append, toolCallsThisTurn, precedingText);
222
+ ensureEnvelope();
375
223
  append({ role: "tool_result", content: outcome.content, toolCallId: event.id });
376
224
  }
377
225
  }
378
- if (xmlMachine) emitXml(xmlMachine.finalize());
379
226
  emitter({ type: "text-flush" });
380
- if (rawSegId) {
381
- const inline = rawContent.split("\n").length < 15;
382
- emitter({ type: "artifact-close", id: rawSegId, content: rawContent, inline });
383
- }
384
227
  } finally {
385
228
  await iterator.return?.(void 0).catch(() => {
386
229
  });
387
230
  }
388
- if (toolCallsThisTurn.length === 0) {
389
- if (pipeline && pipelineIndex < pipeline.length - 1) {
390
- if (precedingText.trim()) {
391
- append({ role: "assistant", content: precedingText.trim() });
392
- }
393
- pipelineIndex++;
394
- emitter({ type: "phase-advance", index: pipelineIndex });
395
- } else {
396
- continueLoop = false;
397
- }
398
- }
231
+ if (toolCallsThisTurn.length === 0) continueLoop = false;
399
232
  }
400
233
  emitter({ type: "run-done", usage: lastUsage });
401
234
  return lastUsage;
402
235
  }
403
- function pushToolCallEnvelope(messages, append, toolCallsThisTurn, precedingText) {
404
- const exists = messages.some(
405
- (m) => m.role === "tool_call" && m.toolCalls?.some((tc) => toolCallsThisTurn.some((t) => t.id === tc.id))
406
- );
407
- if (exists) return;
408
- append({
409
- role: "tool_call",
410
- content: "",
411
- toolCalls: toolCallsThisTurn,
412
- precedingText: precedingText.trim() || void 0
413
- });
414
- }
415
236
  async function invokeToolHandler(handler, call, signal, toolTimeoutMs) {
416
237
  if (signal.aborted) return { status: "aborted" };
417
238
  const controller = new AbortController();
@@ -617,12 +438,45 @@ function compactConversation(input) {
617
438
  };
618
439
  return { compactedMessages, breakdown };
619
440
  }
441
+ const read = (value) => typeof value === "function" ? value() : value;
442
+ const textOf = (message) => {
443
+ if (typeof message.content === "string" && message.content.length > 0) return message.content;
444
+ return (message.segments ?? []).map((segment) => {
445
+ const content = segment.content;
446
+ return typeof content === "string" ? content : "";
447
+ }).join("\n");
448
+ };
449
+ function createCompactionSelector(options) {
450
+ const minKeep = Math.max(0, options.minKeep ?? 2);
451
+ return (messages) => {
452
+ const contextWindow = read(options.contextWindow);
453
+ if (!contextWindow || contextWindow <= 0) return { keep: messages, drop: [] };
454
+ const budget = computeHistoryBudget({
455
+ systemPrompt: read(options.systemPrompt) ?? "",
456
+ toolsArray: read(options.tools),
457
+ config: { ...options.config, contextWindow }
458
+ });
459
+ const windowBudget = splitHistoryBudget(budget.historyBudget, budget.config).window;
460
+ let used = 0;
461
+ let cut = messages.length;
462
+ for (let i = messages.length - 1; i >= 0; i--) {
463
+ const cost = estimateTokens(textOf(messages[i]));
464
+ const kept = messages.length - 1 - i;
465
+ if (used + cost > windowBudget && kept >= minKeep) break;
466
+ used += cost;
467
+ cut = i;
468
+ }
469
+ if (cut === 0) return { keep: messages, drop: [] };
470
+ return { keep: messages.slice(cut), drop: messages.slice(0, cut) };
471
+ };
472
+ }
620
473
  export {
621
- ArtifactXmlStateMachine,
622
474
  DEFAULT_COMPACTION_CONFIG,
623
475
  assembleCompacted,
624
476
  compactConversation,
625
477
  computeHistoryBudget,
478
+ createCompactionSelector,
479
+ deriveArtifactKind,
626
480
  estimateTokens,
627
481
  estimateTokensJson,
628
482
  runStreamAgent,