@agent-native/core 0.84.39 → 0.84.41

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 (31) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/action-continuation-guidance.ts +38 -0
  5. package/corpus/core/src/agent/production-agent.ts +344 -34
  6. package/corpus/core/src/agent/run-loop-with-resume.ts +128 -16
  7. package/corpus/core/src/agent/types.ts +8 -0
  8. package/corpus/core/src/client/agent-chat-adapter.ts +2 -40
  9. package/dist/agent/action-continuation-guidance.d.ts +3 -0
  10. package/dist/agent/action-continuation-guidance.d.ts.map +1 -0
  11. package/dist/agent/action-continuation-guidance.js +38 -0
  12. package/dist/agent/action-continuation-guidance.js.map +1 -0
  13. package/dist/agent/production-agent.d.ts +8 -5
  14. package/dist/agent/production-agent.d.ts.map +1 -1
  15. package/dist/agent/production-agent.js +267 -32
  16. package/dist/agent/production-agent.js.map +1 -1
  17. package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
  18. package/dist/agent/run-loop-with-resume.js +69 -18
  19. package/dist/agent/run-loop-with-resume.js.map +1 -1
  20. package/dist/agent/types.d.ts +2 -0
  21. package/dist/agent/types.d.ts.map +1 -1
  22. package/dist/agent/types.js.map +1 -1
  23. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  24. package/dist/client/agent-chat-adapter.js +2 -39
  25. package/dist/client/agent-chat-adapter.js.map +1 -1
  26. package/dist/collab/routes.d.ts +1 -1
  27. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  28. package/dist/observability/routes.d.ts +5 -5
  29. package/dist/progress/routes.d.ts +1 -1
  30. package/dist/resources/handlers.d.ts +1 -1
  31. package/package.json +1 -1
@@ -16,6 +16,7 @@ import { readBody } from "../server/h3-helpers.js";
16
16
  import { getRequestRunContext, ensureRequestRunContext, getRequestContext, getRequestOrgId, getRequestUserEmail, runWithRequestContext, } from "../server/request-context.js";
17
17
  import { fireInternalDispatch } from "../server/self-dispatch.js";
18
18
  import { isReasoningEffort, normalizeReasoningEffortForModel, } from "../shared/reasoning-effort.js";
19
+ import { actionPreparationContinuationNote } from "./action-continuation-guidance.js";
19
20
  import { applyContextDirectives } from "./context-xray/apply-directives.js";
20
21
  import { loadContextDirectives } from "./context-xray/directives-store.js";
21
22
  import { buildManifest, writeContextManifest, } from "./context-xray/manifest.js";
@@ -558,6 +559,7 @@ function maxRetriesForError(err) {
558
559
  return MAX_RETRIES;
559
560
  }
560
561
  const TOOL_INPUT_ACTIVITY_INTERVAL_MS = 1500;
562
+ const ACTION_PREPARATION_NO_PROGRESS_TIMEOUT_MS = 90_000;
561
563
  const MAX_TEXT_ATTACHMENT_CHARS = 60_000;
562
564
  const MAX_SELECTION_CONTEXT_CHARS = 8_000;
563
565
  const MAX_RESOURCE_INVENTORY_ITEMS = 40;
@@ -1098,7 +1100,7 @@ function collectTextParts(parts) {
1098
1100
  .join("");
1099
1101
  }
1100
1102
  export const AGENT_INTERNAL_CONTINUE_PROMPT = "Continue from where you left off and finish the user's original request. Do not repeat completed work, do not mention internal reconnects, time limits, or step limits, and continue as if this is the same uninterrupted run.";
1101
- export function appendAgentLoopContinuation(messages, reason) {
1103
+ export function appendAgentLoopContinuation(messages, reason, options = {}) {
1102
1104
  const note = reason === "loop_limit"
1103
1105
  ? "The previous run reached an internal step budget."
1104
1106
  : reason === "max_tokens"
@@ -1109,17 +1111,31 @@ export function appendAgentLoopContinuation(messages, reason) {
1109
1111
  ? "The previous LLM call hit an upstream gateway timeout before the response finished streaming."
1110
1112
  : reason === "network_interrupted"
1111
1113
  ? "The previous LLM call was cut off by a transport-level interruption (socket dropped, connection reset, or stream closed unexpectedly)."
1112
- : "The previous run reached an internal execution budget.";
1114
+ : reason === "no_progress"
1115
+ ? "The previous run stopped producing progress events while the connection stayed open."
1116
+ : "The previous run reached an internal execution budget.";
1117
+ const actionInputNote = options.actionPreparationTool
1118
+ ? actionPreparationContinuationNote(options.actionPreparationTool)
1119
+ : "";
1113
1120
  messages.push({
1114
1121
  role: "user",
1115
1122
  content: [
1116
1123
  {
1117
1124
  type: "text",
1118
- text: `${AGENT_INTERNAL_CONTINUE_PROMPT}\n\nInternal note: ${note}`,
1125
+ text: `${AGENT_INTERNAL_CONTINUE_PROMPT}\n\nInternal note: ${note}${actionInputNote}`,
1119
1126
  },
1120
1127
  ],
1121
1128
  });
1122
1129
  }
1130
+ function isAgentLoopContinuationReason(reason) {
1131
+ return (reason === "run_timeout" ||
1132
+ reason === "loop_limit" ||
1133
+ reason === "max_tokens" ||
1134
+ reason === "stream_ended" ||
1135
+ reason === "gateway_timeout" ||
1136
+ reason === "network_interrupted" ||
1137
+ reason === "no_progress");
1138
+ }
1123
1139
  /**
1124
1140
  * True when an error thrown by `runAgentLoop` is a recoverable transport- or
1125
1141
  * gateway-level interruption that the agent can resume from rather than a
@@ -1960,6 +1976,8 @@ export async function runAgentLoop(opts) {
1960
1976
  const toolInputNames = new Map();
1961
1977
  const toolInputBytes = new Map();
1962
1978
  let lastToolInputActivityAt = 0;
1979
+ const activeToolInputs = new Map();
1980
+ let endedForActionPreparationNoProgress = false;
1963
1981
  const sendToolInputActivity = (toolName, toolInputId, progressBytes, force = false) => {
1964
1982
  const now = Date.now();
1965
1983
  if (!force &&
@@ -1975,7 +1993,56 @@ export async function runAgentLoop(opts) {
1975
1993
  ...(typeof progressBytes === "number" ? { progressBytes } : {}),
1976
1994
  });
1977
1995
  };
1996
+ const resetActiveToolInput = (toolName, toolInputId) => {
1997
+ if (toolInputId) {
1998
+ activeToolInputs.delete(toolInputId);
1999
+ return;
2000
+ }
2001
+ if (toolName) {
2002
+ for (const [id, active] of activeToolInputs) {
2003
+ if (active.toolName === toolName) {
2004
+ activeToolInputs.delete(id);
2005
+ }
2006
+ }
2007
+ }
2008
+ };
2009
+ const hasActionPreparationStalled = () => {
2010
+ if (activeToolInputs.size === 0)
2011
+ return false;
2012
+ const now = Date.now();
2013
+ for (const active of activeToolInputs.values()) {
2014
+ if (now - active.lastProgressAt >=
2015
+ ACTION_PREPARATION_NO_PROGRESS_TIMEOUT_MS) {
2016
+ return true;
2017
+ }
2018
+ }
2019
+ return false;
2020
+ };
2021
+ const trackActiveToolInput = (key, toolName, bytes) => {
2022
+ const now = Date.now();
2023
+ const previous = activeToolInputs.get(key);
2024
+ activeToolInputs.set(key, {
2025
+ id: key,
2026
+ ...((toolName ?? previous?.toolName)
2027
+ ? { toolName: toolName ?? previous?.toolName }
2028
+ : {}),
2029
+ startedAt: previous?.startedAt ?? now,
2030
+ lastProgressAt: now,
2031
+ bytes,
2032
+ });
2033
+ };
2034
+ const clearActiveToolInputs = () => {
2035
+ activeToolInputs.clear();
2036
+ };
1978
2037
  for await (const event of eventStream) {
2038
+ if (hasActionPreparationStalled()) {
2039
+ send({
2040
+ type: "auto_continue",
2041
+ reason: "no_progress",
2042
+ });
2043
+ endedForActionPreparationNoProgress = true;
2044
+ break;
2045
+ }
1979
2046
  // In-loop processor seam (stream hook). Each chunk is offered to every
1980
2047
  // processor's `processOutputStream` before the loop handles it. A
1981
2048
  // processor `abort()` throws a TripWire; catch it locally so it is not
@@ -2008,6 +2075,7 @@ export async function runAgentLoop(opts) {
2008
2075
  if (key && event.name) {
2009
2076
  toolInputNames.set(key, event.name);
2010
2077
  toolInputBytes.set(key, 0);
2078
+ trackActiveToolInput(key, event.name, 0);
2011
2079
  }
2012
2080
  sendToolInputActivity(event.name, key, undefined, true);
2013
2081
  }
@@ -2022,6 +2090,9 @@ export async function runAgentLoop(opts) {
2022
2090
  previous +
2023
2091
  new TextEncoder().encode(event.text ?? "").byteLength;
2024
2092
  toolInputBytes.set(key, progressBytes);
2093
+ if (progressBytes > previous) {
2094
+ trackActiveToolInput(key, toolName, progressBytes);
2095
+ }
2025
2096
  }
2026
2097
  sendToolInputActivity(toolName, key, progressBytes);
2027
2098
  }
@@ -2030,8 +2101,10 @@ export async function runAgentLoop(opts) {
2030
2101
  }
2031
2102
  else if (event.type === "tool-call") {
2032
2103
  // The authoritative tool-call blocks arrive in assistant-content.
2104
+ resetActiveToolInput(event.name, event.id);
2033
2105
  }
2034
2106
  else if (event.type === "tool-call-error") {
2107
+ resetActiveToolInput(event.name, event.id);
2035
2108
  toolCallErrors.set(event.id, {
2036
2109
  name: event.name,
2037
2110
  input: event.input,
@@ -2039,6 +2112,7 @@ export async function runAgentLoop(opts) {
2039
2112
  });
2040
2113
  }
2041
2114
  else if (event.type === "assistant-content") {
2115
+ clearActiveToolInputs();
2042
2116
  assistantContent = event.parts;
2043
2117
  }
2044
2118
  else if (event.type === "usage") {
@@ -2058,6 +2132,17 @@ export async function runAgentLoop(opts) {
2058
2132
  });
2059
2133
  }
2060
2134
  }
2135
+ if (hasActionPreparationStalled()) {
2136
+ send({
2137
+ type: "auto_continue",
2138
+ reason: "no_progress",
2139
+ });
2140
+ endedForActionPreparationNoProgress = true;
2141
+ break;
2142
+ }
2143
+ }
2144
+ if (endedForActionPreparationNoProgress) {
2145
+ return usage;
2061
2146
  }
2062
2147
  break;
2063
2148
  }
@@ -3043,6 +3128,141 @@ function endsAtInternalContinuationBoundary(run) {
3043
3128
  }
3044
3129
  return last.type === "error" && isRecoverableContinuationError(last);
3045
3130
  }
3131
+ function isPreparingActionActivityEvent(event) {
3132
+ if (event.type !== "activity")
3133
+ return false;
3134
+ const label = event.label.trim().toLowerCase();
3135
+ return label.startsWith("preparing ") && label.includes(" action");
3136
+ }
3137
+ export function lastUnfinishedPreparingActionToolFromEvents(events) {
3138
+ const active = new Map();
3139
+ const idlessToolStarts = new Map();
3140
+ const removeOldestMatchingActivePreparation = (tool, shouldRemove = () => true) => {
3141
+ let oldest;
3142
+ for (const [key, value] of active) {
3143
+ if (value.tool !== tool || !shouldRemove(value))
3144
+ continue;
3145
+ if (!oldest || value.order < oldest.order) {
3146
+ oldest = {
3147
+ key,
3148
+ order: value.order,
3149
+ };
3150
+ }
3151
+ }
3152
+ if (oldest)
3153
+ active.delete(oldest.key);
3154
+ return Boolean(oldest);
3155
+ };
3156
+ const removeMatchingActivePreparation = (event) => {
3157
+ const id = event.id?.trim();
3158
+ const tool = event.tool?.trim();
3159
+ if (!tool)
3160
+ return;
3161
+ if (id) {
3162
+ if (!active.delete(`id:${id}`)) {
3163
+ removeOldestMatchingActivePreparation(tool, (value) => !value.id);
3164
+ }
3165
+ return;
3166
+ }
3167
+ if (event.type === "tool_start") {
3168
+ if (removeOldestMatchingActivePreparation(tool)) {
3169
+ idlessToolStarts.set(tool, (idlessToolStarts.get(tool) ?? 0) + 1);
3170
+ }
3171
+ return;
3172
+ }
3173
+ const startedCount = idlessToolStarts.get(tool) ?? 0;
3174
+ if (startedCount > 0) {
3175
+ if (startedCount === 1) {
3176
+ idlessToolStarts.delete(tool);
3177
+ }
3178
+ else {
3179
+ idlessToolStarts.set(tool, startedCount - 1);
3180
+ }
3181
+ return;
3182
+ }
3183
+ removeOldestMatchingActivePreparation(tool);
3184
+ };
3185
+ events.forEach((event, order) => {
3186
+ if (isPreparingActionActivityEvent(event)) {
3187
+ const tool = event.tool?.trim();
3188
+ if (tool) {
3189
+ const id = event.id?.trim();
3190
+ const key = id ? `id:${id}` : `tool:${tool}:${order}`;
3191
+ active.set(key, {
3192
+ tool,
3193
+ order,
3194
+ ...(id ? { id } : {}),
3195
+ });
3196
+ }
3197
+ return;
3198
+ }
3199
+ if (event.type === "tool_start" || event.type === "tool_done") {
3200
+ removeMatchingActivePreparation(event);
3201
+ return;
3202
+ }
3203
+ if (event.type === "error" && isRecoverableContinuationError(event)) {
3204
+ return;
3205
+ }
3206
+ if (event.type === "clear" ||
3207
+ event.type === "done" ||
3208
+ event.type === "error" ||
3209
+ event.type === "missing_api_key") {
3210
+ active.clear();
3211
+ idlessToolStarts.clear();
3212
+ }
3213
+ });
3214
+ let latest;
3215
+ for (const value of active.values()) {
3216
+ if (!latest || value.order > latest.order) {
3217
+ latest = value;
3218
+ }
3219
+ }
3220
+ return latest?.tool;
3221
+ }
3222
+ function endsAfterCompletedToolWithoutAssistantFinal(run) {
3223
+ let completedToolAfterLastAssistantText = false;
3224
+ for (const { event } of run.events) {
3225
+ if (event.type === "text" && event.text.trim().length > 0) {
3226
+ completedToolAfterLastAssistantText = false;
3227
+ continue;
3228
+ }
3229
+ if (event.type === "tool_done" && event.isError !== true) {
3230
+ completedToolAfterLastAssistantText = true;
3231
+ continue;
3232
+ }
3233
+ if (event.type === "clear" ||
3234
+ event.type === "error" ||
3235
+ event.type === "missing_api_key" ||
3236
+ event.type === "auto_continue" ||
3237
+ event.type === "loop_limit") {
3238
+ completedToolAfterLastAssistantText = false;
3239
+ }
3240
+ }
3241
+ return completedToolAfterLastAssistantText;
3242
+ }
3243
+ function lastUnfinishedPreparingActionTool(run) {
3244
+ return lastUnfinishedPreparingActionToolFromEvents(run.events.map(({ event }) => event));
3245
+ }
3246
+ export function backgroundContinuationReasonForRun(run) {
3247
+ const last = run.events.at(-1)?.event;
3248
+ if (last?.type === "loop_limit")
3249
+ return "loop_limit";
3250
+ if (last?.type === "auto_continue" &&
3251
+ isAgentLoopContinuationReason(last.reason)) {
3252
+ return last.reason;
3253
+ }
3254
+ if (last?.type === "error" && isRecoverableContinuationError(last)) {
3255
+ return continuationReasonForResumableError(new EngineError(last.error, { errorCode: last.errorCode }));
3256
+ }
3257
+ if (endsAfterCompletedToolWithoutAssistantFinal(run)) {
3258
+ return "stream_ended";
3259
+ }
3260
+ return "run_timeout";
3261
+ }
3262
+ function endsAtContinuationBoundary(run) {
3263
+ return (endsAtInternalContinuationBoundary(run) ||
3264
+ endsAfterCompletedToolWithoutAssistantFinal(run));
3265
+ }
3046
3266
  /**
3047
3267
  * Hard cap on server-driven background→background continuation chunks for a
3048
3268
  * single logical turn. A `backgroundFunction` run gets a ~13-min soft timeout,
@@ -3054,14 +3274,13 @@ export const MAX_BACKGROUND_RUN_CONTINUATIONS = 20;
3054
3274
  /**
3055
3275
  * Whether the background worker should self-fire the next server-driven
3056
3276
  * continuation chunk. True only when this is a background worker run that ended
3057
- * at a recoverable soft-timeout boundary (not aborted/stopped) and the chain is
3058
- * still under its budget. Extracted so the decision is unit testable without
3059
- * booting the whole handler.
3277
+ * at a recoverable unfinished boundary (not aborted/stopped) and the chain is
3278
+ * still under its budget. Aborted / user-stopped runs do NOT chain.
3060
3279
  */
3061
3280
  export function shouldChainBackgroundContinuation(opts) {
3062
3281
  return (opts.isBackgroundWorker &&
3063
3282
  opts.run.status !== "aborted" &&
3064
- endsAtInternalContinuationBoundary(opts.run) &&
3283
+ endsAtContinuationBoundary(opts.run) &&
3065
3284
  opts.continuationCount < MAX_BACKGROUND_RUN_CONTINUATIONS);
3066
3285
  }
3067
3286
  export async function claimBackgroundWorkerRunEarly(opts) {
@@ -3809,7 +4028,16 @@ export function createProductionAgentHandler(options) {
3809
4028
  ?.threadData;
3810
4029
  const resumed = threadDataToEngineMessages(priorThreadData);
3811
4030
  if (resumed.length > 0) {
3812
- appendAgentLoopContinuation(resumed, "run_timeout");
4031
+ const actionPreparationTool = typeof backgroundRunMarker?.actionPreparationTool === "string" &&
4032
+ backgroundRunMarker.actionPreparationTool.trim()
4033
+ ? backgroundRunMarker.actionPreparationTool.trim()
4034
+ : undefined;
4035
+ const continuationReason = isAgentLoopContinuationReason(backgroundRunMarker?.continuationReason)
4036
+ ? backgroundRunMarker.continuationReason
4037
+ : "run_timeout";
4038
+ appendAgentLoopContinuation(resumed, continuationReason, {
4039
+ ...(actionPreparationTool ? { actionPreparationTool } : {}),
4040
+ });
3813
4041
  messages.length = 0;
3814
4042
  messages.push(...resumed);
3815
4043
  }
@@ -3972,10 +4200,15 @@ export function createProductionAgentHandler(options) {
3972
4200
  turnId: effectiveTurnId,
3973
4201
  }
3974
4202
  : null;
4203
+ const willChainBackgroundContinuation = (run) => shouldChainBackgroundContinuation({
4204
+ isBackgroundWorker,
4205
+ run,
4206
+ continuationCount: backgroundContinuationCount,
4207
+ });
3975
4208
  const completeTrackedProgressRun = async (run, completionError) => {
3976
4209
  if (!trackedProgressRunId || !trackedProgressOwner)
3977
4210
  return;
3978
- if (!completionError && endsAtInternalContinuationBoundary(run)) {
4211
+ if (!completionError && willChainBackgroundContinuation(run)) {
3979
4212
  return;
3980
4213
  }
3981
4214
  const terminalStatus = run.status === "aborted"
@@ -4060,7 +4293,7 @@ export function createProductionAgentHandler(options) {
4060
4293
  // below, they did not "throw").
4061
4294
  if (isBackgroundWorker &&
4062
4295
  run.status === "errored" &&
4063
- !endsAtInternalContinuationBoundary(run)) {
4296
+ !willChainBackgroundContinuation(run)) {
4064
4297
  const errEvent = [...run.events]
4065
4298
  .reverse()
4066
4299
  .find((e) => e.event.type === "error")?.event;
@@ -4081,34 +4314,32 @@ export function createProductionAgentHandler(options) {
4081
4314
  // agent-teams `fireInternalDispatch({ body: { mode: "continue" }})`
4082
4315
  // chain. Bounded by MAX_BACKGROUND_RUN_CONTINUATIONS. Aborted /
4083
4316
  // user-stopped runs do NOT chain.
4084
- if (shouldChainBackgroundContinuation({
4085
- // Self-chain server-side for EVERY durable worker, not only the
4086
- // ones inside a `-background` function. Server-driven
4087
- // continuation is the whole point of durable background: the run
4088
- // must survive the client disconnecting (closed tab), so it
4089
- // cannot depend on the browser re-POSTing `auto_continue`. A
4090
- // worker on the regular ~60s function — a Netlify routing miss,
4091
- // or a non-Netlify host (Vercel/Cloudflare/Render/Fly) that
4092
- // never emits a `-background` function checkpoints at the 40s
4093
- // soft-timeout and self-dispatches the next 40s chunk; a worker
4094
- // in a real `-background` function chains ~13-min chunks. Only
4095
- // the per-chunk BUDGET differs by function type (gated by
4096
- // `runsInBackgroundFunction` at the startRun call below); the
4097
- // continuation itself must stay server-driven on both. (The
4098
- // self-chain is only reachable when the initial dispatch already
4099
- // succeeded a dispatch fast-fail degrades to the inline
4100
- // foreground fallback, which is not a worker and rides the
4101
- // connected client's auto_continue instead.)
4102
- isBackgroundWorker,
4103
- run,
4104
- continuationCount: backgroundContinuationCount,
4105
- })) {
4317
+ // Self-chain server-side for EVERY durable worker, not only the
4318
+ // ones inside a `-background` function. Server-driven
4319
+ // continuation is the whole point of durable background: the run
4320
+ // must survive the client disconnecting (closed tab), so it
4321
+ // cannot depend on the browser re-POSTing `auto_continue`. A
4322
+ // worker on the regular ~60s function — a Netlify routing miss,
4323
+ // or a non-Netlify host (Vercel/Cloudflare/Render/Fly) that
4324
+ // never emits a `-background` function checkpoints at the 40s
4325
+ // soft-timeout and self-dispatches the next 40s chunk; a worker
4326
+ // in a real `-background` function chains ~13-min chunks. Only
4327
+ // the per-chunk BUDGET differs by function type (gated by
4328
+ // `runsInBackgroundFunction` at the startRun call below); the
4329
+ // continuation itself must stay server-driven on both. (The
4330
+ // self-chain is only reachable when the initial dispatch already
4331
+ // succeeded a dispatch fast-fail degrades to the inline
4332
+ // foreground fallback, which is not a worker and rides the
4333
+ // connected client's auto_continue instead.)
4334
+ if (willChainBackgroundContinuation(run)) {
4106
4335
  // Mint the next chunk's runId here and sign the dispatch token
4107
4336
  // over it, so the `_process-run` route's HMAC check and the
4108
4337
  // worker's run identity agree. Fresh runId (not this chunk's) so
4109
4338
  // its seq log starts clean; same turnId folds the assistant
4110
4339
  // message across chunks.
4111
4340
  const nextRunId = generateRunId();
4341
+ const actionPreparationTool = lastUnfinishedPreparingActionTool(run);
4342
+ const continuationReason = backgroundContinuationReasonForRun(run);
4112
4343
  const continuationDispatchPath = resolveAgentChatProcessRunDispatchPath();
4113
4344
  const continuationExpectsNetlifyBackgroundFunction = dispatchPathTargetsNetlifyBackgroundFunction(continuationDispatchPath);
4114
4345
  try {
@@ -4129,6 +4360,10 @@ export function createProductionAgentHandler(options) {
4129
4360
  runId: nextRunId,
4130
4361
  turnId: effectiveTurnId,
4131
4362
  continuationCount: backgroundContinuationCount + 1,
4363
+ continuationReason,
4364
+ ...(actionPreparationTool
4365
+ ? { actionPreparationTool }
4366
+ : {}),
4132
4367
  backgroundFunctionRuntimeExpected: continuationExpectsNetlifyBackgroundFunction,
4133
4368
  },
4134
4369
  },