agent-accelerator 0.1.0 → 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.
Files changed (48) hide show
  1. package/README.md +174 -123
  2. package/{SYSTEM_PROMPT_AGENT.md → examples/prompts/SYSTEM_PROMPT_AGENT.md} +1 -1
  3. package/package.json +14 -9
  4. package/src/agent/agent.ts +385 -73
  5. package/src/agent/context.ts +1 -0
  6. package/src/agent/delegation.ts +443 -30
  7. package/src/agent/loop.ts +183 -53
  8. package/src/data/README.md +6 -6
  9. package/src/index.ts +163 -46
  10. package/src/models/catalog-cache.ts +60 -9
  11. package/src/models/catalog.ts +55 -8
  12. package/src/providers/google.ts +926 -0
  13. package/src/providers/openai-compat.ts +1147 -0
  14. package/src/providers/openai.ts +959 -0
  15. package/src/providers/openrouter-responses.ts +949 -0
  16. package/src/providers/openrouter.ts +1037 -0
  17. package/src/{ai-sdk → providers}/registry.ts +43 -55
  18. package/src/providers.ts +492 -0
  19. package/src/session/store.ts +806 -0
  20. package/src/streaming/sse-parser.ts +6 -4
  21. package/src/tools/executor.ts +23 -7
  22. package/src/tools/schema.ts +21 -11
  23. package/src/types/agent.ts +33 -2
  24. package/src/types/core.ts +3 -2
  25. package/src/types/message.ts +5 -0
  26. package/src/types/model.ts +3 -7
  27. package/src/types/provider-payloads.ts +2 -84
  28. package/src/types/response.ts +24 -0
  29. package/src/types/tool.ts +8 -0
  30. package/src/update-models.ts +56 -0
  31. package/src/utils/cache.ts +1 -1
  32. package/src/utils/documents.ts +517 -0
  33. package/src/utils/env.ts +0 -7
  34. package/src/{ai-sdk → utils}/errors.ts +61 -2
  35. package/src/utils/headers.ts +10 -20
  36. package/src/utils/media.ts +5 -2
  37. package/src/utils/retry.ts +89 -0
  38. package/src/utils/serialization.ts +15 -0
  39. package/src/utils/session.ts +36 -0
  40. package/src/ai-sdk/converters.ts +0 -342
  41. package/src/ai-sdk/executor.ts +0 -454
  42. package/src/ai-sdk/index.ts +0 -55
  43. package/src/ai-sdk/model-provider.ts +0 -303
  44. package/src/ai-sdk/options.ts +0 -306
  45. package/src/ai-sdk/provider.ts +0 -415
  46. package/src/tokens/counter.ts +0 -136
  47. /package/{SYSTEM_PROMPT.md → examples/prompts/SYSTEM_PROMPT.md} +0 -0
  48. /package/{SYSTEM_PROMPT_TOOLS.md → examples/prompts/SYSTEM_PROMPT_TOOLS.md} +0 -0
@@ -0,0 +1,492 @@
1
+ /**
2
+ * Canonical provider contract for Agent Accelerator.
3
+ *
4
+ * This module is provider-agnostic: it defines how canonical Agent Accelerator
5
+ * capabilities map onto provider concepts WITHOUT containing any
6
+ * provider-specific HTTP, endpoints, headers, or response parsing. Those live
7
+ * in `src/providers/<provider>.ts` (e.g. `src/providers/google.ts`).
8
+ *
9
+ * Rule: OpenAI, OpenRouter, and future providers must be addable here without
10
+ * redesigning this abstraction — only new per-provider mappers/adapters.
11
+ */
12
+ import type { CacheConfig, ServiceTier, ThinkingLevel } from "./types/core.ts";
13
+
14
+ /** Classification of a canonical capability on a given provider. */
15
+ export type ProviderCapabilityStatus =
16
+ | "NATIVE"
17
+ | "TRANSLATED"
18
+ | "AUTOMATIC"
19
+ | "EMULATED"
20
+ | "UNSUPPORTED";
21
+
22
+ /**
23
+ * Single warning mechanism for provider capability differences.
24
+ *
25
+ * There is no pre-existing logger in `src/` (only CLI logs in
26
+ * `update-models.ts`), so this `console.warn` line IS the mechanism — do not
27
+ * introduce a second one. Used when a requested capability cannot be applied
28
+ * as-is (e.g. Google explicit cache retention) so it never silently
29
+ * disappears.
30
+ *
31
+ * Warnings are deduped per provider+capability+requested: agent loops call
32
+ * mappers once per turn, and repeating the same warning every turn is noise.
33
+ * A repeated identical request logs once per process.
34
+ *
35
+ * @example `emitProviderWarning({ provider: "google", capability: "cache retention", requested: "high", reason: "...", fallback: "..." })`
36
+ */
37
+ const emittedWarnings = new Set<string>();
38
+
39
+ export function emitProviderWarning(options: {
40
+ provider: string;
41
+ capability: string;
42
+ requested?: string;
43
+ reason: string;
44
+ fallback: string;
45
+ }): void {
46
+ const key = `${options.provider}::${options.capability}::${options.requested ?? ""}`;
47
+ if (emittedWarnings.has(key)) return;
48
+ emittedWarnings.add(key);
49
+ const requested = options.requested ? ` (requested: "${options.requested}")` : "";
50
+ // Leading newline: warnings fire at the start of a turn's request build,
51
+ // when the previous turn's streamed text may have left the terminal cursor
52
+ // mid-line. Without it the warning glues onto streamed output.
53
+ console.warn(
54
+ `\n[Agent Accelerator] WARNING [${options.provider}] ${options.capability}${requested}: ${options.reason} Using ${options.fallback} instead.`
55
+ );
56
+ }
57
+
58
+ /** Clears deduped-warning state (mainly for tests). */
59
+ export function clearEmittedWarnings(): void {
60
+ emittedWarnings.clear();
61
+ }
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Per-session provider routing state (canonical conversation stays agnostic)
65
+ // ---------------------------------------------------------------------------
66
+
67
+ interface SessionRoutingState {
68
+ /** Provider id that served the last turn in this session. */
69
+ lastProvider?: string;
70
+ /** True once a session mixed providers and must stay on explicit history. */
71
+ mixedProviders?: boolean;
72
+ }
73
+
74
+ const sessionRouting = new Map<string, SessionRoutingState>();
75
+
76
+ /** Records which provider served a turn so adapters can detect switches. */
77
+ export function noteProviderTurn(sessionId: string | undefined, providerId: string): void {
78
+ if (!sessionId) return;
79
+ const state = sessionRouting.get(sessionId) ?? {};
80
+ if (state.lastProvider && state.lastProvider !== providerId) {
81
+ state.mixedProviders = true;
82
+ }
83
+ state.lastProvider = providerId;
84
+ sessionRouting.set(sessionId, state);
85
+ }
86
+
87
+ /** Returns the provider id that served the previous turn in this session. */
88
+ export function lastProviderFor(sessionId: string | undefined): string | undefined {
89
+ if (!sessionId) return undefined;
90
+ return sessionRouting.get(sessionId)?.lastProvider;
91
+ }
92
+
93
+ /** True when this session already mixed providers (history must be explicit). */
94
+ export function isMixedProviderSession(sessionId: string | undefined): boolean {
95
+ if (!sessionId) return false;
96
+ return sessionRouting.get(sessionId)?.mixedProviders === true;
97
+ }
98
+
99
+ /** Clears routing state for a session (mainly for tests). */
100
+ export function clearSessionRouting(sessionId?: string): void {
101
+ if (sessionId) sessionRouting.delete(sessionId);
102
+ else sessionRouting.clear();
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // Canonical capability mappers (pure, no I/O)
107
+ // ---------------------------------------------------------------------------
108
+
109
+ /**
110
+ * Maps a canonical ThinkingLevel onto Google Interactions `thinking_level`.
111
+ * Google values: minimal|low|medium|high (no off switch, no xhigh/max).
112
+ */
113
+ export function mapThinkingLevelToGoogle(
114
+ level: ThinkingLevel | string | undefined
115
+ ): { thinkingLevel?: string } {
116
+ if (!level) return {};
117
+ const norm = String(level).toLowerCase().trim();
118
+ if (norm === "dynamic") return {}; // server dynamic default (NATIVE)
119
+ if (norm === "none") {
120
+ emitProviderWarning({
121
+ provider: "google",
122
+ capability: "thinking level",
123
+ requested: String(level),
124
+ reason: "the Interactions API has no disable/off level — thought steps are always present.",
125
+ fallback: "the server default (omit thinking_level)",
126
+ });
127
+ return {};
128
+ }
129
+ if (norm === "xhigh" || norm === "max") {
130
+ emitProviderWarning({
131
+ provider: "google",
132
+ capability: "thinking level",
133
+ requested: String(level),
134
+ reason: "Google's maximum thinking level is high.",
135
+ fallback: "thinking_level high",
136
+ });
137
+ return { thinkingLevel: "high" };
138
+ }
139
+ if (norm === "minimal" || norm === "low" || norm === "medium" || norm === "high") {
140
+ return { thinkingLevel: norm };
141
+ }
142
+ return {};
143
+ }
144
+
145
+ /** Maps canonical ServiceTier onto Google `service_tier` (omit = standard). */
146
+ export function mapServiceTierToGoogle(tier: ServiceTier | undefined): "flex" | "priority" | undefined {
147
+ if (tier === "flex" || tier === "priority") return tier;
148
+ return undefined;
149
+ }
150
+
151
+ /**
152
+ * Maps a canonical ThinkingLevel onto OpenRouter Chat Completions
153
+ * `reasoning.effort`.
154
+ *
155
+ * Completions documents `reasoning_effort: xhigh|high|medium|low|minimal|none`
156
+ * (parameters.md) and live probes accept `xhigh` verbatim (`03`), so — unlike
157
+ * the discontinued Responses skin, which clamped `xhigh` — everything passes
158
+ * through. `max` rides the same verbatim path. `dynamic` omits (server default).
159
+ */
160
+ export function mapThinkingLevelToOpenRouterChat(
161
+ level: ThinkingLevel | string | undefined
162
+ ): { effort?: string } {
163
+ if (!level) return {};
164
+ const norm = String(level).toLowerCase().trim();
165
+ if (norm === "dynamic") return {}; // server default
166
+ if (
167
+ norm === "none" ||
168
+ norm === "minimal" ||
169
+ norm === "low" ||
170
+ norm === "medium" ||
171
+ norm === "high" ||
172
+ norm === "xhigh" ||
173
+ norm === "max"
174
+ ) {
175
+ return { effort: norm };
176
+ }
177
+ return {};
178
+ }
179
+
180
+ /**
181
+ * Maps a canonical ThinkingLevel onto OpenRouter Responses `reasoning.effort`.
182
+ * Documented efforts: minimal|low|medium|high (server default medium).
183
+ * Wire-validated extras: `none` disables reasoning output; `xhigh`/`max` are
184
+ * echoed but are NOT documented levels, so they clamp to `high` with a warning.
185
+ *
186
+ * @deprecated The Responses skin is discontinued for OpenRouter
187
+ * (`src/providers/openrouter-responses.ts`, archived). Use
188
+ * {@link mapThinkingLevelToOpenRouterChat} with the stable Chat Completions
189
+ * transport instead.
190
+ */
191
+ export function mapThinkingLevelToOpenRouter(
192
+ level: ThinkingLevel | string | undefined
193
+ ): { effort?: string } {
194
+ if (!level) return {};
195
+ const norm = String(level).toLowerCase().trim();
196
+ if (norm === "dynamic") return {}; // server default (medium)
197
+ if (norm === "none") return { effort: "none" };
198
+ if (norm === "xhigh" || norm === "max") {
199
+ emitProviderWarning({
200
+ provider: "openrouter",
201
+ capability: "thinking level",
202
+ requested: String(level),
203
+ reason: "OpenRouter documents reasoning efforts up to high.",
204
+ fallback: "reasoning effort high",
205
+ });
206
+ return { effort: "high" };
207
+ }
208
+ if (norm === "minimal" || norm === "low" || norm === "medium" || norm === "high") {
209
+ return { effort: norm };
210
+ }
211
+ return {};
212
+ }
213
+
214
+ /** Maps canonical ServiceTier onto OpenRouter `service_tier` (omit = auto). */
215
+ export function mapServiceTierToOpenRouter(tier: ServiceTier | undefined): "flex" | "priority" | undefined {
216
+ if (tier === "flex" || tier === "priority") return tier;
217
+ return undefined;
218
+ }
219
+
220
+ /**
221
+ * Applies canonical cache config for OpenRouter. Session affinity flows via
222
+ * top-level body `session_id` (sent by the adapter) plus the `x-session-id`
223
+ * header fallback; there is no retention body primitive, so explicit
224
+ * retention/cachedContentId are UNSUPPORTED: warn and drop.
225
+ */
226
+ export function applyCacheForOpenRouter(
227
+ cache: CacheConfig | undefined,
228
+ modelRef: string
229
+ ): void {
230
+ if (!cache) return;
231
+ if (cache.retention && cache.retention !== "implicit") {
232
+ emitProviderWarning({
233
+ provider: "openrouter",
234
+ capability: "cache retention",
235
+ requested: `${cache.retention} (${modelRef})`,
236
+ reason: "OpenRouter documents no retention control on the stable Chat Completions endpoint.",
237
+ fallback: "default gateway caching with headers-only session affinity (no retention payload is sent)",
238
+ });
239
+ }
240
+ if (cache.cachedContentId) {
241
+ emitProviderWarning({
242
+ provider: "openrouter",
243
+ capability: "explicit cached content",
244
+ requested: cache.cachedContentId,
245
+ reason: "cached content references do not exist on the Responses API.",
246
+ fallback: "full-history sends instead (cachedContentId is ignored)",
247
+ });
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Applies canonical cache config for custom OpenAI-compatible endpoints.
253
+ * Affinity is headers-only (`x-session-id`): unknown body properties make
254
+ * strict endpoints (groq, ollama, …) fail, so nothing cache-related is ever
255
+ * placed in the body. Explicit retention/cachedContentId are UNSUPPORTED:
256
+ * warn and drop.
257
+ */
258
+ export function applyCacheForCustom(
259
+ prefix: string,
260
+ cache: CacheConfig | undefined,
261
+ modelRef: string
262
+ ): void {
263
+ if (!cache) return;
264
+ if (cache.retention && cache.retention !== "implicit") {
265
+ emitProviderWarning({
266
+ provider: prefix,
267
+ capability: "cache retention",
268
+ requested: `${cache.retention} (${modelRef})`,
269
+ reason: "custom OpenAI-compatible endpoints define no retention control, and strict endpoints reject unknown body properties.",
270
+ fallback: "default caching with headers-only session affinity (no retention payload is sent)",
271
+ });
272
+ }
273
+ if (cache.cachedContentId) {
274
+ emitProviderWarning({
275
+ provider: prefix,
276
+ capability: "explicit cached content",
277
+ requested: cache.cachedContentId,
278
+ reason: "cached content references do not exist on OpenAI-compatible endpoints.",
279
+ fallback: "full-history sends instead (cachedContentId is ignored)",
280
+ });
281
+ }
282
+ }
283
+
284
+ /** OpenRouter Chat Completions `tool_choice` wire values. */
285
+ export type OpenRouterChatToolChoice =
286
+ | "auto"
287
+ | "none"
288
+ | "required"
289
+ | { type: "function"; function: { name: string } };
290
+
291
+ /**
292
+ * Maps canonical toolChoice onto Chat Completions `tool_choice`.
293
+ * `auto` is the server default (omitted); `required` is documented
294
+ * (parameters.md) and accepted live (`05`); a function pin passes through
295
+ * verbatim. Note the shape differs from the discontinued Responses skin
296
+ * (`{type:function,name}`): completions nests the name under `function`.
297
+ */
298
+ export function mapToolChoiceToOpenRouterChat(
299
+ choice: "auto" | "none" | "required" | { type: "function"; function: { name: string } } | undefined
300
+ ): OpenRouterChatToolChoice | undefined {
301
+ if (!choice || choice === "auto") return undefined;
302
+ if (choice === "none" || choice === "required") return choice;
303
+ const name = choice.function?.name;
304
+ if (name) return { type: "function", function: { name } };
305
+ return undefined;
306
+ }
307
+
308
+ /** OpenRouter Responses `tool_choice` wire values.
309
+ * @deprecated Discontinued skin; see {@link OpenRouterChatToolChoice}. */
310
+ export type OpenRouterToolChoice = "auto" | "none" | "required" | { type: "function"; name: string };
311
+
312
+ /**
313
+ * Maps canonical toolChoice onto OpenRouter Responses `tool_choice`.
314
+ * `auto` is the server default (omitted); `required` is natively supported
315
+ * (validated); a function pin passes through verbatim.
316
+ */
317
+ export function mapToolChoiceToOpenRouter(
318
+ choice: "auto" | "none" | "required" | { type: "function"; function: { name: string } } | undefined
319
+ ): OpenRouterToolChoice | undefined {
320
+ if (!choice || choice === "auto") return undefined;
321
+ if (choice === "none" || choice === "required") return choice;
322
+ const name = choice.function?.name;
323
+ if (name) return { type: "function", name };
324
+ return undefined;
325
+ }
326
+
327
+ /**
328
+ * Maps a canonical ThinkingLevel onto OpenAI Responses `reasoning.effort`.
329
+ * Documented efforts: none|minimal|low|medium|high|xhigh|max (server default
330
+ * medium). Unlike OpenRouter (which clamps xhigh), OpenAI documents xhigh
331
+ * and max natively so they pass through verbatim. `dynamic` omits (server default).
332
+ */
333
+ export function mapThinkingLevelToOpenAI(
334
+ level: ThinkingLevel | string | undefined
335
+ ): { effort?: string } {
336
+ if (!level) return {};
337
+ const norm = String(level).toLowerCase().trim();
338
+ if (norm === "dynamic") return {}; // server default
339
+ if (
340
+ norm === "none" ||
341
+ norm === "minimal" ||
342
+ norm === "low" ||
343
+ norm === "medium" ||
344
+ norm === "high" ||
345
+ norm === "xhigh" ||
346
+ norm === "max"
347
+ ) {
348
+ return { effort: norm };
349
+ }
350
+ return {};
351
+ }
352
+
353
+ /** Maps canonical ServiceTier onto OpenAI `service_tier` (omit = auto). */
354
+ export function mapServiceTierToOpenAI(tier: ServiceTier | undefined): "flex" | "priority" | undefined {
355
+ if (tier === "flex" || tier === "priority") return tier;
356
+ return undefined;
357
+ }
358
+
359
+ /**
360
+ * Applies canonical cache config for OpenAI. Session affinity flows via
361
+ * `prompt_cache_key` (handled by the adapter); explicit retention control
362
+ * lives in `prompt_cache_options` (gpt-5.6+ explicit breakpoints) which is
363
+ * out of scope for the most-important subset, so retention/cachedContentId
364
+ * are UNSUPPORTED: warn and drop. `prompt_cache_retention` is deprecated.
365
+ */
366
+ export function applyCacheForOpenAI(
367
+ cache: CacheConfig | undefined,
368
+ modelRef: string
369
+ ): void {
370
+ if (!cache) return;
371
+ if (cache.retention && cache.retention !== "implicit") {
372
+ emitProviderWarning({
373
+ provider: "openai",
374
+ capability: "cache retention",
375
+ requested: `${cache.retention} (${modelRef})`,
376
+ reason: "the native Responses adapter uses prompt_cache_key affinity only; explicit retention modes are out of scope.",
377
+ fallback: "default caching with prompt_cache_key affinity (no retention payload is sent)",
378
+ });
379
+ }
380
+ if (cache.cachedContentId) {
381
+ emitProviderWarning({
382
+ provider: "openai",
383
+ capability: "explicit cached content",
384
+ requested: cache.cachedContentId,
385
+ reason: "cached content references do not exist on the Responses API.",
386
+ fallback: "full-history sends instead (cachedContentId is ignored)",
387
+ });
388
+ }
389
+ }
390
+
391
+ /** OpenAI Responses `tool_choice` wire values (most-important subset). */
392
+ export type OpenAIToolChoice = "auto" | "none" | "required" | { type: "function"; name: string };
393
+
394
+ /**
395
+ * Maps canonical toolChoice onto OpenAI Responses `tool_choice`.
396
+ * `auto` is the server default (omitted); `required` forces one or more
397
+ * calls; a function pin passes through verbatim. Built-in/MCP/allowed_tools
398
+ * variants are out of scope and never emitted.
399
+ */
400
+ export function mapToolChoiceToOpenAI(
401
+ choice: "auto" | "none" | "required" | { type: "function"; function: { name: string } } | undefined
402
+ ): OpenAIToolChoice | undefined {
403
+ if (!choice || choice === "auto") return undefined;
404
+ if (choice === "none" || choice === "required") return choice;
405
+ const name = choice.function?.name;
406
+ if (name) return { type: "function", name };
407
+ return undefined;
408
+ }
409
+
410
+ /**
411
+ * Applies canonical cache config for Google. The Interactions API supports
412
+ * implicit/automatic caching ONLY — there is no cache payload to send.
413
+ * Explicit retention/cachedContentId are UNSUPPORTED: warn and drop.
414
+ */
415
+ export function applyCacheForGoogle(
416
+ cache: CacheConfig | undefined,
417
+ modelRef: string
418
+ ): void {
419
+ if (!cache) return;
420
+ if (cache.retention && cache.retention !== "implicit") {
421
+ emitProviderWarning({
422
+ provider: "google",
423
+ capability: "cache retention",
424
+ requested: `${cache.retention} (${modelRef})`,
425
+ reason: "the Interactions API does not support user-defined explicit cache retention.",
426
+ fallback: "Google automatic/implicit caching (no cache payload is sent)",
427
+ });
428
+ }
429
+ if (cache.cachedContentId) {
430
+ emitProviderWarning({
431
+ provider: "google",
432
+ capability: "explicit cached content",
433
+ requested: cache.cachedContentId,
434
+ reason: "cachedContents resources do not exist on the Interactions API.",
435
+ fallback: "stateful interaction chaining / stateless history instead (cachedContentId is ignored)",
436
+ });
437
+ }
438
+ }
439
+
440
+ /** Normalized tool-choice mode shared by provider adapters. */
441
+ export type CanonicalToolChoiceMode = "auto" | "any" | "none";
442
+
443
+ /**
444
+ * Normalizes canonical toolChoice into a provider-neutral mode + optional
445
+ * pinned tool name. Google renders this as
446
+ * `{ allowed_tools: { mode, tools? } }` in its own adapter.
447
+ */
448
+ export function normalizeToolChoice(
449
+ choice: "auto" | "none" | "required" | { type: "function"; function: { name: string } } | undefined,
450
+ availableToolNames: string[]
451
+ ): { mode: CanonicalToolChoiceMode; tools?: string[] } | undefined {
452
+ if (!choice) return undefined;
453
+ if (typeof choice === "string") {
454
+ if (choice === "auto") return undefined; // server default
455
+ if (choice === "none") return { mode: "none" };
456
+ if (choice === "required") {
457
+ return availableToolNames.length > 0 ? { mode: "any", tools: availableToolNames } : { mode: "any" };
458
+ }
459
+ return undefined;
460
+ }
461
+ const name = choice.function?.name;
462
+ if (name) return { mode: "any", tools: [name] };
463
+ return { mode: "any" };
464
+ }
465
+
466
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
467
+ return !!value && typeof value === "object" && !Array.isArray(value);
468
+ }
469
+
470
+ /**
471
+ * Reconstructs tool arguments from streamed chunks.
472
+ *
473
+ * Wire behavior varies across providers: some send the complete JSON in a
474
+ * single delta after an empty initial payload, others genuinely split
475
+ * partials across start + deltas. Concatenating blindly can yield
476
+ * `"{}{...}"` (unparseable), so candidates are tried in order and the first
477
+ * chunk that parses to a plain object wins.
478
+ *
479
+ * @example `parseStreamedToolArguments("{}", '{"location":"Paris"}')`
480
+ */
481
+ export function parseStreamedToolArguments(startText: string, deltaText: string): Record<string, unknown> {
482
+ const candidates = [startText + deltaText, deltaText, startText].filter((c) => c && c.trim());
483
+ for (const candidate of candidates) {
484
+ try {
485
+ const parsed: unknown = JSON.parse(candidate);
486
+ if (isPlainObject(parsed)) return parsed;
487
+ } catch {
488
+ // try next candidate
489
+ }
490
+ }
491
+ return { raw: startText + deltaText };
492
+ }