@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 +1 -1
- package/dist/index.d.ts +50 -14
- package/dist/node/index.d.ts +50 -14
- package/dist/test-utils/index.d.ts +50 -14
- package/dist/test-utils/index.js +56 -48
- package/dist/worker/index.js +56 -48
- package/package.json +1 -1
- package/reference/claude-config/sync-notes/2026-07-28-agent-reply-is-its-own-field.md +84 -0
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.
|
|
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
|
-
*
|
|
1069
|
-
*
|
|
1070
|
-
*
|
|
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
|
-
*
|
|
11252
|
-
*
|
|
11253
|
-
*
|
|
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
|
-
*
|
|
11256
|
-
*
|
|
11257
|
-
*
|
|
11258
|
-
*
|
|
11259
|
-
*
|
|
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
|
}
|
package/dist/node/index.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
939
|
-
*
|
|
940
|
-
*
|
|
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
|
-
*
|
|
2753
|
-
*
|
|
2754
|
-
*
|
|
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
|
-
*
|
|
2757
|
-
*
|
|
2758
|
-
*
|
|
2759
|
-
*
|
|
2760
|
-
*
|
|
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
|
-
*
|
|
902
|
-
*
|
|
903
|
-
*
|
|
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
|
-
*
|
|
10326
|
-
*
|
|
10327
|
-
*
|
|
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
|
-
*
|
|
10330
|
-
*
|
|
10331
|
-
*
|
|
10332
|
-
*
|
|
10333
|
-
*
|
|
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
|
}
|
package/dist/test-utils/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
4472
|
-
actions.push(`${actionCount}. navigate-knowledge (load knowledge node)`);
|
|
4466
|
+
actionNames.push("navigate-knowledge (load knowledge node)");
|
|
4473
4467
|
}
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
const
|
|
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": {...} }${
|
|
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
|
|
4514
|
-
-
|
|
4515
|
-
- When you have your answer,
|
|
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
|
|
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: [
|
|
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: [
|
|
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: [
|
|
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: [
|
|
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: [
|
|
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. "
|
|
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
|
}
|
package/dist/worker/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
2594
|
-
actions.push(`${actionCount}. navigate-knowledge (load knowledge node)`);
|
|
2588
|
+
actionNames.push("navigate-knowledge (load knowledge node)");
|
|
2595
2589
|
}
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
const
|
|
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": {...} }${
|
|
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
|
|
2636
|
-
-
|
|
2637
|
-
- When you have your answer,
|
|
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
|
|
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: [
|
|
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: [
|
|
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: [
|
|
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: [
|
|
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: [
|
|
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. "
|
|
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
|
@@ -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.
|