@elevasis/sdk 1.41.0 → 1.42.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/cli.cjs CHANGED
@@ -45834,7 +45834,7 @@ function wrapAction(commandName, fn) {
45834
45834
  // package.json
45835
45835
  var package_default = {
45836
45836
  name: "@elevasis/sdk",
45837
- version: "1.41.0",
45837
+ version: "1.42.0",
45838
45838
  description: "SDK for building Elevasis organization resources",
45839
45839
  type: "module",
45840
45840
  bin: {
package/dist/index.d.ts CHANGED
@@ -502,6 +502,15 @@ interface ModelConfig {
502
502
  modelOptions?: ModelSpecificOptions;
503
503
  }
504
504
 
505
+ /**
506
+ * What happened to `strict` on a request, recorded per call rather than inferred.
507
+ *
508
+ * `applied` and `notAttempted` are the two states that a refusal-only field cannot tell apart —
509
+ * both leave `strictRefusalReasons` empty. Recording the verdict positively is what makes "was
510
+ * this agent's output actually enforced?" answerable from an `ai_calls` row.
511
+ */
512
+ type StrictStatus = 'applied' | 'refused' | 'compileRejected' | 'notAttempted';
513
+
505
514
  declare const ResourceGovernanceStatusSchema: z.ZodEnum<{
506
515
  active: "active";
507
516
  deprecated: "deprecated";
@@ -1029,6 +1038,7 @@ interface WorkflowDefinition {
1029
1038
  * Generic LLM Types
1030
1039
  * Universal interfaces for LLM interaction across all resource types
1031
1040
  */
1041
+
1032
1042
  /**
1033
1043
  * Standard chat message format
1034
1044
  * Compatible with OpenAI, Anthropic, and other providers
@@ -1062,15 +1072,30 @@ interface LLMGenerateResponse<T = unknown> {
1062
1072
  totalTokens: number;
1063
1073
  };
1064
1074
  cost?: number;
1075
+ /**
1076
+ * What actually happened to `strict` on the request that produced this response. Every server
1077
+ * adapter sets it on every call, so the value is a statement rather than an inference:
1078
+ *
1079
+ * - `applied` — the request carried `strict: true` and the grammar was in effect
1080
+ * - `refused` — `toStrictSchema` could not express the schema, so the request went out unstrict
1081
+ * - `compileRejected` — the schema passed `toStrictSchema` but the provider's grammar compiler
1082
+ * rejected it at request time, and the call was retried unstrict
1083
+ * - `notAttempted` — this adapter does not send strict at all (OpenAI, Google, OpenRouter)
1084
+ *
1085
+ * This exists because `strictRefusalReasons` alone cannot answer the question. Its absence means
1086
+ * "strict held" OR "nothing ever tried", and a prod run that recorded zero refusals while
1087
+ * returning an array-typed field as a string is exactly the case where the difference matters.
1088
+ *
1089
+ * Internal-only, like `usage` — `UniversalLLMAdapter` lifts it onto the `ai_calls` row and
1090
+ * strips it before the response reaches callers.
1091
+ */
1092
+ strictStatus?: StrictStatus;
1065
1093
  /**
1066
1094
  * Why this call went out WITHOUT `strict`, on an adapter that tried to send it with one.
1067
1095
  *
1068
- * Present only on a refusal, so absence means either "strict was in effect" or "this adapter
1069
- * does not attempt strict at all" the two are distinguished by which adapter answered, not by
1070
- * this field. `toStrictSchema` already computes these reasons and, until now, nothing consumed
1071
- * them at the call site: an unstrict call was indistinguishable from a strict one anywhere
1072
- * outside a dev-only flow log. That invisibility is what let every tenant run unstrict against a
1073
- * strict-capable API for as long as it took someone to recognise a pre-strict error signature.
1096
+ * The detail behind a `refused` / `compileRejected` `strictStatus` the short, stable reason
1097
+ * strings `toStrictSchema` computes. Read `strictStatus` to answer "was it enforced"; read this
1098
+ * to answer "why not".
1074
1099
  *
1075
1100
  * Internal-only, like `usage` — `UniversalLLMAdapter` lifts it onto the `ai_calls` row and
1076
1101
  * strips it before the response reaches callers.
@@ -11248,15 +11273,24 @@ interface BaseAICall {
11248
11273
  */
11249
11274
  unvalidatedOutput?: string;
11250
11275
  /**
11251
- * Why this call went out without `strict` structured output, on an adapter that tried to send it
11252
- * with one. Present ONLY on a refusal its absence on a row from a strict-capable adapter means
11253
- * strict was in effect.
11276
+ * What happened to `strict` structured output on this call `applied`, `refused`,
11277
+ * `compileRejected`, or `notAttempted`. Every server adapter sets it, so this is the field that
11278
+ * answers "is this agent's output actually being enforced?" without reading source.
11254
11279
  *
11255
- * This is the row that answers "is this agent's output actually being enforced?" without reading
11256
- * source. Counting rows that carry it, per resource, is the refusal count: a redeployed agent on
11257
- * a current bundle should read zero. It exists because the previous answer was a dev-only flow
11258
- * log, which meant production had no answer and every sync-managed tenant ran unstrict against
11259
- * a strict-capable API until a 14-turn session surfaced a pre-strict error signature.
11280
+ * It replaces an inference that turned out to be unsound. `strictRefusalReasons` alone records
11281
+ * only refusals, so an empty row meant "strict held" OR "this adapter never tries" and a prod
11282
+ * run recorded zero refusals while a call returned an array-typed field as a string, which a
11283
+ * grammar makes impossible. The absence proved nothing, because the deployed API had no way to
11284
+ * write the field at all.
11285
+ *
11286
+ * Absent on rows written before this field existed; that absence is itself diagnostic (the API
11287
+ * predates the change). Existing readers that only look at the fields above are unaffected.
11288
+ */
11289
+ strictStatus?: StrictStatus;
11290
+ /**
11291
+ * Why this call went out without `strict`, when a strict-capable adapter refused the schema. The
11292
+ * detail behind `strictStatus: 'refused' | 'compileRejected'` — read `strictStatus` for whether
11293
+ * it was enforced, this for why not.
11260
11294
  *
11261
11295
  * Existing readers that only look at the fields above are unaffected.
11262
11296
  */
@@ -11316,6 +11350,8 @@ interface LLMUsageData {
11316
11350
  outputValidationError?: string;
11317
11351
  /** Raw model output that failed validation — JSON-stringified, truncated, truncation marked inline */
11318
11352
  unvalidatedOutput?: string;
11353
+ /** What happened to `strict` on this call — set by every server adapter, refusal or not */
11354
+ strictStatus?: StrictStatus;
11319
11355
  /** Why the call went out unstrict, when a strict-capable adapter refused the schema */
11320
11356
  strictRefusalReasons?: string[];
11321
11357
  }
@@ -372,6 +372,15 @@ interface ModelConfig {
372
372
  modelOptions?: ModelSpecificOptions;
373
373
  }
374
374
 
375
+ /**
376
+ * What happened to `strict` on a request, recorded per call rather than inferred.
377
+ *
378
+ * `applied` and `notAttempted` are the two states that a refusal-only field cannot tell apart —
379
+ * both leave `strictRefusalReasons` empty. Recording the verdict positively is what makes "was
380
+ * this agent's output actually enforced?" answerable from an `ai_calls` row.
381
+ */
382
+ type StrictStatus = 'applied' | 'refused' | 'compileRejected' | 'notAttempted';
383
+
375
384
  declare const ResourceGovernanceStatusSchema: z.ZodEnum<{
376
385
  active: "active";
377
386
  deprecated: "deprecated";
@@ -899,6 +908,7 @@ interface WorkflowDefinition {
899
908
  * Generic LLM Types
900
909
  * Universal interfaces for LLM interaction across all resource types
901
910
  */
911
+
902
912
  /**
903
913
  * Standard chat message format
904
914
  * Compatible with OpenAI, Anthropic, and other providers
@@ -932,15 +942,30 @@ interface LLMGenerateResponse<T = unknown> {
932
942
  totalTokens: number;
933
943
  };
934
944
  cost?: number;
945
+ /**
946
+ * What actually happened to `strict` on the request that produced this response. Every server
947
+ * adapter sets it on every call, so the value is a statement rather than an inference:
948
+ *
949
+ * - `applied` — the request carried `strict: true` and the grammar was in effect
950
+ * - `refused` — `toStrictSchema` could not express the schema, so the request went out unstrict
951
+ * - `compileRejected` — the schema passed `toStrictSchema` but the provider's grammar compiler
952
+ * rejected it at request time, and the call was retried unstrict
953
+ * - `notAttempted` — this adapter does not send strict at all (OpenAI, Google, OpenRouter)
954
+ *
955
+ * This exists because `strictRefusalReasons` alone cannot answer the question. Its absence means
956
+ * "strict held" OR "nothing ever tried", and a prod run that recorded zero refusals while
957
+ * returning an array-typed field as a string is exactly the case where the difference matters.
958
+ *
959
+ * Internal-only, like `usage` — `UniversalLLMAdapter` lifts it onto the `ai_calls` row and
960
+ * strips it before the response reaches callers.
961
+ */
962
+ strictStatus?: StrictStatus;
935
963
  /**
936
964
  * Why this call went out WITHOUT `strict`, on an adapter that tried to send it with one.
937
965
  *
938
- * Present only on a refusal, so absence means either "strict was in effect" or "this adapter
939
- * does not attempt strict at all" the two are distinguished by which adapter answered, not by
940
- * this field. `toStrictSchema` already computes these reasons and, until now, nothing consumed
941
- * them at the call site: an unstrict call was indistinguishable from a strict one anywhere
942
- * outside a dev-only flow log. That invisibility is what let every tenant run unstrict against a
943
- * strict-capable API for as long as it took someone to recognise a pre-strict error signature.
966
+ * The detail behind a `refused` / `compileRejected` `strictStatus` the short, stable reason
967
+ * strings `toStrictSchema` computes. Read `strictStatus` to answer "was it enforced"; read this
968
+ * to answer "why not".
944
969
  *
945
970
  * Internal-only, like `usage` — `UniversalLLMAdapter` lifts it onto the `ai_calls` row and
946
971
  * strips it before the response reaches callers.
@@ -2749,15 +2774,24 @@ interface BaseAICall {
2749
2774
  */
2750
2775
  unvalidatedOutput?: string;
2751
2776
  /**
2752
- * Why this call went out without `strict` structured output, on an adapter that tried to send it
2753
- * with one. Present ONLY on a refusal its absence on a row from a strict-capable adapter means
2754
- * strict was in effect.
2777
+ * What happened to `strict` structured output on this call `applied`, `refused`,
2778
+ * `compileRejected`, or `notAttempted`. Every server adapter sets it, so this is the field that
2779
+ * answers "is this agent's output actually being enforced?" without reading source.
2755
2780
  *
2756
- * This is the row that answers "is this agent's output actually being enforced?" without reading
2757
- * source. Counting rows that carry it, per resource, is the refusal count: a redeployed agent on
2758
- * a current bundle should read zero. It exists because the previous answer was a dev-only flow
2759
- * log, which meant production had no answer and every sync-managed tenant ran unstrict against
2760
- * a strict-capable API until a 14-turn session surfaced a pre-strict error signature.
2781
+ * It replaces an inference that turned out to be unsound. `strictRefusalReasons` alone records
2782
+ * only refusals, so an empty row meant "strict held" OR "this adapter never tries" and a prod
2783
+ * run recorded zero refusals while a call returned an array-typed field as a string, which a
2784
+ * grammar makes impossible. The absence proved nothing, because the deployed API had no way to
2785
+ * write the field at all.
2786
+ *
2787
+ * Absent on rows written before this field existed; that absence is itself diagnostic (the API
2788
+ * predates the change). Existing readers that only look at the fields above are unaffected.
2789
+ */
2790
+ strictStatus?: StrictStatus;
2791
+ /**
2792
+ * Why this call went out without `strict`, when a strict-capable adapter refused the schema. The
2793
+ * detail behind `strictStatus: 'refused' | 'compileRejected'` — read `strictStatus` for whether
2794
+ * it was enforced, this for why not.
2761
2795
  *
2762
2796
  * Existing readers that only look at the fields above are unaffected.
2763
2797
  */
@@ -2817,6 +2851,8 @@ interface LLMUsageData {
2817
2851
  outputValidationError?: string;
2818
2852
  /** Raw model output that failed validation — JSON-stringified, truncated, truncation marked inline */
2819
2853
  unvalidatedOutput?: string;
2854
+ /** What happened to `strict` on this call — set by every server adapter, refusal or not */
2855
+ strictStatus?: StrictStatus;
2820
2856
  /** Why the call went out unstrict, when a strict-capable adapter refused the schema */
2821
2857
  strictRefusalReasons?: string[];
2822
2858
  }
@@ -335,6 +335,15 @@ interface ModelConfig {
335
335
  modelOptions?: ModelSpecificOptions;
336
336
  }
337
337
 
338
+ /**
339
+ * What happened to `strict` on a request, recorded per call rather than inferred.
340
+ *
341
+ * `applied` and `notAttempted` are the two states that a refusal-only field cannot tell apart —
342
+ * both leave `strictRefusalReasons` empty. Recording the verdict positively is what makes "was
343
+ * this agent's output actually enforced?" answerable from an `ai_calls` row.
344
+ */
345
+ type StrictStatus = 'applied' | 'refused' | 'compileRejected' | 'notAttempted';
346
+
338
347
  declare const ResourceGovernanceStatusSchema: z.ZodEnum<{
339
348
  active: "active";
340
349
  deprecated: "deprecated";
@@ -862,6 +871,7 @@ interface WorkflowDefinition {
862
871
  * Generic LLM Types
863
872
  * Universal interfaces for LLM interaction across all resource types
864
873
  */
874
+
865
875
  /**
866
876
  * Standard chat message format
867
877
  * Compatible with OpenAI, Anthropic, and other providers
@@ -895,15 +905,30 @@ interface LLMGenerateResponse<T = unknown> {
895
905
  totalTokens: number;
896
906
  };
897
907
  cost?: number;
908
+ /**
909
+ * What actually happened to `strict` on the request that produced this response. Every server
910
+ * adapter sets it on every call, so the value is a statement rather than an inference:
911
+ *
912
+ * - `applied` — the request carried `strict: true` and the grammar was in effect
913
+ * - `refused` — `toStrictSchema` could not express the schema, so the request went out unstrict
914
+ * - `compileRejected` — the schema passed `toStrictSchema` but the provider's grammar compiler
915
+ * rejected it at request time, and the call was retried unstrict
916
+ * - `notAttempted` — this adapter does not send strict at all (OpenAI, Google, OpenRouter)
917
+ *
918
+ * This exists because `strictRefusalReasons` alone cannot answer the question. Its absence means
919
+ * "strict held" OR "nothing ever tried", and a prod run that recorded zero refusals while
920
+ * returning an array-typed field as a string is exactly the case where the difference matters.
921
+ *
922
+ * Internal-only, like `usage` — `UniversalLLMAdapter` lifts it onto the `ai_calls` row and
923
+ * strips it before the response reaches callers.
924
+ */
925
+ strictStatus?: StrictStatus;
898
926
  /**
899
927
  * Why this call went out WITHOUT `strict`, on an adapter that tried to send it with one.
900
928
  *
901
- * Present only on a refusal, so absence means either "strict was in effect" or "this adapter
902
- * does not attempt strict at all" the two are distinguished by which adapter answered, not by
903
- * this field. `toStrictSchema` already computes these reasons and, until now, nothing consumed
904
- * them at the call site: an unstrict call was indistinguishable from a strict one anywhere
905
- * outside a dev-only flow log. That invisibility is what let every tenant run unstrict against a
906
- * strict-capable API for as long as it took someone to recognise a pre-strict error signature.
929
+ * The detail behind a `refused` / `compileRejected` `strictStatus` the short, stable reason
930
+ * strings `toStrictSchema` computes. Read `strictStatus` to answer "was it enforced"; read this
931
+ * to answer "why not".
907
932
  *
908
933
  * Internal-only, like `usage` — `UniversalLLMAdapter` lifts it onto the `ai_calls` row and
909
934
  * strips it before the response reaches callers.
@@ -10322,15 +10347,24 @@ interface BaseAICall {
10322
10347
  */
10323
10348
  unvalidatedOutput?: string;
10324
10349
  /**
10325
- * Why this call went out without `strict` structured output, on an adapter that tried to send it
10326
- * with one. Present ONLY on a refusal its absence on a row from a strict-capable adapter means
10327
- * strict was in effect.
10350
+ * What happened to `strict` structured output on this call `applied`, `refused`,
10351
+ * `compileRejected`, or `notAttempted`. Every server adapter sets it, so this is the field that
10352
+ * answers "is this agent's output actually being enforced?" without reading source.
10328
10353
  *
10329
- * This is the row that answers "is this agent's output actually being enforced?" without reading
10330
- * source. Counting rows that carry it, per resource, is the refusal count: a redeployed agent on
10331
- * a current bundle should read zero. It exists because the previous answer was a dev-only flow
10332
- * log, which meant production had no answer and every sync-managed tenant ran unstrict against
10333
- * a strict-capable API until a 14-turn session surfaced a pre-strict error signature.
10354
+ * It replaces an inference that turned out to be unsound. `strictRefusalReasons` alone records
10355
+ * only refusals, so an empty row meant "strict held" OR "this adapter never tries" and a prod
10356
+ * run recorded zero refusals while a call returned an array-typed field as a string, which a
10357
+ * grammar makes impossible. The absence proved nothing, because the deployed API had no way to
10358
+ * write the field at all.
10359
+ *
10360
+ * Absent on rows written before this field existed; that absence is itself diagnostic (the API
10361
+ * predates the change). Existing readers that only look at the fields above are unaffected.
10362
+ */
10363
+ strictStatus?: StrictStatus;
10364
+ /**
10365
+ * Why this call went out without `strict`, when a strict-capable adapter refused the schema. The
10366
+ * detail behind `strictStatus: 'refused' | 'compileRejected'` — read `strictStatus` for whether
10367
+ * it was enforced, this for why not.
10334
10368
  *
10335
10369
  * Existing readers that only look at the fields above are unaffected.
10336
10370
  */
@@ -10390,6 +10424,8 @@ interface LLMUsageData {
10390
10424
  outputValidationError?: string;
10391
10425
  /** Raw model output that failed validation — JSON-stringified, truncated, truncation marked inline */
10392
10426
  unvalidatedOutput?: string;
10427
+ /** What happened to `strict` on this call — set by every server adapter, refusal or not */
10428
+ strictStatus?: StrictStatus;
10393
10429
  /** Why the call went out unstrict, when a strict-capable adapter refused the schema */
10394
10430
  strictRefusalReasons?: string[];
10395
10431
  }
@@ -4461,26 +4461,24 @@ function resolveSecurityLevel(config2) {
4461
4461
 
4462
4462
  // ../core/src/execution/engine/agent/reasoning/prompt-sections/base-actions.ts
4463
4463
  function buildBaseActionsPrompt(includeMessageAction, includeNavigateKnowledge) {
4464
- let actionCount = 2;
4465
- const actions = ["1. tool-call (call a tool)"];
4466
- if (includeMessageAction) {
4467
- actionCount++;
4468
- actions.push(`${actionCount}. message (send message to user)`);
4469
- }
4464
+ const actionNames = ["tool-call (call a tool)"];
4470
4465
  if (includeNavigateKnowledge) {
4471
- actionCount++;
4472
- actions.push(`${actionCount}. navigate-knowledge (load knowledge node)`);
4466
+ actionNames.push("navigate-knowledge (load knowledge node)");
4473
4467
  }
4474
- actions.push(`${actionCount + 1}. complete (finish task)`);
4475
- actionCount++;
4476
- const actionsList = actions.join("\n");
4468
+ actionNames.push("complete (finish task)");
4469
+ const actionsList = actionNames.map((name, index2) => `${index2 + 1}. ${name}`).join("\n");
4470
+ const actionCount = actionNames.length;
4477
4471
  return `# CORE AGENT INSTRUCTIONS
4478
4472
 
4479
- You are an AI agent. Your response is captured as structured output. Two fields are required on
4473
+ You are an AI agent. Your response is captured as structured output. ${includeMessageAction ? "Three fields are required" : "Two fields are required"} on
4480
4474
  every response:
4481
4475
 
4482
4476
  - **reasoning** -- your thought process, as plain prose.
4483
- - **nextActions** -- the actions to execute.
4477
+ - **nextActions** -- the actions to execute.${includeMessageAction ? `
4478
+ - **message** -- your reply to the user, as plain prose. This is the ONLY field the user sees.
4479
+ Whatever you decide to say, it goes here. Use an empty string only when this iteration just calls
4480
+ tools and you genuinely have nothing to tell the user yet -- an empty message ends the turn in
4481
+ silence.` : ""}
4484
4482
 
4485
4483
  **reasoning is prose, not JSON.** Do not write field names, braces, or a response object inside it,
4486
4484
  and never continue the response envelope in the reasoning text -- nextActions is a separate field
@@ -4491,14 +4489,16 @@ that you fill separately. A response carrying reasoning alone is discarded and r
4491
4489
  ${actionsList}
4492
4490
 
4493
4491
  **Formats:**
4494
- - tool-call: { "type": "tool-call", "id": "unique-id", "name": "tool_name", "input": {...} }${includeMessageAction ? `
4495
- - message: { "type": "message", "text": "Your message" }` : ""}${includeNavigateKnowledge ? `
4492
+ - tool-call: { "type": "tool-call", "id": "unique-id", "name": "tool_name", "input": {...} }${includeNavigateKnowledge ? `
4496
4493
  - navigate-knowledge: { "type": "navigate-knowledge", "id": "unique-id", "nodeId": "node-name" }` : ""}
4497
4494
  - complete: { "type": "complete" }
4498
-
4495
+ ${includeMessageAction ? `
4496
+ Talking to the user is NOT an action. There is no message action -- put your reply in the
4497
+ **message** field beside nextActions.
4498
+ ` : ""}
4499
4499
  ## Execution Flow
4500
4500
 
4501
- 1. You respond with reasoning + actions
4501
+ 1. You respond with reasoning + actions${includeMessageAction ? " + your message to the user" : ""}
4502
4502
  2. System executes actions (tool calls run **in parallel**)
4503
4503
  3. Tool results automatically appear in your next iteration
4504
4504
  4. You see results and decide: more work needed? Or complete?
@@ -4510,10 +4510,10 @@ ${actionsList}
4510
4510
  - Dependent operations need separate iterations (tool B needs tool A's result)
4511
4511
  - "complete" cannot mix with navigate-knowledge${includeNavigateKnowledge ? "" : " (when available)"}
4512
4512
  - "complete" can mix with tool-call when the tool is a fire-and-forget side effect and you do not need its result before ending${includeMessageAction ? `
4513
- - Always send at least one message before completing
4514
- - Send at most one message per iteration. Multiple messages in a session turn are collapsed into one visible assistant message.
4515
- - When you have your answer, send message + complete in the SAME iteration. Never send a message alone then complete in a later iteration.
4516
- - Never repeat or rephrase the same answer across iterations. One clear answer, then complete.` : ""}
4513
+ - Always fill message before completing -- it is the only field the user sees, and completing with it empty ends the turn in silence
4514
+ - message holds one reply. Write the whole reply in it; do not split a reply across iterations
4515
+ - When you have your answer, put it in message and include complete in the SAME iteration. Never reply on one iteration then complete on a later one
4516
+ - Never repeat or rephrase the same answer across iterations. One clear answer, then complete` : ""}
4517
4517
 
4518
4518
  **Use "complete" when:**
4519
4519
  - Task finished successfully
@@ -4527,27 +4527,27 @@ ${actionsList}
4527
4527
 
4528
4528
  ## Examples
4529
4529
 
4530
- Each example shows the two field values, not a JSON document to copy.
4530
+ Each example shows the field values, not a JSON document to copy.
4531
4531
 
4532
4532
  ### Example 1: Simple Task (No Tools)
4533
- - reasoning: Simple greeting, no tools needed.
4534
- - nextActions: [${includeMessageAction ? '{ "type": "message", "text": "Hi! How can I help?" }, ' : ""}{ "type": "complete" }]
4533
+ - reasoning: Simple greeting, no tools needed.${includeMessageAction ? "\n- message: Hi! How can I help?" : ""}
4534
+ - nextActions: [{ "type": "complete" }]
4535
4535
 
4536
4536
  ### Example 2: Tool Usage (Two Iterations)
4537
4537
 
4538
4538
  **Iteration 1 - Call tool (NO complete - waiting for results):**
4539
- - reasoning: User asked for time. Calling get_time tool.
4540
- - nextActions: [${includeMessageAction ? '{ "type": "message", "text": "Checking the time..." }, ' : ""}{ "type": "tool-call", "id": "t1", "name": "get_time", "input": { "timezone": "UTC" } }]
4539
+ - reasoning: User asked for time. Calling get_time tool.${includeMessageAction ? "\n- message: Checking the time..." : ""}
4540
+ - nextActions: [{ "type": "tool-call", "id": "t1", "name": "get_time", "input": { "timezone": "UTC" } }]
4541
4541
 
4542
4542
  **Iteration 2 - Tool result received, now complete:**
4543
- - reasoning: Got time result: 12:00 PM UTC. Task done.
4544
- - nextActions: [${includeMessageAction ? '{ "type": "message", "text": "The current time is 12:00 PM UTC." }, ' : ""}{ "type": "complete" }]
4543
+ - reasoning: Got time result: 12:00 PM UTC. Task done.${includeMessageAction ? "\n- message: The current time is 12:00 PM UTC." : ""}
4544
+ - nextActions: [{ "type": "complete" }]
4545
4545
 
4546
4546
  ### Example 3: Parallel Tool Calls (Independent Operations)
4547
4547
  When tools don't depend on each other, batch them for faster execution.
4548
4548
 
4549
- - reasoning: User wants time AND weather. Independent operations - calling both in parallel.
4550
- - nextActions: [${includeMessageAction ? '{ "type": "message", "text": "Getting time and weather..." }, ' : ""}{ "type": "tool-call", "id": "t1", "name": "get_time", "input": {} }, { "type": "tool-call", "id": "w1", "name": "get_weather", "input": { "city": "NYC" } }]
4549
+ - reasoning: User wants time AND weather. Independent operations - calling both in parallel.${includeMessageAction ? "\n- message: Getting time and weather..." : ""}
4550
+ - nextActions: [{ "type": "tool-call", "id": "t1", "name": "get_time", "input": {} }, { "type": "tool-call", "id": "w1", "name": "get_weather", "input": { "city": "NYC" } }]
4551
4551
 
4552
4552
  ### Example 4: Dependent Operations (Separate Iterations Required)
4553
4553
 
@@ -4557,8 +4557,8 @@ When tools don't depend on each other, batch them for faster execution.
4557
4557
  Problem: update_user needs userId from search_user result!
4558
4558
 
4559
4559
  **\u2705 CORRECT - Iteration 1 (get the dependency):**
4560
- - reasoning: Need to find user first before updating.
4561
- - nextActions: [${includeMessageAction ? '{ "type": "message", "text": "Looking up user..." }, ' : ""}{ "type": "tool-call", "id": "1", "name": "search_user", "input": { "email": "user@example.com" } }]
4560
+ - reasoning: Need to find user first before updating.${includeMessageAction ? "\n- message: Looking up user..." : ""}
4561
+ - nextActions: [{ "type": "tool-call", "id": "1", "name": "search_user", "input": { "email": "user@example.com" } }]
4562
4562
 
4563
4563
  **\u2705 CORRECT - Iteration 2 (use the result):**
4564
4564
  - reasoning: Found userId: user_123. Now can update.
@@ -4641,7 +4641,7 @@ function buildToolsPrompt(tools) {
4641
4641
  section += '{\n "type": "tool-call",\n "id": "unique-id",\n "name": "tool-name",\n "input": { /* tool input matching schema */ }\n}\n\n';
4642
4642
  section += "**IMPORTANT RULES:**\n";
4643
4643
  section += '1. "complete" CANNOT mix with navigate-knowledge actions in the same response\n';
4644
- section += '2. "complete" CAN mix with message - always pair your final message with complete in the same iteration\n';
4644
+ section += '2. The "message" field CAN be filled on the same response that completes - always pair your final message with complete in the same iteration\n';
4645
4645
  section += '3. "complete" CAN mix with fire-and-forget tool-call actions when you do not need their results\n';
4646
4646
  section += "4. To use tools and inspect their results, return ONLY tool-call actions, then wait for results in the next iteration\n";
4647
4647
  section += "5. After receiving tool results, you can either call more tools OR complete with final answer\n";
@@ -5204,6 +5204,7 @@ var MemoryOperationsSchema = z.object({
5204
5204
  });
5205
5205
  var AgentIterationOutputSchema = z.object({
5206
5206
  reasoning: z.string(),
5207
+ message: z.string().optional(),
5207
5208
  memoryOps: MemoryOperationsSchema.optional(),
5208
5209
  nextActions: z.array(AgentActionSchema)
5209
5210
  });
@@ -5252,6 +5253,17 @@ ${memory.framing}` : memory.framing },
5252
5253
  }
5253
5254
  return messages;
5254
5255
  }
5256
+ function withSynthesizedMessage(nextActions, message) {
5257
+ const text = message?.trim();
5258
+ if (!text) {
5259
+ return nextActions;
5260
+ }
5261
+ const alreadyPresent = nextActions.some((action) => action.type === "message" && action.text.trim() === text);
5262
+ if (alreadyPresent) {
5263
+ return nextActions;
5264
+ }
5265
+ return [{ type: "message", text }, ...nextActions];
5266
+ }
5255
5267
  async function callLLMForAgentIteration(adapter, request) {
5256
5268
  validateTokenConfiguration(request.model, request.constraints.maxOutputTokens);
5257
5269
  const messages = buildAgentMessages(
@@ -5289,7 +5301,7 @@ async function callLLMForAgentIteration(adapter, request) {
5289
5301
  return {
5290
5302
  reasoning: validated.reasoning,
5291
5303
  memoryOps: validated.memoryOps,
5292
- nextActions: validated.nextActions
5304
+ nextActions: withSynthesizedMessage(validated.nextActions, validated.message)
5293
5305
  };
5294
5306
  } catch (error) {
5295
5307
  flowLog("agent.iteration.validationFailed", {
@@ -5297,6 +5309,7 @@ async function callLLMForAgentIteration(adapter, request) {
5297
5309
  missingRequired: ["reasoning", "nextActions"].filter(
5298
5310
  (k2) => !(typeof response.output === "object" && response.output !== null && k2 in response.output)
5299
5311
  ),
5312
+ messagePresent: typeof response.output === "object" && response.output !== null && typeof response.output.message === "string",
5300
5313
  zodIssues: error instanceof ZodError ? error.issues.map((i) => ({ path: i.path.join("."), code: i.code })) : void 0
5301
5314
  });
5302
5315
  throw new AgentOutputValidationError("Agent iteration output validation failed", {
@@ -5374,17 +5387,6 @@ function buildIterationResponseSchema(tools, includeMessageAction, includeNaviga
5374
5387
  required: ["type"],
5375
5388
  additionalProperties: false
5376
5389
  });
5377
- if (includeMessageAction) {
5378
- actionSchemas.push({
5379
- type: "object",
5380
- properties: {
5381
- type: { type: "string", enum: ["message"] },
5382
- text: { type: "string" }
5383
- },
5384
- required: ["type", "text"],
5385
- additionalProperties: false
5386
- });
5387
- }
5388
5390
  if (includeNavigateKnowledge) {
5389
5391
  actionSchemas.push({
5390
5392
  type: "object",
@@ -5403,9 +5405,15 @@ function buildIterationResponseSchema(tools, includeMessageAction, includeNaviga
5403
5405
  items: {
5404
5406
  anyOf: actionSchemas
5405
5407
  }
5406
- },
5407
- reasoning: { type: "string", description: "Your reasoning process" }
5408
+ }
5408
5409
  };
5410
+ if (includeMessageAction) {
5411
+ properties.message = {
5412
+ type: "string",
5413
+ description: "Your reply to the user, as plain prose. This is the ONLY field the user sees. Use an empty string only when this iteration just calls tools and you have nothing to say yet."
5414
+ };
5415
+ }
5416
+ properties.reasoning = { type: "string", description: "Your reasoning process" };
5409
5417
  if (includeMemoryOps) {
5410
5418
  properties.memoryOps = {
5411
5419
  type: "object",
@@ -5436,7 +5444,7 @@ function buildIterationResponseSchema(tools, includeMessageAction, includeNaviga
5436
5444
  return {
5437
5445
  type: "object",
5438
5446
  properties,
5439
- required: ["nextActions", "reasoning"],
5447
+ required: includeMessageAction ? ["nextActions", "message", "reasoning"] : ["nextActions", "reasoning"],
5440
5448
  additionalProperties: false
5441
5449
  };
5442
5450
  }
@@ -2583,26 +2583,24 @@ function resolveSecurityLevel(config) {
2583
2583
 
2584
2584
  // ../core/src/execution/engine/agent/reasoning/prompt-sections/base-actions.ts
2585
2585
  function buildBaseActionsPrompt(includeMessageAction, includeNavigateKnowledge) {
2586
- let actionCount = 2;
2587
- const actions = ["1. tool-call (call a tool)"];
2588
- if (includeMessageAction) {
2589
- actionCount++;
2590
- actions.push(`${actionCount}. message (send message to user)`);
2591
- }
2586
+ const actionNames = ["tool-call (call a tool)"];
2592
2587
  if (includeNavigateKnowledge) {
2593
- actionCount++;
2594
- actions.push(`${actionCount}. navigate-knowledge (load knowledge node)`);
2588
+ actionNames.push("navigate-knowledge (load knowledge node)");
2595
2589
  }
2596
- actions.push(`${actionCount + 1}. complete (finish task)`);
2597
- actionCount++;
2598
- const actionsList = actions.join("\n");
2590
+ actionNames.push("complete (finish task)");
2591
+ const actionsList = actionNames.map((name, index) => `${index + 1}. ${name}`).join("\n");
2592
+ const actionCount = actionNames.length;
2599
2593
  return `# CORE AGENT INSTRUCTIONS
2600
2594
 
2601
- You are an AI agent. Your response is captured as structured output. Two fields are required on
2595
+ You are an AI agent. Your response is captured as structured output. ${includeMessageAction ? "Three fields are required" : "Two fields are required"} on
2602
2596
  every response:
2603
2597
 
2604
2598
  - **reasoning** -- your thought process, as plain prose.
2605
- - **nextActions** -- the actions to execute.
2599
+ - **nextActions** -- the actions to execute.${includeMessageAction ? `
2600
+ - **message** -- your reply to the user, as plain prose. This is the ONLY field the user sees.
2601
+ Whatever you decide to say, it goes here. Use an empty string only when this iteration just calls
2602
+ tools and you genuinely have nothing to tell the user yet -- an empty message ends the turn in
2603
+ silence.` : ""}
2606
2604
 
2607
2605
  **reasoning is prose, not JSON.** Do not write field names, braces, or a response object inside it,
2608
2606
  and never continue the response envelope in the reasoning text -- nextActions is a separate field
@@ -2613,14 +2611,16 @@ that you fill separately. A response carrying reasoning alone is discarded and r
2613
2611
  ${actionsList}
2614
2612
 
2615
2613
  **Formats:**
2616
- - tool-call: { "type": "tool-call", "id": "unique-id", "name": "tool_name", "input": {...} }${includeMessageAction ? `
2617
- - message: { "type": "message", "text": "Your message" }` : ""}${includeNavigateKnowledge ? `
2614
+ - tool-call: { "type": "tool-call", "id": "unique-id", "name": "tool_name", "input": {...} }${includeNavigateKnowledge ? `
2618
2615
  - navigate-knowledge: { "type": "navigate-knowledge", "id": "unique-id", "nodeId": "node-name" }` : ""}
2619
2616
  - complete: { "type": "complete" }
2620
-
2617
+ ${includeMessageAction ? `
2618
+ Talking to the user is NOT an action. There is no message action -- put your reply in the
2619
+ **message** field beside nextActions.
2620
+ ` : ""}
2621
2621
  ## Execution Flow
2622
2622
 
2623
- 1. You respond with reasoning + actions
2623
+ 1. You respond with reasoning + actions${includeMessageAction ? " + your message to the user" : ""}
2624
2624
  2. System executes actions (tool calls run **in parallel**)
2625
2625
  3. Tool results automatically appear in your next iteration
2626
2626
  4. You see results and decide: more work needed? Or complete?
@@ -2632,10 +2632,10 @@ ${actionsList}
2632
2632
  - Dependent operations need separate iterations (tool B needs tool A's result)
2633
2633
  - "complete" cannot mix with navigate-knowledge${includeNavigateKnowledge ? "" : " (when available)"}
2634
2634
  - "complete" can mix with tool-call when the tool is a fire-and-forget side effect and you do not need its result before ending${includeMessageAction ? `
2635
- - Always send at least one message before completing
2636
- - Send at most one message per iteration. Multiple messages in a session turn are collapsed into one visible assistant message.
2637
- - When you have your answer, send message + complete in the SAME iteration. Never send a message alone then complete in a later iteration.
2638
- - Never repeat or rephrase the same answer across iterations. One clear answer, then complete.` : ""}
2635
+ - Always fill message before completing -- it is the only field the user sees, and completing with it empty ends the turn in silence
2636
+ - message holds one reply. Write the whole reply in it; do not split a reply across iterations
2637
+ - When you have your answer, put it in message and include complete in the SAME iteration. Never reply on one iteration then complete on a later one
2638
+ - Never repeat or rephrase the same answer across iterations. One clear answer, then complete` : ""}
2639
2639
 
2640
2640
  **Use "complete" when:**
2641
2641
  - Task finished successfully
@@ -2649,27 +2649,27 @@ ${actionsList}
2649
2649
 
2650
2650
  ## Examples
2651
2651
 
2652
- Each example shows the two field values, not a JSON document to copy.
2652
+ Each example shows the field values, not a JSON document to copy.
2653
2653
 
2654
2654
  ### Example 1: Simple Task (No Tools)
2655
- - reasoning: Simple greeting, no tools needed.
2656
- - nextActions: [${includeMessageAction ? '{ "type": "message", "text": "Hi! How can I help?" }, ' : ""}{ "type": "complete" }]
2655
+ - reasoning: Simple greeting, no tools needed.${includeMessageAction ? "\n- message: Hi! How can I help?" : ""}
2656
+ - nextActions: [{ "type": "complete" }]
2657
2657
 
2658
2658
  ### Example 2: Tool Usage (Two Iterations)
2659
2659
 
2660
2660
  **Iteration 1 - Call tool (NO complete - waiting for results):**
2661
- - reasoning: User asked for time. Calling get_time tool.
2662
- - nextActions: [${includeMessageAction ? '{ "type": "message", "text": "Checking the time..." }, ' : ""}{ "type": "tool-call", "id": "t1", "name": "get_time", "input": { "timezone": "UTC" } }]
2661
+ - reasoning: User asked for time. Calling get_time tool.${includeMessageAction ? "\n- message: Checking the time..." : ""}
2662
+ - nextActions: [{ "type": "tool-call", "id": "t1", "name": "get_time", "input": { "timezone": "UTC" } }]
2663
2663
 
2664
2664
  **Iteration 2 - Tool result received, now complete:**
2665
- - reasoning: Got time result: 12:00 PM UTC. Task done.
2666
- - nextActions: [${includeMessageAction ? '{ "type": "message", "text": "The current time is 12:00 PM UTC." }, ' : ""}{ "type": "complete" }]
2665
+ - reasoning: Got time result: 12:00 PM UTC. Task done.${includeMessageAction ? "\n- message: The current time is 12:00 PM UTC." : ""}
2666
+ - nextActions: [{ "type": "complete" }]
2667
2667
 
2668
2668
  ### Example 3: Parallel Tool Calls (Independent Operations)
2669
2669
  When tools don't depend on each other, batch them for faster execution.
2670
2670
 
2671
- - reasoning: User wants time AND weather. Independent operations - calling both in parallel.
2672
- - nextActions: [${includeMessageAction ? '{ "type": "message", "text": "Getting time and weather..." }, ' : ""}{ "type": "tool-call", "id": "t1", "name": "get_time", "input": {} }, { "type": "tool-call", "id": "w1", "name": "get_weather", "input": { "city": "NYC" } }]
2671
+ - reasoning: User wants time AND weather. Independent operations - calling both in parallel.${includeMessageAction ? "\n- message: Getting time and weather..." : ""}
2672
+ - nextActions: [{ "type": "tool-call", "id": "t1", "name": "get_time", "input": {} }, { "type": "tool-call", "id": "w1", "name": "get_weather", "input": { "city": "NYC" } }]
2673
2673
 
2674
2674
  ### Example 4: Dependent Operations (Separate Iterations Required)
2675
2675
 
@@ -2679,8 +2679,8 @@ When tools don't depend on each other, batch them for faster execution.
2679
2679
  Problem: update_user needs userId from search_user result!
2680
2680
 
2681
2681
  **\u2705 CORRECT - Iteration 1 (get the dependency):**
2682
- - reasoning: Need to find user first before updating.
2683
- - nextActions: [${includeMessageAction ? '{ "type": "message", "text": "Looking up user..." }, ' : ""}{ "type": "tool-call", "id": "1", "name": "search_user", "input": { "email": "user@example.com" } }]
2682
+ - reasoning: Need to find user first before updating.${includeMessageAction ? "\n- message: Looking up user..." : ""}
2683
+ - nextActions: [{ "type": "tool-call", "id": "1", "name": "search_user", "input": { "email": "user@example.com" } }]
2684
2684
 
2685
2685
  **\u2705 CORRECT - Iteration 2 (use the result):**
2686
2686
  - reasoning: Found userId: user_123. Now can update.
@@ -2763,7 +2763,7 @@ function buildToolsPrompt(tools) {
2763
2763
  section += '{\n "type": "tool-call",\n "id": "unique-id",\n "name": "tool-name",\n "input": { /* tool input matching schema */ }\n}\n\n';
2764
2764
  section += "**IMPORTANT RULES:**\n";
2765
2765
  section += '1. "complete" CANNOT mix with navigate-knowledge actions in the same response\n';
2766
- section += '2. "complete" CAN mix with message - always pair your final message with complete in the same iteration\n';
2766
+ section += '2. The "message" field CAN be filled on the same response that completes - always pair your final message with complete in the same iteration\n';
2767
2767
  section += '3. "complete" CAN mix with fire-and-forget tool-call actions when you do not need their results\n';
2768
2768
  section += "4. To use tools and inspect their results, return ONLY tool-call actions, then wait for results in the next iteration\n";
2769
2769
  section += "5. After receiving tool results, you can either call more tools OR complete with final answer\n";
@@ -3296,6 +3296,7 @@ var MemoryOperationsSchema = z.object({
3296
3296
  });
3297
3297
  var AgentIterationOutputSchema = z.object({
3298
3298
  reasoning: z.string(),
3299
+ message: z.string().optional(),
3299
3300
  memoryOps: MemoryOperationsSchema.optional(),
3300
3301
  nextActions: z.array(AgentActionSchema)
3301
3302
  });
@@ -3344,6 +3345,17 @@ ${memory.framing}` : memory.framing },
3344
3345
  }
3345
3346
  return messages;
3346
3347
  }
3348
+ function withSynthesizedMessage(nextActions, message) {
3349
+ const text = message?.trim();
3350
+ if (!text) {
3351
+ return nextActions;
3352
+ }
3353
+ const alreadyPresent = nextActions.some((action) => action.type === "message" && action.text.trim() === text);
3354
+ if (alreadyPresent) {
3355
+ return nextActions;
3356
+ }
3357
+ return [{ type: "message", text }, ...nextActions];
3358
+ }
3347
3359
  async function callLLMForAgentIteration(adapter, request) {
3348
3360
  validateTokenConfiguration(request.model, request.constraints.maxOutputTokens);
3349
3361
  const messages = buildAgentMessages(
@@ -3381,7 +3393,7 @@ async function callLLMForAgentIteration(adapter, request) {
3381
3393
  return {
3382
3394
  reasoning: validated.reasoning,
3383
3395
  memoryOps: validated.memoryOps,
3384
- nextActions: validated.nextActions
3396
+ nextActions: withSynthesizedMessage(validated.nextActions, validated.message)
3385
3397
  };
3386
3398
  } catch (error) {
3387
3399
  flowLog("agent.iteration.validationFailed", {
@@ -3389,6 +3401,7 @@ async function callLLMForAgentIteration(adapter, request) {
3389
3401
  missingRequired: ["reasoning", "nextActions"].filter(
3390
3402
  (k) => !(typeof response.output === "object" && response.output !== null && k in response.output)
3391
3403
  ),
3404
+ messagePresent: typeof response.output === "object" && response.output !== null && typeof response.output.message === "string",
3392
3405
  zodIssues: error instanceof ZodError ? error.issues.map((i) => ({ path: i.path.join("."), code: i.code })) : void 0
3393
3406
  });
3394
3407
  throw new AgentOutputValidationError("Agent iteration output validation failed", {
@@ -3466,17 +3479,6 @@ function buildIterationResponseSchema(tools, includeMessageAction, includeNaviga
3466
3479
  required: ["type"],
3467
3480
  additionalProperties: false
3468
3481
  });
3469
- if (includeMessageAction) {
3470
- actionSchemas.push({
3471
- type: "object",
3472
- properties: {
3473
- type: { type: "string", enum: ["message"] },
3474
- text: { type: "string" }
3475
- },
3476
- required: ["type", "text"],
3477
- additionalProperties: false
3478
- });
3479
- }
3480
3482
  if (includeNavigateKnowledge) {
3481
3483
  actionSchemas.push({
3482
3484
  type: "object",
@@ -3495,9 +3497,15 @@ function buildIterationResponseSchema(tools, includeMessageAction, includeNaviga
3495
3497
  items: {
3496
3498
  anyOf: actionSchemas
3497
3499
  }
3498
- },
3499
- reasoning: { type: "string", description: "Your reasoning process" }
3500
+ }
3500
3501
  };
3502
+ if (includeMessageAction) {
3503
+ properties.message = {
3504
+ type: "string",
3505
+ description: "Your reply to the user, as plain prose. This is the ONLY field the user sees. Use an empty string only when this iteration just calls tools and you have nothing to say yet."
3506
+ };
3507
+ }
3508
+ properties.reasoning = { type: "string", description: "Your reasoning process" };
3501
3509
  if (includeMemoryOps) {
3502
3510
  properties.memoryOps = {
3503
3511
  type: "object",
@@ -3528,7 +3536,7 @@ function buildIterationResponseSchema(tools, includeMessageAction, includeNaviga
3528
3536
  return {
3529
3537
  type: "object",
3530
3538
  properties,
3531
- required: ["nextActions", "reasoning"],
3539
+ required: includeMessageAction ? ["nextActions", "message", "reasoning"] : ["nextActions", "reasoning"],
3532
3540
  additionalProperties: false
3533
3541
  };
3534
3542
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elevasis/sdk",
3
- "version": "1.41.0",
3
+ "version": "1.42.0",
4
4
  "description": "SDK for building Elevasis organization resources",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,84 @@
1
+ # The agent's reply to the user is now its own field
2
+
3
+ ## Why this note exists
4
+
5
+ **Your agents' user-facing text was being silently corrupted, and no layer could catch it.**
6
+
7
+ Measured across three 14-turn production sessions on 2026-07-28: 42 of 42 turns passed, and **20
8
+ corruption sites landed across 12 of the 42 replies — 28.6%**. The model emits a JSON escape
9
+ sequence for an em-dash and then completes it with the next letter of the sentence, so
10
+ `Hold on — red-line for me` becomes `Hold on \red-line for me`. That parses as valid JSON, satisfies
11
+ the response schema, and passes validation. There is no error state, no retry, and no provider-side
12
+ fix to wait for. The characters are simply gone from the stored transcript.
13
+
14
+ The cause is where the text sat in the schema, not the text itself. The reply used to be one variant
15
+ of an `anyOf` union nested inside the `nextActions` array. In the very same responses, the flat
16
+ top-level `reasoning` string carried **33 em-dashes with zero corruption** while the nested reply
17
+ text carried **59 em-dashes with 20 corruption sites** — and `reasoning` is the longer field.
18
+
19
+ The reply is now a flat top-level `message` property, declared beside `reasoning` rather than nested
20
+ inside the actions array. Re-measured over a fresh multi-turn session: **42 em-dashes, zero
21
+ corruption.**
22
+
23
+ **This supersedes the two-field description in `2026-07-27-agent-strict-output-and-turn-drift.md`.**
24
+ That note described the iteration response as `nextActions` then `reasoning`. It is now three
25
+ fields, in this order: `nextActions`, `message`, `reasoning`. The ordering rationale from that note
26
+ is unchanged and still holds — `reasoning` stays last so the actions and the reply are already on the
27
+ wire before any mid-response drift can start.
28
+
29
+ **`message` is required for session-capable agents.** This is not a detail you can ignore. When the
30
+ field shipped as optional, the model filled it **zero times in three turns** and those turns produced
31
+ no user-facing message at all. Under grammar-constrained sampling the model fills keys in declaration
32
+ order, so `nextActions` is chosen before the model has reasoned about the reply, `complete` is the
33
+ cheapest legal action, and a skippable `message` then gets skipped. Making it required costs nothing:
34
+ an empty or whitespace-only value is suppressed and produces no message bubble.
35
+
36
+ ## Applies to
37
+
38
+ - **Every `sessionCapable: true` agent.** `message` is the only field the user sees, so this is the
39
+ field that was being damaged.
40
+ - **Agents on Anthropic models**, where the enforced-schema path is active. On the unenforced path
41
+ the platform still accepts a message delivered the old way, as an entry in the actions list, so
42
+ nothing breaks mid-transition.
43
+ - **Anything that reads stored assistant messages** — a transcript, an export, a summarizer, a
44
+ downstream workflow. Already-stored text is not repaired by this change; see below.
45
+ - **No agent definition changes are required.** You do not edit your agents. The shape lives in the
46
+ runtime your bundle carries.
47
+
48
+ ## Required actions
49
+
50
+ 1. **Take the `@elevasis/sdk` baseline bump** this train propagates, then reinstall in `operations/`
51
+ so the new worker bundle is present.
52
+ 2. **Redeploy your operations bundle.** This is the step that actually closes the defect. The
53
+ response schema is emitted by the runtime inlined into your deployed bundle, so an existing
54
+ deployment keeps emitting the old nested shape — and keeps corrupting replies — until it is
55
+ redeployed:
56
+
57
+ ```bash
58
+ pnpm -C operations exec elevasis-sdk deploy --prod
59
+ ```
60
+
61
+ A platform-side deploy does not fix this for you, and neither does the reinstall on its own.
62
+
63
+ 3. **If you read or display stored assistant text, do not treat old records as clean.** Nothing is
64
+ rewritten retroactively. Transcripts written before your redeploy keep whatever corruption they
65
+ already have.
66
+
67
+ ## Verification
68
+
69
+ - Run a session and prompt for a reply likely to contain an em-dash — asking the agent to summarize
70
+ something in a couple of sentences is usually enough. Confirm the reply arrives, and read it: the
71
+ signature failure is a missing letter immediately after where punctuation belonged
72
+ (`Hold on \red-line`), not a visible error.
73
+ - **Confirm every turn produced an assistant message at all.** A turn that completes with no message
74
+ is the symptom of a bundle that has the flat field but not the required flag — that combination
75
+ only exists in an unreleased build, but it is the one failure worth ruling out explicitly.
76
+ - Check the observability rows for an execution: iteration calls should still record the enforced
77
+ path with no fallback reasons. This change does not push the schema off it.
78
+
79
+ ## Not handled by /git-sync
80
+
81
+ - **The redeploy.** `/git-sync` commits and pushes the propagated dependency baseline. The corruption
82
+ continues on your deployed agents until you run action 2 above.
83
+ - **Repairing existing transcripts.** No backfill is performed, and none is possible — the dropped
84
+ characters were never received.