@exellix/ai-tasks 8.4.2 → 8.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/.docs/DOWNSTREAM_ENV.md +42 -0
  2. package/.docs/FEEDBACK_TO_CLIENT_DOWNSTREAM_FIXES.md +64 -0
  3. package/.docs/INTERMEDIATE_STEPS.md +82 -0
  4. package/.docs/activity-structure.md +31 -0
  5. package/.docs/ai-task-ai-scoping-spec.md +338 -0
  6. package/.docs/ai-tasks-model-profile-aliases-7x.md +74 -0
  7. package/.docs/blockers-and-issues.md +346 -0
  8. package/.docs/building-runTask-sdk.md +659 -0
  9. package/.docs/building-skill-execution-orchestrator.md +968 -0
  10. package/.docs/code-used-before/run-task.txt +39 -0
  11. package/.docs/code-used-before/task-executor.ts.old +57 -0
  12. package/.docs/code-used-before/test-run-task.ts.old +42 -0
  13. package/.docs/code-used-before/types.txt +23 -0
  14. package/.docs/env-ready-policy.md +40 -0
  15. package/.docs/flow-io/flow-README.md +76 -0
  16. package/.docs/flow-io/narrix.md +124 -0
  17. package/.docs/flow-io/web-scoping.md +135 -0
  18. package/.docs/flow-io/xynthesis-post.md +154 -0
  19. package/.docs/flow-io/xynthesis-pre.md +181 -0
  20. package/.docs/gap-analysis.md +201 -0
  21. package/.docs/integration-facts-ai-tasks.md +109 -0
  22. package/.docs/investigation/ai-skills.md +170 -0
  23. package/.docs/investigation/external-packages-assignments.md +66 -0
  24. package/.docs/investigation/integration-summary.md +20 -0
  25. package/.docs/investigation/narrix-catalox.md +29 -0
  26. package/.docs/investigation/workplan-close-graph-engine-gaps.md +101 -0
  27. package/.docs/logging-stack.md +30 -0
  28. package/.docs/memory-narrix-adapter-developer-guide.md +402 -0
  29. package/.docs/memory-narrix-adapter-requirements.md +112 -0
  30. package/.docs/narrix-context-consumption-gap.md +184 -0
  31. package/.docs/narrix-context-downstream-report.md +30 -0
  32. package/.docs/narrix-ingest-and-packs-library-spec.md +240 -0
  33. package/.docs/narrix-record-input-current-design.md +48 -0
  34. package/.docs/pacakge.md +48 -0
  35. package/.docs/possible-components/README.md +11 -0
  36. package/.docs/possible-components/integration/README.md +10 -0
  37. package/.docs/possible-components/integration/gaps-when-merging.md +16 -0
  38. package/.docs/possible-components/integration/platform.md +54 -0
  39. package/.docs/possible-components/integration/reintegrate-into-ai-tasks.md +26 -0
  40. package/.docs/possible-components/integration/roadmap-and-checklists.md +54 -0
  41. package/.docs/possible-components/post-component/README.md +18 -0
  42. package/.docs/possible-components/post-component/builder-guide.md +175 -0
  43. package/.docs/possible-components/post-component/gaps-and-artifacts.md +52 -0
  44. package/.docs/possible-components/post-component/handler-audit.md +47 -0
  45. package/.docs/possible-components/post-component/handler-polish.md +41 -0
  46. package/.docs/possible-components/post-component/unified-protocol.md +59 -0
  47. package/.docs/possible-components/pre-component/README.md +22 -0
  48. package/.docs/possible-components/pre-component/builder-guide.md +127 -0
  49. package/.docs/possible-components/pre-component/gaps-and-artifacts.md +35 -0
  50. package/.docs/possible-components/pre-component/handler-ai-scoping.md +45 -0
  51. package/.docs/possible-components/pre-component/handler-narrix-preprocessor.md +49 -0
  52. package/.docs/possible-components/pre-component/handler-narrix-system2.md +35 -0
  53. package/.docs/possible-components/pre-component/handler-synthesized-context.md +65 -0
  54. package/.docs/possible-components/pre-component/handler-web-scope.md +29 -0
  55. package/.docs/possible-components/pre-component/unified-protocol.md +89 -0
  56. package/.docs/prefer-openrouter-routing-policy.md +132 -0
  57. package/.docs/questions-for-ai-skills.md +123 -0
  58. package/.docs/realtime-narrixing-gap-analysis.md +40 -0
  59. package/.docs/realtime-narrixing.md +433 -0
  60. package/.docs/run-context-object.md +32 -0
  61. package/.docs/session-id-usage.md +26 -0
  62. package/.docs/skill-library-spec.md +249 -0
  63. package/.docs/synthesized-context-strategy-spec.md +906 -0
  64. package/.docs/upstream-issue/2026-03-21_woroces-ai-tasks_ISSUE-006_web-scope-question-from-cni-entity.md +46 -0
  65. package/.docs/web-scopper-embed.md +93 -0
  66. package/.docs/xynthesis-wiring-and-io.md +12 -0
  67. package/CHANGELOG.md +16 -0
  68. package/README.md +15 -13
  69. package/dist/core/task-sdk.d.ts.map +1 -1
  70. package/dist/core/task-sdk.js +40 -15
  71. package/dist/core/task-sdk.js.map +1 -1
  72. package/dist/execution-strategies/constants.d.ts +11 -4
  73. package/dist/execution-strategies/constants.d.ts.map +1 -1
  74. package/dist/execution-strategies/constants.js +11 -4
  75. package/dist/execution-strategies/constants.js.map +1 -1
  76. package/dist/execution-strategies/executionStrategyCatalogMetadata.d.ts +6 -3
  77. package/dist/execution-strategies/executionStrategyCatalogMetadata.d.ts.map +1 -1
  78. package/dist/execution-strategies/executionStrategyCatalogMetadata.js +36 -5
  79. package/dist/execution-strategies/executionStrategyCatalogMetadata.js.map +1 -1
  80. package/dist/execution-strategies/executionStrategyRequestPayload.d.ts +19 -0
  81. package/dist/execution-strategies/executionStrategyRequestPayload.d.ts.map +1 -0
  82. package/dist/execution-strategies/executionStrategyRequestPayload.js +21 -0
  83. package/dist/execution-strategies/executionStrategyRequestPayload.js.map +1 -0
  84. package/dist/execution-strategies/runExecutionStrategyViaXynthesis.d.ts +35 -0
  85. package/dist/execution-strategies/runExecutionStrategyViaXynthesis.d.ts.map +1 -0
  86. package/dist/execution-strategies/runExecutionStrategyViaXynthesis.js +129 -0
  87. package/dist/execution-strategies/runExecutionStrategyViaXynthesis.js.map +1 -0
  88. package/dist/index.d.ts +16 -5
  89. package/dist/index.d.ts.map +1 -1
  90. package/dist/index.js +15 -5
  91. package/dist/index.js.map +1 -1
  92. package/dist/internal/resolveLlmCallForXynthesis.d.ts.map +1 -1
  93. package/dist/internal/resolveLlmCallForXynthesis.js +6 -0
  94. package/dist/internal/resolveLlmCallForXynthesis.js.map +1 -1
  95. package/dist/internal/runPostStepLlmCall.d.ts.map +1 -1
  96. package/dist/internal/runPostStepLlmCall.js +8 -3
  97. package/dist/internal/runPostStepLlmCall.js.map +1 -1
  98. package/dist/invocation/resolveProfileInvocationRouting.js +2 -2
  99. package/dist/invocation/resolveProfileInvocationRouting.js.map +1 -1
  100. package/dist/planWebScopeQuestions/index.d.ts +4 -8
  101. package/dist/planWebScopeQuestions/index.d.ts.map +1 -1
  102. package/dist/planWebScopeQuestions/index.js +109 -149
  103. package/dist/planWebScopeQuestions/index.js.map +1 -1
  104. package/dist/task-strategies/canonicalInputExecutionStrategies.d.ts +21 -21
  105. package/dist/task-strategies/canonicalInputExecutionStrategies.js +12 -12
  106. package/dist/task-strategies/canonicalInputExecutionStrategies.js.map +1 -1
  107. package/dist/task-strategies/types.d.ts +4 -2
  108. package/dist/task-strategies/types.d.ts.map +1 -1
  109. package/dist/types/llmCall.d.ts +1 -1
  110. package/dist/types/llmCall.d.ts.map +1 -1
  111. package/dist/types/llmCall.js.map +1 -1
  112. package/dist/utils/aiProfileModelFormat.d.ts +2 -2
  113. package/dist/utils/aiProfileModelFormat.js +2 -2
  114. package/dist/utils/aiProfilesCatalog.d.ts +16 -0
  115. package/dist/utils/aiProfilesCatalog.d.ts.map +1 -0
  116. package/dist/utils/aiProfilesCatalog.js +23 -0
  117. package/dist/utils/aiProfilesCatalog.js.map +1 -0
  118. package/dist/utils/resolveAiProfileModel.d.ts +2 -2
  119. package/dist/utils/resolveAiProfileModel.d.ts.map +1 -1
  120. package/dist/utils/resolveAiProfileModel.js +5 -5
  121. package/dist/utils/resolveAiProfileModel.js.map +1 -1
  122. package/dist/utils/routeModelConfigSlots.d.ts +3 -1
  123. package/dist/utils/routeModelConfigSlots.d.ts.map +1 -1
  124. package/dist/utils/routeModelConfigSlots.js +2 -1
  125. package/dist/utils/routeModelConfigSlots.js.map +1 -1
  126. package/documenations/upstream-feature-requests/README.md +3 -2
  127. package/documenations/upstream-feature-requests/ai-tasks-wrap-up-after-upstream.md +5 -2
  128. package/documenations/upstream-feature-requests/xynthesis-execution-strategies-option-a.md +637 -0
  129. package/documenations/upstream-feature-requests/xynthesis-orchestrator-invoke-contract-4.2.md +2 -2
  130. package/package.json +3 -4
@@ -0,0 +1,906 @@
1
+ # Synthesized-Context Execution Strategy — Specification
2
+
3
+ ## The Problem
4
+
5
+ Today, the DIRECT strategy passes raw memory/narrix content straight to the main task model. The main model receives everything — often far more context than it needs — and must simultaneously understand the domain material *and* perform the task. This wastes tokens on an expensive model doing work that a cheaper model could handle: distilling raw context into task-relevant input.
6
+
7
+ The aiScoping feature partially addresses this by scoping individual memory paths, but it operates at a field level with separate calls per path. There is no mechanism that synthesizes the *entire available context* (narrix output or memory) into a single, task-aware input — one that is shaped by what the downstream task actually needs to do.
8
+
9
+ ## The Idea
10
+
11
+ Introduce a new execution strategy — `synthesized-context` — that inserts a **synthesis pre-pass** before the main task execution. A weak/cheap model reads the available context (narrix output when present, memory otherwise) together with the downstream task's own instructions, and produces a **synthesized input** — a compressed, task-relevant representation of everything the main model needs to know.
12
+
13
+ The main model then runs against this synthesized input instead of raw memory, getting exactly the information it needs in a form it can act on immediately.
14
+
15
+ This strategy works **with or without Narrix**. It is not a Narrix feature; it is an execution strategy that *leverages* Narrix when available but falls back to memory when not.
16
+
17
+ ---
18
+
19
+ ## Why This Works
20
+
21
+ | Concern | How this strategy addresses it |
22
+ |---|---|
23
+ | Token waste on the main model | The expensive model receives only what it needs, in pre-digested form |
24
+ | Context overload | The synthesis pass compresses and filters before the main model sees anything |
25
+ | Task alignment | The synthesis prompt includes the downstream task instructions, so the weak model knows *what matters* |
26
+ | Narrix independence | Context source is configurable: narrix-first with memory fallback, or memory-only |
27
+ | Cost control | Synthesis uses a weak/cheap model — the total cost (weak + main) is often less than sending everything to the main model |
28
+
29
+ ---
30
+
31
+ ## Execution pipeline: PRE → main → POST
32
+
33
+ Execution is a **pipeline** of steps with three phases:
34
+
35
+ 1. **PRE** — zero or more steps that run **before** the main task (e.g. synthesize context, run narrix). Each can mutate request/context for the next step.
36
+ 2. **Main** — exactly one step that runs the actual task (e.g. DIRECT: call the skill with the current input and context).
37
+ 3. **POST** — zero or more steps that run **after** the main task (e.g. future: validate, enrich response, log). Optional; none defined in this spec.
38
+
39
+ The synthesized-context feature is a **PRE step**: it runs **before** the real task. The weak model synthesizes context; then the main step runs the skill with that synthesized context as if it had been the normal context. There is no POST step in this change.
40
+
41
+ The request carries an **array** of steps (see "Execution pipeline API" below). Order of execution: all PRE steps in array order → the single MAIN step → all POST steps in array order. This allows many pre/post strategies over time; this spec only adds one PRE step type.
42
+
43
+ ---
44
+
45
+ ## Strategy identity (synthesized-context as PRE step)
46
+
47
+ | Property | Value |
48
+ |---|---|
49
+ | Phase | **PRE** (runs before the main task) |
50
+ | Step type | `"synthesized-context"` |
51
+ | Requires `includeContextInPrompt` | **Yes** — the synthesized output is delivered via the context message to the main step |
52
+
53
+ > **Critical constraint**: This step only works when `includeContextInPrompt: true`. The synthesized output replaces the context markdown that would normally be generated for the main step. If `includeContextInPrompt` is not set, the implementation must either throw a clear error or force it to `true` (configurable via `autoEnableContext`, see config below).
54
+
55
+ ---
56
+
57
+ ## Context Source Resolution
58
+
59
+ The synthesis pass needs raw material to work with. Where that material comes from depends on **`contextSourcePolicy`** and what's available at runtime.
60
+
61
+ ### Policy table (authoritative)
62
+
63
+ | `contextSourcePolicy` | Narrix markdown | Web evidence markdown (`buildWebContextEvidenceMarkdown`) | Memory JSON (`jobMemory` / `taskMemory` / `executionMemory`) |
64
+ |----------------------|-----------------|-----------------------------------------------------------|---------------------------------------------------------------|
65
+ | `narrix-web` | Yes (required) | Yes if `executionMemory.webContext` hit | No |
66
+ | `narrix-web-memory` | Yes if attachment/coercion exists | Yes if hit | Yes — **`webContext` is never serialized as raw JSON** (use markdown only when web is included) |
67
+ | `memory-web` | **No** (Narrix fields stripped from bundle before serialize) | Yes if hit | Yes |
68
+ | `memory-only` | No | No | Yes — **`webContext` never appears as raw JSON** |
69
+ | `auto` | — | — | Resolves at runtime: **`narrix-web-memory`** if Narrix output exists; else **`memory-web`** if web scoping hit; else **`memory-only`** |
70
+
71
+ **Legacy aliases (same behavior):** `narrix-only` → `narrix-web`, `narrix+memory` → `narrix-web-memory`.
72
+
73
+ **`webEvidence` (optional on `SynthesisConfig`):** Passed to web markdown builder — `preferCleanContent` (default true), `maxSources` (default 5), `dedupeByUrl` (default true), `maxTotalChars`.
74
+
75
+ **`narrixAttachToField`:** Taken from `request.narrix.attachToField` (default `_narrix`) when stripping Narrix for `memory-web`.
76
+
77
+ ### What "narrix output" means
78
+
79
+ When narrix is in play (via `request.narrix` pre-processor or `narrix-then-direct` flow), the narrix output is the `NarrixEnrichedAttachment` that would normally go into `executionMemory._narrix` or `taskMemory.narrix`. In synthesized-context mode, instead of injecting it raw, we feed it to the synthesis pass.
80
+
81
+ ### What "memory" means
82
+
83
+ The enriched memory bundle — `jobMemory`, `taskMemory`, `executionMemory` — after standard enrichment (same bundle that `_executeDirect` builds today). The `bindingDefaultsDb` and other internal fields are cleansed before synthesis, same as today.
84
+
85
+ ---
86
+
87
+ ## The Synthesis Call
88
+
89
+ ### How it runs: AIGateway
90
+
91
+ The synthesis call is an internal LLM invocation via `AIGateway` — the same mechanism used by `runScopingCall` in aiScoping. It follows the exact same pattern:
92
+
93
+ ```ts
94
+ import { AIGateway } from "@athenices/ai-gateway";
95
+
96
+ const gateway = new AIGateway();
97
+ const response = await gateway.invoke({
98
+ jobId: request.jobId ?? "synthesis",
99
+ agentId: request.agentId ?? "synthesis",
100
+ instructions: SYNTHESIS_SYSTEM_PROMPT, // the synthesis template (see below)
101
+ workingMemory: { input: synthesisUserPrompt }, // "Synthesize now."
102
+ config: { model: synthesisModel }, // the weak/cheap model
103
+ });
104
+ ```
105
+
106
+ This is a standard gateway call. No new infrastructure needed — the synthesis strategy just makes one extra `gateway.invoke()` before the main task execution.
107
+
108
+ ### Inputs to the synthesis model
109
+
110
+ The synthesis model must see what the downstream task will actually receive — not raw templates with `{{handlebars}}` placeholders, but the **fully rendered, parsed versions** with memory and variables already populated.
111
+
112
+ 1. **Rendered downstream instructions** — the `.instructions` template for `skillKey`, **parsed with the enriched memory bundle and variables** (the same rendering the gateway would do for the main task). This is the actual system prompt the main model will see.
113
+ 2. **Rendered downstream prompt** — the `.prompt` template for `skillKey`, **parsed with the enriched memory bundle, variables, and `request.input`**. This is the actual user message the main model will see.
114
+ 3. **Source material** — the resolved context source (narrix output, memory, or both) serialized to structured text
115
+ 4. **Synthesis instructions** — the synthesis prompt template (see below)
116
+
117
+ **Why rendered, not raw?** If the `.instructions` file contains `You are an analyst for {{orgName}}. Review the data in light of {{variables.complianceFramework}}...`, the weak model needs to see `You are an analyst for Acme Corp. Review the data in light of PCI-DSS...` — the actual rendered instructions. Otherwise it cannot know what the downstream task cares about. Same for the prompt: if it says `{{input}}`, the synthesis model must see the actual input JSON, not the placeholder.
118
+
119
+ In practice, this means the strategy must use the same template rendering pipeline that the gateway/executor uses — `generateContextMarkdown` and the content registry's template engine — to produce the rendered strings *before* passing them to the synthesis prompt. The enriched memory bundle (from step 3 of the execution flow) and `request.variables` and `request.input` are the rendering context.
120
+
121
+ ### The Synthesis Prompt Template
122
+
123
+ This is the system instruction for the synthesis gateway call (the weak model). It is invariant across tasks — the task-specific parts come from the injected **rendered** instructions and prompt.
124
+
125
+ **Template location (easy to see and update):** The synthesis system and user prompts are loaded from **project files** so they can be edited without changing code. Recommended layout:
126
+
127
+ ```
128
+ templates/
129
+ synthesis/
130
+ system.md ← synthesis system prompt (placeholders below)
131
+ user.txt ← synthesis user prompt (one short line)
132
+ ```
133
+
134
+ - **Resolve order:** (1) Load from `templates/synthesis/system.md` and `templates/synthesis/user.txt` (relative to project root or a configurable base path). (2) If a file is missing or unreadable, fall back to the built-in defaults in code so the feature still works.
135
+ - **Placeholders** in `system.md`: `{{rendered_downstream_instructions}}`, `{{rendered_downstream_prompt}}`, `{{source_material}}`. The implementation replaces these when building the synthesis request.
136
+ - **Override:** Request-level `synthesisConfig.synthesisPromptOverride` can still replace the entire system prompt when provided.
137
+ - **Custom synthesizing guidelines (optional):** When `synthesisConfig.customSynthesizingGuidelines` is set, the implementation **adds** it to the instructions sent to the LLM: after the main template (and after the source material section), insert a section `## Additional guidelines` followed by the guidelines text, then the existing `## Your output` section. The weak model sees the base instructions plus these extra points (e.g. domain rules, what to emphasize, or format preferences). The implementation appends this block when the option is present; no placeholder in the template file is required.
138
+
139
+ Default content (used when the template file is not present) is given below.
140
+
141
+ **Default system prompt (fallback):**
142
+
143
+ ```
144
+ You are a context synthesizer. Your job is to read raw context material and produce a focused, synthesized input for a downstream AI task.
145
+
146
+ ## Your constraints
147
+
148
+ - Use ONLY the provided source material. Do not invent, assume, or hallucinate any facts.
149
+ - Your output will be consumed by another AI model as its primary context. Make it count.
150
+ - Be concise but complete — include everything relevant to the downstream task, exclude everything irrelevant.
151
+ - Preserve factual precision: names, numbers, dates, identifiers, severity levels, statuses — keep them exact.
152
+ - Do not explain what you are doing. Do not add meta-commentary. Just produce the synthesized context.
153
+
154
+ ## What the downstream task needs to do
155
+
156
+ The downstream task has these instructions (its fully rendered system prompt, with variables and memory already populated):
157
+
158
+ <downstream_instructions>
159
+ {{rendered_downstream_instructions}}
160
+ </downstream_instructions>
161
+
162
+ The downstream task will receive this user message (the fully rendered prompt, with input and variables already populated):
163
+
164
+ <rendered_downstream_prompt>
165
+ {{rendered_downstream_prompt}}
166
+ </rendered_downstream_prompt>
167
+
168
+ ## Source material to synthesize from
169
+
170
+ <source_material>
171
+ {{source_material}}
172
+ </source_material>
173
+
174
+ ## Your output
175
+
176
+ Produce a synthesized context document that gives the downstream task exactly what it needs. Structure your output to align with what the downstream task instructions describe and what the rendered prompt contains. The instructions and prompt above are fully rendered — they show exactly what the downstream AI model will see. Use that to determine what information from the source material is relevant and how to organize it.
177
+ ```
178
+
179
+ ### The Synthesis User Prompt
180
+
181
+ **File:** `templates/synthesis/user.txt` (or fallback below).
182
+
183
+ **Default user prompt (fallback):**
184
+
185
+ ```
186
+ Synthesize the source material above for the downstream task. Output only the synthesized context — nothing else.
187
+ ```
188
+
189
+ ### Synthesis Model Configuration
190
+
191
+ The model used for the `AIGateway.invoke()` synthesis call is configured via `synthesisConfig.modelConfig` on the request. It defaults to the environment variable `SYNTHESIS_MODEL` or the implementation default `gpt-5-nano`.
192
+
193
+ ```ts
194
+ // Resolution order for the model passed to gateway.invoke({ config: { model } }):
195
+ // 1. request.synthesisConfig.modelConfig.model
196
+ // 2. process.env.SYNTHESIS_MODEL
197
+ // 3. fallback (implementation default: "gpt-5-nano")
198
+ ```
199
+
200
+ ---
201
+
202
+ ## How the Synthesized Output Reaches the Main Task
203
+
204
+ The synthesized output replaces the standard context markdown. Here's the flow comparison:
205
+
206
+ ### Today (DIRECT)
207
+
208
+ ```
209
+ enrichMemories → generateContextMarkdown → build enrichedInput.context → executor
210
+ ```
211
+
212
+ ### Synthesized-Context
213
+
214
+ ```
215
+ enrichMemories → resolve source material
216
+ → render downstream .instructions + .prompt (with memory, variables, input)
217
+ → run synthesis call via AIGateway (weak model)
218
+ → synthesized output becomes enrichedInput.context
219
+ → executor (main model)
220
+ ```
221
+
222
+ The main model receives the synthesized output in the same `context` field it would normally receive the generated context markdown. From the executor's perspective, nothing changes — it just gets better, pre-digested context.
223
+
224
+ ### Template-Awareness (the "smart" part)
225
+
226
+ The key insight: the synthesis model sees the **rendered** downstream instructions and prompt — not raw templates, but the fully parsed versions with memory, variables, and input already substituted. The weak model sees *exactly* what the main model will see as its system prompt and user message. It therefore knows precisely what the main model cares about, what tone it should use, what structure the main model expects, and what input fields are in play.
227
+
228
+ This means the synthesis output naturally aligns with the actual task execution. The downstream model gets context that *fits* its real prompt.
229
+
230
+ ---
231
+
232
+ ## API Contract
233
+
234
+ ### Execution pipeline (replaces single `executionType`)
235
+
236
+ Execution is driven by an **array** of steps. Each step has a phase and a type. This is a **breaking change** from the previous single `executionType`; see [BREAKING-CHANGES.md](BREAKING-CHANGES.md) for migration.
237
+
238
+ ```ts
239
+ type ExecutionPhase = "pre" | "main" | "post";
240
+
241
+ interface ExecutionStep {
242
+ phase: ExecutionPhase;
243
+ type: string; // e.g. "synthesized-context", "direct", "narrix-then-direct"
244
+ config?: unknown; // step-specific config (e.g. SynthesisConfig for synthesized-context)
245
+ }
246
+
247
+ interface RunTaskRequest {
248
+ // ...
249
+ /** Pipeline of execution steps. Order: all pre (in order) → single main → all post (in order). Default when omitted: [{ phase: "main", type: "direct" }]. */
250
+ executionPipeline?: ExecutionStep[];
251
+ // Deprecated / removed: executionType (use executionPipeline)
252
+ }
253
+ ```
254
+
255
+ - **Exactly one** step must have `phase: "main"`. Typically `{ phase: "main", type: "direct" }`.
256
+ - **PRE** steps run first, in array order. Example: `{ phase: "pre", type: "synthesized-context", config: synthesisConfig }`.
257
+ - **POST** steps run last, in array order. Optional; no built-in post types in this spec.
258
+ - When `executionPipeline` is omitted, behavior is equivalent to `[{ phase: "main", type: "direct" }]`.
259
+
260
+ Example — synthesized-context (PRE) then direct (main):
261
+
262
+ ```ts
263
+ executionPipeline: [
264
+ { phase: "pre", type: "synthesized-context", config: { modelConfig: { model: "gpt-5-nano" }, contextSourcePolicy: "auto" } },
265
+ { phase: "main", type: "direct" },
266
+ ],
267
+ includeContextInPrompt: true,
268
+ ```
269
+
270
+ ### New fields on `RunTaskRequest` (and step configs)
271
+
272
+ ```ts
273
+ type ContextSourcePolicy =
274
+ | "auto"
275
+ | "narrix-only"
276
+ | "narrix+memory"
277
+ | "memory-only"
278
+ | "narrix-web"
279
+ | "narrix-web-memory"
280
+ | "memory-web";
281
+
282
+ interface SynthesisConfig {
283
+ /** Model configuration for the synthesis call. If omitted, uses SYNTHESIS_MODEL env or fallback. */
284
+ modelConfig?: ModelConfig;
285
+
286
+ /**
287
+ * What feeds synthesis `source_material`. See policy table in "Context Source Resolution".
288
+ */
289
+ contextSourcePolicy?: ContextSourcePolicy;
290
+
291
+ /**
292
+ * Options for serializing web scoper results into markdown in source material.
293
+ */
294
+ webEvidence?: {
295
+ preferCleanContent?: boolean;
296
+ maxSources?: number;
297
+ dedupeByUrl?: boolean;
298
+ maxTotalChars?: number;
299
+ };
300
+
301
+ /**
302
+ * When true, automatically sets includeContextInPrompt to true if not already set.
303
+ * When false, throws if includeContextInPrompt is not true.
304
+ * Default: true
305
+ */
306
+ autoEnableContext?: boolean;
307
+
308
+ /**
309
+ * Optional override for the synthesis system prompt.
310
+ * When provided, replaces the default synthesis template entirely.
311
+ * Must include {{rendered_downstream_instructions}}, {{rendered_downstream_prompt}},
312
+ * and {{source_material}} placeholders.
313
+ */
314
+ synthesisPromptOverride?: string;
315
+
316
+ /** Timeout in ms for the synthesis call. Default: 30000. */
317
+ timeoutMs?: number;
318
+
319
+ /** Max output length for synthesis result. Default: no limit. */
320
+ maxOutputLength?: number;
321
+
322
+ /**
323
+ * Memory paths to include when policy involves memory.
324
+ * Default: all of jobMemory, taskMemory, executionMemory.
325
+ * Example: ["jobMemory.customerProfile", "jobMemory.incidents"]
326
+ */
327
+ memoryPaths?: string[];
328
+
329
+ /**
330
+ * Optional custom synthesizing guidelines. When provided, they are ADDED to the synthesis
331
+ * instructions sent to the weak model as additional points to follow (e.g. domain rules,
332
+ * formatting preferences, or what to emphasize). They are appended after the main template
333
+ * in a "## Additional guidelines" section so the synthesizer sees both the base instructions
334
+ * and these extra points.
335
+ */
336
+ customSynthesizingGuidelines?: string;
337
+
338
+ /**
339
+ * When true, if the synthesis call fails (timeout, gateway error), run the main step anyway
340
+ * without synthesized context (same as skipping the pre step). Default: false — synthesis
341
+ * failure is a failure; no fallback to DIRECT.
342
+ */
343
+ fallbackToDirect?: boolean;
344
+ }
345
+
346
+ interface RunTaskRequest {
347
+ // ... existing fields ...
348
+
349
+ /** Execution pipeline (pre → main → post). When a step has type "synthesized-context", that step's config is SynthesisConfig. */
350
+ executionPipeline?: ExecutionStep[];
351
+ }
352
+ ```
353
+
354
+ ### Usage: PRE step `synthesized-context` then main `direct`
355
+
356
+ ```ts
357
+ const result = await tasks.runTask({
358
+ skillKey: "tasks/security-risk-summary",
359
+ executionPipeline: [
360
+ { phase: "pre", type: "synthesized-context", config: { modelConfig: { model: "gpt-5-nano", temperature: 0.2 }, contextSourcePolicy: "auto" } },
361
+ { phase: "main", type: "direct" },
362
+ ],
363
+ includeContextInPrompt: true,
364
+ input: { assetId: "a-123", windowDays: 30 },
365
+
366
+ // Standard fields — memory is the source material when no narrix
367
+ jobMemory: {
368
+ incidents: [ /* ...large array... */ ],
369
+ assetProfile: { /* ...detailed profile... */ },
370
+ historicalAlerts: [ /* ...hundreds of alerts... */ ],
371
+ },
372
+ taskMemory: { previousFindings: [ /* ... */ ] },
373
+ });
374
+ ```
375
+
376
+ ### Usage with Narrix
377
+
378
+ ```ts
379
+ const result = await tasks.runTask({
380
+ skillKey: "tasks/security-risk-summary",
381
+ executionPipeline: [
382
+ { phase: "pre", type: "synthesized-context", config: { modelConfig: { model: "gpt-5-nano" }, contextSourcePolicy: "narrix-only" } },
383
+ { phase: "main", type: "direct" },
384
+ ],
385
+ includeContextInPrompt: true,
386
+ input: { assetId: "a-123" },
387
+ narrix: { datasetId: "ds-security" },
388
+ jobMemory: { record: { /* ... */ } },
389
+ });
390
+ ```
391
+
392
+ ### Usage with memory path filtering
393
+
394
+ ```ts
395
+ const result = await tasks.runTask({
396
+ skillKey: "tasks/incident-triage",
397
+ executionPipeline: [
398
+ { phase: "pre", type: "synthesized-context", config: { contextSourcePolicy: "memory-only", memoryPaths: [
399
+ "jobMemory.currentIncident",
400
+ "jobMemory.customerContext",
401
+ "taskMemory.previousAssessments",
402
+ ] } },
403
+ { phase: "main", type: "direct" },
404
+ ],
405
+ includeContextInPrompt: true,
406
+ input: { question: "What is the severity?" },
407
+ jobMemory: {
408
+ currentIncident: { /* relevant */ },
409
+ customerContext: { /* relevant */ },
410
+ hugeIrrelevantDump: { /* ignored because not in memoryPaths */ },
411
+ },
412
+ });
413
+ ```
414
+
415
+ ### Usage with custom synthesizing guidelines
416
+
417
+ ```ts
418
+ const result = await tasks.runTask({
419
+ skillKey: "tasks/security-risk-summary",
420
+ executionPipeline: [
421
+ { phase: "pre", type: "synthesized-context", config: {
422
+ modelConfig: { model: "gpt-5-nano" },
423
+ contextSourcePolicy: "auto",
424
+ customSynthesizingGuidelines:
425
+ "Emphasize any finding that affects PCI-DSS scope or requires disclosure.\n" +
426
+ "Keep executive summary to under 200 words.\n" +
427
+ "Include severity and date for each risk factor.",
428
+ } },
429
+ { phase: "main", type: "direct" },
430
+ ],
431
+ includeContextInPrompt: true,
432
+ input: { assetId: "a-123" },
433
+ jobMemory: { /* ... */ },
434
+ });
435
+ ```
436
+
437
+ The weak model receives the standard synthesis instructions **plus** an "## Additional guidelines" section containing the above text, so it can follow both the base rules and these extra points when producing the synthesized context.
438
+
439
+ ---
440
+
441
+ ## Execution Flow (Normative)
442
+
443
+ ### Full sequence when pipeline has a PRE step of type `synthesized-context`
444
+
445
+ (The pipeline runs: PRE steps in order → MAIN step → POST steps. Below is the flow for the PRE step "synthesized-context" and the following MAIN step "direct".)
446
+
447
+ ```
448
+ 1. VALIDATE
449
+ ├── executionPipeline has exactly one step with phase "main"
450
+ ├── for the pre step with type "synthesized-context": step.config (SynthesisConfig) present or use defaults
451
+ ├── includeContextInPrompt is true (or autoEnableContext forces it)
452
+ └── if contextSourcePolicy is "narrix-only", verify narrix config exists
453
+
454
+ 2. NARRIX PRE-PROCESSOR (if request.narrix is set)
455
+ ├── run narrix enrichment (same as today)
456
+ └── capture narrix attachment (do NOT inject into memory yet)
457
+
458
+ 3. ENRICH MEMORIES
459
+ └── standard enrichment path (same as _executeDirect today)
460
+
461
+ 4. RESOLVE SOURCE MATERIAL (from pre step's config)
462
+ ├── based on contextSourcePolicy:
463
+ │ ├── "auto" → narrix attachment if available, else enriched memory bundle
464
+ │ ├── "narrix-only" → narrix attachment only
465
+ │ ├── "narrix+memory" → merge narrix attachment + enriched memory bundle
466
+ │ └── "memory-only" → enriched memory bundle only
467
+ ├── if pre step's config.memoryPaths specified → filter memory to only those paths
468
+ └── serialize to structured text
469
+
470
+ 5. RENDER DOWNSTREAM TASK TEMPLATES
471
+ ├── resolve {skillKey}.instructions from content registry
472
+ ├── resolve {skillKey}.prompt from content registry
473
+ ├── render .instructions with enriched memory bundle + request.variables → rendered_instructions
474
+ └── render .prompt with enriched memory bundle + request.variables + request.input → rendered_prompt
475
+ (same template engine the gateway/executor uses — the result is what the main model WOULD see)
476
+
477
+ 6. BUILD SYNTHESIS PROMPT
478
+ ├── populate synthesis template with:
479
+ │ ├── {{rendered_downstream_instructions}} → step 5 rendered instructions
480
+ │ ├── {{rendered_downstream_prompt}} → step 5 rendered prompt
481
+ │ └── {{source_material}} → step 4 serialized source
482
+ ├── if pre step's config.customSynthesizingGuidelines is set: append "\n\n## Additional guidelines\n\n" + customSynthesizingGuidelines before the "## Your output" section (or at end of template)
483
+ └── build synthesis request for AIGateway
484
+
485
+ 7. RUN SYNTHESIS CALL (AIGateway) — model from pre step's config.modelConfig or env
486
+ ├── gateway = new AIGateway()
487
+ ├── gateway.invoke({ instructions: synthesisSystemPrompt, workingMemory: { input: synthesisUserPrompt }, config: { model: weakModel } })
488
+ ├── extract text response (same extractTextFromResponse pattern as runScopingCall)
489
+ ├── trim + enforce maxOutputLength
490
+ └── result = synthesized context string
491
+
492
+ 8. BUILD ENRICHED INPUT
493
+ ├── context = synthesized context string (replaces generateContextMarkdown output)
494
+ ├── input = request.input (unchanged — the synthesis is in context, not input)
495
+ └── everything else same as _executeDirect
496
+
497
+ 9. EXECUTE MAIN TASK
498
+ └── executor.execute(enrichedInput) — main model runs with synthesized context
499
+
500
+ 10. POST-PROCESS
501
+ ├── liftIntermediateSteps
502
+ ├── add synthesis step to intermediateSteps:
503
+ │ { step: 1, id: "synthesis", ok: true, summary: "context synthesized" }
504
+ └── return result (no POST steps in this spec; pipeline ends after main)
505
+ ```
506
+
507
+ ### Error handling
508
+
509
+ | Failure point | Behavior |
510
+ |---|---|
511
+ | Narrix fails (when narrix is configured) | Throw — same as current narrix pre-processor behavior |
512
+ | Synthesis call fails | By default throw. When `synthesisConfig.fallbackToDirect === true`, run main step without synthesized context instead (no throw). |
513
+ | Synthesis call times out | Throw timeout error |
514
+ | Template fetch fails (instructions/prompt not found) | Fall back to synthesis without downstream template context; log warning |
515
+ | `includeContextInPrompt` not set and `autoEnableContext` is false | Throw configuration error |
516
+
517
+ ---
518
+
519
+ ## Relationship to Existing Patterns
520
+
521
+ ### vs. DIRECT
522
+
523
+ DIRECT passes raw memory/context to the main model. Synthesized-context adds a pre-pass. Think of it as: `synthesized-context = synthesis(context) → DIRECT`.
524
+
525
+ ### vs. narrix-then-direct
526
+
527
+ `narrix-then-direct` runs narrix and injects results into taskMemory, then runs DIRECT. `synthesized-context` can *include* narrix in its pipeline, but goes further — it synthesizes the narrix output (and/or memory) before the main call.
528
+
529
+ ### vs. aiScoping
530
+
531
+ aiScoping scopes individual memory paths with separate LLM calls per path. Synthesized-context synthesizes the *entire* context holistically in one call. They can coexist: aiScoping can run *after* synthesis if both are configured, though in practice you'd typically use one or the other.
532
+
533
+ ### Composability
534
+
535
+ ```
536
+ narrix → synthesized-context → aiScoping → DIRECT execution
537
+ ```
538
+
539
+ All layers are optional and stack. The synthesized-context strategy handles the narrix + synthesis portion, then delegates to the DIRECT path for the final execution (including aiScoping if configured).
540
+
541
+ ---
542
+
543
+ ## Builder Extension
544
+
545
+ ```ts
546
+ class TaskRequestBuilder {
547
+ // ... existing methods ...
548
+
549
+ /** Set the execution pipeline (pre / main / post steps). */
550
+ withExecutionPipeline(steps: ExecutionStep[]): this {
551
+ this.request.executionPipeline = steps;
552
+ return this;
553
+ }
554
+
555
+ /** Add a PRE step: synthesized-context with optional config. Sets includeContextInPrompt true. */
556
+ withSynthesizedContextPreStep(modelOrConfig?: string | SynthesisConfig): this {
557
+ const config = typeof modelOrConfig === "string" ? { modelConfig: { model: modelOrConfig } } : modelOrConfig ?? {};
558
+ const steps = this.request.executionPipeline ?? [{ phase: "main", type: "direct" }];
559
+ const preSteps = steps.filter(s => s.phase === "pre");
560
+ const mainStep = steps.find(s => s.phase === "main") ?? { phase: "main" as const, type: "direct" };
561
+ const postSteps = steps.filter(s => s.phase === "post");
562
+ this.request.executionPipeline = [
563
+ ...preSteps,
564
+ { phase: "pre" as const, type: "synthesized-context", config },
565
+ mainStep,
566
+ ...postSteps,
567
+ ];
568
+ this.request.includeContextInPrompt = true;
569
+ return this;
570
+ }
571
+ }
572
+ ```
573
+
574
+ ---
575
+
576
+ ## Synthesis Template Files (project-local, editable)
577
+
578
+ So that prompts can be changed without code changes, the synthesis system and user prompts are read from the project:
579
+
580
+ | File | Purpose |
581
+ |---|---|
582
+ | `templates/synthesis/system.md` | System prompt for the synthesis (weak) model. Placeholders: `{{rendered_downstream_instructions}}`, `{{rendered_downstream_prompt}}`, `{{source_material}}`. |
583
+ | `templates/synthesis/user.txt` | User prompt for the synthesis call (e.g. one line: "Synthesize the source material above..."). |
584
+
585
+ - **Base path:** Resolved relative to process cwd or a configurable base (e.g. `synthesisConfig.templatesBasePath` or env `SYNTHESIS_TEMPLATES_PATH`). Default: project root (cwd) so `templates/synthesis/` is at repo root.
586
+ - **Fallback:** If a file is missing, the implementation uses the built-in default text (see "The Synthesis Prompt Template" and "The Synthesis User Prompt" above).
587
+ - **Override:** `synthesisConfig.synthesisPromptOverride` on the request still replaces the entire system prompt when provided, and takes precedence over the file.
588
+
589
+ ---
590
+
591
+ ## Environment Variables
592
+
593
+ | Variable | Purpose | Default |
594
+ |---|---|---|
595
+ | `SYNTHESIS_MODEL` | Default model for synthesis calls when not specified in request | `gpt-5-nano` |
596
+ | `SYNTHESIS_TIMEOUT_MS` | Default timeout for synthesis calls | `30000` |
597
+ | `SYNTHESIS_MAX_OUTPUT_LENGTH` | Default max output length | No limit |
598
+ | `SYNTHESIS_TEMPLATES_PATH` | Optional base path for `templates/synthesis/` (system.md, user.txt). When unset, use cwd. | (none — use cwd) |
599
+
600
+ ---
601
+
602
+ ## Template Resolution and Rendering — How the Strategy Gets Downstream Instructions
603
+
604
+ The strategy needs to produce the **fully rendered** downstream instructions and prompt — what the main model would actually see. This is a two-step process: resolve the raw templates from the content registry, then render them with the actual memory/variables/input.
605
+
606
+ ### Step 1: Resolve raw templates from content registry
607
+
608
+ ```ts
609
+ const skillId = stripPrefix(request.skillKey); // "tasks/foo" → "foo"
610
+
611
+ // Same resolution chain as the gateway (see SKILL-CONTENT-GUIDE.md)
612
+ let rawInstructions = await contentRegistry.resolve(`${skillId}.instructions`);
613
+ let rawPrompt = await contentRegistry.resolve(`${skillId}.prompt`);
614
+
615
+ // Fallback: if .instructions missing, try base skillId
616
+ if (!rawInstructions) {
617
+ rawInstructions = await contentRegistry.resolve(skillId);
618
+ }
619
+
620
+ // If prompt template missing, use "{{input}}" as default
621
+ if (!rawPrompt) {
622
+ rawPrompt = "{{input}}";
623
+ }
624
+ ```
625
+
626
+ ### Step 2: Render templates with actual data
627
+
628
+ ```ts
629
+ // Use the same template engine the gateway uses (handlebars-style rendering)
630
+ // The rendering context includes everything the main task execution would have:
631
+ const renderContext = {
632
+ input: request.input, // the task input object
633
+ ...request.variables, // orgName, tone, etc.
634
+ jobMemory: enrichedBundle.jobMemory, // enriched job memory
635
+ taskMemory: enrichedBundle.taskMemory, // enriched task memory
636
+ executionMemory: enrichedBundle.executionMemory,
637
+ };
638
+
639
+ const renderedInstructions = templateEngine.render(rawInstructions, renderContext);
640
+ const renderedPrompt = templateEngine.render(rawPrompt, renderContext);
641
+ ```
642
+
643
+ **The result**: `renderedInstructions` is the exact system prompt the main model will see (e.g. `"You are a security analyst for Acme Corp. Assess the risk..."`). `renderedPrompt` is the exact user message (e.g. the full JSON input or a structured question). These rendered strings go into the synthesis prompt so the weak model knows precisely what the main model expects.
644
+
645
+ This mirrors the existing resolution logic described in the Skill Content Guide — same two-file convention, same fallback chain. The strategy resolves and renders them *before* the synthesis call, rather than letting the gateway resolve them during the main execution.
646
+
647
+ ---
648
+
649
+ ## What the Main Model Sees (Before/After Comparison)
650
+
651
+ ### Before (DIRECT with raw context)
652
+
653
+ ```
654
+ [System] {instructions from .instructions file}
655
+
656
+ [Context message]
657
+ ## Scoping and discovery
658
+ ### Scoping
659
+ **Signals:**
660
+ - HIGH_RISK_VENDOR
661
+ - DATA_BREACH_INDICATOR
662
+ - COMPLIANCE_GAP_PCI
663
+ ...50 more signals...
664
+
665
+ **Stories:**
666
+ - incident-timeline
667
+ - vendor-risk-narrative
668
+ ...20 more stories...
669
+
670
+ ### Discovery
671
+ **Signals:**
672
+ ...another 30 signals...
673
+
674
+ [User] {prompt template populated with raw input}
675
+ ```
676
+
677
+ ### After (synthesized-context)
678
+
679
+ ```
680
+ [System] {instructions from .instructions file}
681
+
682
+ [Context message]
683
+ This asset (vendor-acme-prod, third-party vendor) presents elevated security risk
684
+ based on three converging factors:
685
+
686
+ 1. Active data breach indicator (HIGH severity) detected on 2025-12-15 involving
687
+ PII exposure in the vendor's staging environment.
688
+ 2. PCI compliance gap identified in Q4 audit — missing requirement 6.5.1
689
+ (injection flaws protection).
690
+ 3. Vendor risk score escalated from 62→89 after December incident.
691
+
692
+ Previous assessment (2025-11-01) rated this vendor as moderate risk. The breach
693
+ and compliance gap represent material change.
694
+
695
+ Key identifiers: assetId=a-123, vendorId=v-acme, datasetId=ds-security.
696
+ Window: last 30 days.
697
+
698
+ [User] {prompt template populated with original input}
699
+ ```
700
+
701
+ The main model can now focus on its actual job — producing the security risk summary — instead of parsing raw signals and stories.
702
+
703
+ ---
704
+
705
+ ## Full Flow Example — One Complete Run
706
+
707
+ This section walks through a single execution of `executionType: "synthesized-context"` with concrete request data, so you can see exactly what is resolved, what is sent to the weak model, and what the main model receives.
708
+
709
+ ### 1. Incoming request
710
+
711
+ ```ts
712
+ await tasks.runTask({
713
+ skillKey: "tasks/security-risk-summary",
714
+ executionPipeline: [
715
+ { phase: "pre", type: "synthesized-context", config: { modelConfig: { model: "gpt-5-nano", temperature: 0.2 }, contextSourcePolicy: "narrix-only" } },
716
+ { phase: "main", type: "direct" },
717
+ ],
718
+ includeContextInPrompt: true,
719
+ input: {
720
+ assetId: "a-123",
721
+ windowDays: 30,
722
+ format: "executive-bullet",
723
+ },
724
+ variables: {
725
+ orgName: "Acme Corp",
726
+ complianceFramework: "PCI-DSS",
727
+ },
728
+ narrix: { datasetId: "ds-security" },
729
+ jobMemory: {
730
+ record: { /* raw record that narrix will consume */ },
731
+ },
732
+ });
733
+ ```
734
+
735
+ ### 2. What happens inside the pipeline
736
+
737
+ **Step 1 — Validate**
738
+
739
+ - `synthesisConfig` present (with defaults applied: `contextSourcePolicy: "narrix-only"`, `autoEnableContext: true`).
740
+ - `includeContextInPrompt` is true.
741
+ - `contextSourcePolicy` is `"narrix-only"` and `request.narrix` is set → valid.
742
+
743
+ **Step 2 — Narrix pre-processor**
744
+
745
+ - Narrix runs on `jobMemory.record` (or adapted input) and produces signals/stories.
746
+ - Result is turned into `NarrixEnrichedAttachment` and stored on `request.executionMemory._narrix` (and `request.jobMemory._narrix`).
747
+ - No change to how this works today; the attachment is now available for the synthesis step.
748
+
749
+ **Step 3 — Enrich memories**
750
+
751
+ - Standard enrichment: `enrichMemoriesWithScoping("security-risk-summary", "task", memoryBundle)`.
752
+ - Output: `enrichedBundle` (jobMemory, taskMemory, executionMemory with scoping applied, execution cleansed).
753
+
754
+ **Step 4 — Resolve source material**
755
+
756
+ - Policy is `"narrix-only"`, so only the narrix attachment is used.
757
+ - Source material is built from `request.executionMemory._narrix` (scoping + discovery signals/stories) and serialized to text, for example:
758
+
759
+ ```
760
+ ## Narrix output (scoping)
761
+ Signals: HIGH_RISK_VENDOR, DATA_BREACH_INDICATOR, COMPLIANCE_GAP_PCI, VENDOR_RISK_ESCALATION
762
+ Stories: incident-timeline, vendor-risk-narrative, compliance-gap-audit
763
+
764
+ ## Narrix output (discovery)
765
+ Signals: PII_EXPOSURE_STAGING, ...
766
+ Stories: ...
767
+ ```
768
+
769
+ - This string becomes `source_material` in the synthesis prompt.
770
+
771
+ **Step 5 — Render downstream task templates**
772
+
773
+ - Skill id derived from `skillKey`: `"security-risk-summary"`.
774
+ - Content registry is asked for `security-risk-summary.instructions` and `security-risk-summary.prompt`.
775
+ - Raw instructions (example):
776
+
777
+ ```
778
+ You are a security analyst for {{orgName}}. Produce a concise executive risk summary.
779
+ Focus on factors relevant to {{complianceFramework}}. Be factual; do not speculate.
780
+ ```
781
+
782
+ - Raw prompt (example): `{{input}}` or `Summarize risk for asset {{input.assetId}} over the last {{input.windowDays}} days. Output format: {{input.format}}.`
783
+ - Render context: `{ input: request.input, orgName: "Acme Corp", complianceFramework: "PCI-DSS", jobMemory: enrichedBundle.jobMemory, taskMemory: ..., executionMemory: ... }`.
784
+ - **Rendered instructions** (what the main model will see as system prompt):
785
+
786
+ ```
787
+ You are a security analyst for Acme Corp. Produce a concise executive risk summary.
788
+ Focus on factors relevant to PCI-DSS. Be factual; do not speculate.
789
+ ```
790
+
791
+ - **Rendered prompt** (what the main model will see as user message):
792
+
793
+ ```
794
+ Summarize risk for asset a-123 over the last 30 days. Output format: executive-bullet.
795
+ ```
796
+
797
+ - These two strings are passed into the synthesis prompt as `rendered_downstream_instructions` and `rendered_downstream_prompt`.
798
+
799
+ **Step 6 — Build synthesis prompt**
800
+
801
+ - Default synthesis system template is filled with:
802
+ - `{{rendered_downstream_instructions}}` → the rendered instructions above.
803
+ - `{{rendered_downstream_prompt}}` → the rendered prompt above.
804
+ - `{{source_material}}` → the serialized narrix output from step 4.
805
+ - Synthesis user message: `"Synthesize the source material above for the downstream task. Output only the synthesized context — nothing else."`
806
+
807
+ **Step 7 — Run synthesis call (AIGateway)**
808
+
809
+ - Gateway is invoked with:
810
+ - `instructions`: the populated synthesis system prompt (with downstream instructions, prompt, and source material).
811
+ - `workingMemory: { input: synthesisUserPrompt }`.
812
+ - `config: { model: "gpt-5-nano" }`.
813
+ - Weak model returns plain text, e.g.:
814
+
815
+ ```
816
+ This asset (vendor-acme-prod, third-party vendor) presents elevated security risk based on three converging factors:
817
+
818
+ 1. Active data breach indicator (HIGH severity) detected on 2025-12-15 involving PII exposure in the vendor's staging environment.
819
+ 2. PCI compliance gap identified in Q4 audit — missing requirement 6.5.1 (injection flaws protection).
820
+ 3. Vendor risk score escalated from 62→89 after December incident.
821
+
822
+ Previous assessment (2025-11-01) rated this vendor as moderate risk. The breach and compliance gap represent material change.
823
+
824
+ Key identifiers: assetId=a-123, vendorId=v-acme, datasetId=ds-security. Window: last 30 days.
825
+ ```
826
+
827
+ - This string is trimmed and optionally truncated by `maxOutputLength`; it becomes `synthesizedContext`.
828
+
829
+ **Step 8 — Build enriched input for main task**
830
+
831
+ - Same shape as in DIRECT: `enrichedInput = { ...request, skillKey, jobMemory, taskMemory, executionMemory, context, input }`.
832
+ - **Only difference**: `context` is set to `synthesizedContext` (the weak model’s output) instead of `generateContextMarkdown(...)` or raw narrix markdown.
833
+ - `input` is unchanged: `{ assetId: "a-123", windowDays: 30, format: "executive-bullet" }`.
834
+
835
+ **Step 9 — Execute main task**
836
+
837
+ - `executor.execute(enrichedInput)` is called.
838
+ - The main model (e.g. GPT-4) receives:
839
+ - **System**: the same rendered instructions (“You are a security analyst for Acme Corp…”).
840
+ - **Context message**: the synthesized text above (no raw signals/stories list).
841
+ - **User**: the same rendered prompt (“Summarize risk for asset a-123…”).
842
+ - The main model produces the executive risk summary using only the synthesized context.
843
+
844
+ **Step 10 — Post-process**
845
+
846
+ - Response is returned; `intermediateSteps` is updated so the first step is `{ step: 1, id: "synthesis", ok: true, summary: "context synthesized" }`, and any steps from the main task are renumbered (2, 3, …).
847
+
848
+ ### 3. Same flow without Narrix (memory-only)
849
+
850
+ If the same request had **no** `narrix` and `contextSourcePolicy: "memory-only"` (or `"auto"` with no narrix):
851
+
852
+ - Step 2 (narrix) is skipped; there is no `_narrix` attachment.
853
+ - Step 4 uses the **enriched memory bundle** as source: e.g. `jobMemory`, `taskMemory`, `executionMemory` serialized (optionally filtered by `memoryPaths`). For example, `jobMemory` might contain `incidents`, `assetProfile`, `historicalAlerts`.
854
+ - Steps 5–10 are unchanged: the weak model still receives the **rendered** instructions and prompt, but the **source material** is now the serialized memory (e.g. JSON or a markdown summary of those keys) instead of narrix output.
855
+ - The rest of the flow (synthesis call, override context, main task, intermediate steps) is the same.
856
+
857
+ ### 4. Summary diagram
858
+
859
+ ```
860
+ Request (synthesized-context, narrix-only)
861
+
862
+ ├─► Narrix pre-processor → executionMemory._narrix
863
+
864
+ ├─► Enrich memories → enrichedBundle
865
+
866
+ ├─► Resolve source material → "Narrix output: signals/stories..."
867
+
868
+ ├─► Render templates → "You are a security analyst for Acme Corp...", "Summarize risk for asset a-123..."
869
+
870
+ ├─► Build synthesis prompt → system + user for weak model
871
+
872
+ ├─► AIGateway.invoke(weak model) → synthesizedContext string
873
+
874
+ ├─► enrichedInput.context = synthesizedContext
875
+
876
+ └─► executor.execute(enrichedInput) → main model sees only synthesized context
877
+ ```
878
+
879
+ This is the full flow as it would look like end-to-end for one run.
880
+
881
+ ---
882
+
883
+ ## Acceptance Criteria
884
+
885
+ 1. Execution uses **execution pipeline** (array of steps: pre → main → post); `executionPipeline` replaces single `executionType`.
886
+ 2. PRE step type `"synthesized-context"` is supported; when used, synthesis runs **before** the main task using a configurable weak model.
887
+ 3. Synthesis prompt includes rendered downstream task instructions and prompt.
888
+ 4. Source material comes from narrix (when available) or memory, controlled by `contextSourcePolicy`.
889
+ 5. Synthesized output is delivered to the main model via the context message (`enrichedInput.context`).
890
+ 6. Requires `includeContextInPrompt: true` (with configurable auto-enable).
891
+ 7. Works with narrix, without narrix, and with memory path filtering; optional `fallbackToDirect`, custom guidelines, template files in project.
892
+ 8. Synthesis step appears in `intermediateSteps`; synthesis is **non-streaming** (complete response only).
893
+ 9. Builder has `withExecutionPipeline` and `withSynthesizedContextPreStep`.
894
+ 10. **README is very clear** about all new abilities and changes: breaking change (link to BREAKING-CHANGES.md), pipeline model, synthesized-context PRE step, SynthesisConfig options, template files, caching (nx-cache), non-streaming, and link to this spec.
895
+
896
+ ---
897
+
898
+ ## Resolved implementation decisions
899
+
900
+ 1. **Synthesis failures and fallback to DIRECT:** **No** by default. Synthesis failure is a failure. A config option (e.g. `fallbackToDirect?: boolean`) may be provided so callers can opt in to falling back to running the main task without synthesized context; **default is false** (no fallback).
901
+
902
+ 2. **Template fallback location:** Fallback content lives **in the project**, not inside the code. The default synthesis prompts are the checked-in files under `templates/synthesis/` (system.md, user.txt). If those files are missing, the implementation may use a minimal in-code fallback or fail with a clear error pointing to the project path; the canonical “default” is the project template files, not strings in source.
903
+
904
+ 3. **Caching:** Yes. Use the **nx-cache** package (already in the project) to cache synthesis results when the same inputs (e.g. source material + task instructions / skillKey + config) are seen again, to reduce cost for batch or repeated runs. Cache key and TTL are implementation-defined.
905
+
906
+ 4. **Streaming:** No at this point. The synthesis call returns a complete response; no streaming. This should be **clearly stated in the README** so callers know synthesis is non-streaming.