@framers/agentos 0.1.32 → 0.1.34

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 (102) hide show
  1. package/README.md +5 -2
  2. package/dist/api/AgentOS.d.ts +62 -1
  3. package/dist/api/AgentOS.d.ts.map +1 -1
  4. package/dist/api/AgentOS.js +177 -2
  5. package/dist/api/AgentOS.js.map +1 -1
  6. package/dist/api/AgentOSOrchestrator.d.ts +187 -0
  7. package/dist/api/AgentOSOrchestrator.d.ts.map +1 -1
  8. package/dist/api/AgentOSOrchestrator.js +709 -16
  9. package/dist/api/AgentOSOrchestrator.js.map +1 -1
  10. package/dist/cognitive_substrate/GMI.d.ts.map +1 -1
  11. package/dist/cognitive_substrate/GMI.js +36 -1
  12. package/dist/cognitive_substrate/GMI.js.map +1 -1
  13. package/dist/cognitive_substrate/IGMI.d.ts +21 -0
  14. package/dist/cognitive_substrate/IGMI.d.ts.map +1 -1
  15. package/dist/cognitive_substrate/IGMI.js.map +1 -1
  16. package/dist/config/AgentOSConfig.d.ts.map +1 -1
  17. package/dist/config/AgentOSConfig.js +17 -0
  18. package/dist/config/AgentOSConfig.js.map +1 -1
  19. package/dist/config/VectorStoreConfiguration.d.ts +2 -1
  20. package/dist/config/VectorStoreConfiguration.d.ts.map +1 -1
  21. package/dist/config/VectorStoreConfiguration.js.map +1 -1
  22. package/dist/core/knowledge/Neo4jKnowledgeGraph.d.ts +89 -0
  23. package/dist/core/knowledge/Neo4jKnowledgeGraph.d.ts.map +1 -0
  24. package/dist/core/knowledge/Neo4jKnowledgeGraph.js +683 -0
  25. package/dist/core/knowledge/Neo4jKnowledgeGraph.js.map +1 -0
  26. package/dist/core/llm/providers/implementations/OllamaProvider.d.ts +14 -1
  27. package/dist/core/llm/providers/implementations/OllamaProvider.d.ts.map +1 -1
  28. package/dist/core/llm/providers/implementations/OllamaProvider.js +142 -37
  29. package/dist/core/llm/providers/implementations/OllamaProvider.js.map +1 -1
  30. package/dist/core/llm/providers/implementations/OpenAIProvider.js +3 -3
  31. package/dist/core/llm/providers/implementations/OpenAIProvider.js.map +1 -1
  32. package/dist/core/observability/otel.d.ts +2 -0
  33. package/dist/core/observability/otel.d.ts.map +1 -1
  34. package/dist/core/observability/otel.js +14 -0
  35. package/dist/core/observability/otel.js.map +1 -1
  36. package/dist/core/orchestration/SqlTaskOutcomeTelemetryStore.d.ts +30 -0
  37. package/dist/core/orchestration/SqlTaskOutcomeTelemetryStore.d.ts.map +1 -0
  38. package/dist/core/orchestration/SqlTaskOutcomeTelemetryStore.js +123 -0
  39. package/dist/core/orchestration/SqlTaskOutcomeTelemetryStore.js.map +1 -0
  40. package/dist/core/orchestration/TurnPlanner.d.ts +89 -0
  41. package/dist/core/orchestration/TurnPlanner.d.ts.map +1 -0
  42. package/dist/core/orchestration/TurnPlanner.js +242 -0
  43. package/dist/core/orchestration/TurnPlanner.js.map +1 -0
  44. package/dist/discovery/CapabilityDiscoveryEngine.js +4 -4
  45. package/dist/discovery/CapabilityDiscoveryEngine.js.map +1 -1
  46. package/dist/discovery/CapabilityGraph.d.ts +2 -2
  47. package/dist/discovery/CapabilityGraph.d.ts.map +1 -1
  48. package/dist/discovery/CapabilityGraph.js +46 -17
  49. package/dist/discovery/CapabilityGraph.js.map +1 -1
  50. package/dist/discovery/Neo4jCapabilityGraph.d.ts +58 -0
  51. package/dist/discovery/Neo4jCapabilityGraph.d.ts.map +1 -0
  52. package/dist/discovery/Neo4jCapabilityGraph.js +226 -0
  53. package/dist/discovery/Neo4jCapabilityGraph.js.map +1 -0
  54. package/dist/discovery/index.d.ts +1 -0
  55. package/dist/discovery/index.d.ts.map +1 -1
  56. package/dist/discovery/index.js +1 -0
  57. package/dist/discovery/index.js.map +1 -1
  58. package/dist/discovery/types.d.ts +1 -1
  59. package/dist/discovery/types.d.ts.map +1 -1
  60. package/dist/index.d.ts +2 -0
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +2 -0
  63. package/dist/index.js.map +1 -1
  64. package/dist/neo4j/Neo4jConnectionManager.d.ts +59 -0
  65. package/dist/neo4j/Neo4jConnectionManager.d.ts.map +1 -0
  66. package/dist/neo4j/Neo4jConnectionManager.js +115 -0
  67. package/dist/neo4j/Neo4jConnectionManager.js.map +1 -0
  68. package/dist/neo4j/Neo4jCypherRunner.d.ts +39 -0
  69. package/dist/neo4j/Neo4jCypherRunner.d.ts.map +1 -0
  70. package/dist/neo4j/Neo4jCypherRunner.js +74 -0
  71. package/dist/neo4j/Neo4jCypherRunner.js.map +1 -0
  72. package/dist/neo4j/index.d.ts +12 -0
  73. package/dist/neo4j/index.d.ts.map +1 -0
  74. package/dist/neo4j/index.js +11 -0
  75. package/dist/neo4j/index.js.map +1 -0
  76. package/dist/neo4j/types.d.ts +27 -0
  77. package/dist/neo4j/types.d.ts.map +1 -0
  78. package/dist/neo4j/types.js +6 -0
  79. package/dist/neo4j/types.js.map +1 -0
  80. package/dist/rag/VectorStoreManager.d.ts.map +1 -1
  81. package/dist/rag/VectorStoreManager.js +6 -7
  82. package/dist/rag/VectorStoreManager.js.map +1 -1
  83. package/dist/rag/graphrag/GraphRAGEngine.d.ts.map +1 -1
  84. package/dist/rag/graphrag/GraphRAGEngine.js +42 -10
  85. package/dist/rag/graphrag/GraphRAGEngine.js.map +1 -1
  86. package/dist/rag/graphrag/Neo4jGraphRAGEngine.d.ts +95 -0
  87. package/dist/rag/graphrag/Neo4jGraphRAGEngine.d.ts.map +1 -0
  88. package/dist/rag/graphrag/Neo4jGraphRAGEngine.js +748 -0
  89. package/dist/rag/graphrag/Neo4jGraphRAGEngine.js.map +1 -0
  90. package/dist/rag/graphrag/index.d.ts +1 -0
  91. package/dist/rag/graphrag/index.d.ts.map +1 -1
  92. package/dist/rag/graphrag/index.js +1 -0
  93. package/dist/rag/graphrag/index.js.map +1 -1
  94. package/dist/rag/implementations/vector_stores/Neo4jVectorStore.d.ts +55 -0
  95. package/dist/rag/implementations/vector_stores/Neo4jVectorStore.d.ts.map +1 -0
  96. package/dist/rag/implementations/vector_stores/Neo4jVectorStore.js +369 -0
  97. package/dist/rag/implementations/vector_stores/Neo4jVectorStore.js.map +1 -0
  98. package/dist/rag/implementations/vector_stores/index.d.ts +1 -0
  99. package/dist/rag/implementations/vector_stores/index.d.ts.map +1 -1
  100. package/dist/rag/implementations/vector_stores/index.js +2 -0
  101. package/dist/rag/implementations/vector_stores/index.js.map +1 -1
  102. package/package.json +5 -1
@@ -19,6 +19,26 @@ import { DEFAULT_PROMPT_PROFILE_CONFIG, selectPromptProfile, } from '../core/pro
19
19
  import { DEFAULT_ROLLING_SUMMARY_COMPACTION_CONFIG, maybeCompactConversationMessages, } from '../core/conversation/RollingSummaryCompactor.js';
20
20
  import { DEFAULT_LONG_TERM_MEMORY_POLICY, hasAnyLongTermMemoryScope, LONG_TERM_MEMORY_POLICY_METADATA_KEY, resolveLongTermMemoryPolicy, } from '../core/conversation/LongTermMemoryPolicy.js';
21
21
  import { getActiveTraceMetadata, recordAgentOSToolResultMetrics, recordAgentOSTurnMetrics, recordExceptionOnActiveSpan, runWithSpanContext, shouldIncludeTraceInAgentOSResponses, startAgentOSSpan, withAgentOSSpan, } from '../core/observability/otel.js';
22
+ const RECALL_PROFILE_DEFAULTS = {
23
+ aggressive: {
24
+ cadenceTurns: 2,
25
+ forceOnCompaction: true,
26
+ maxContextChars: 4200,
27
+ topKByScope: { user: 8, persona: 8, organization: 8 },
28
+ },
29
+ balanced: {
30
+ cadenceTurns: 4,
31
+ forceOnCompaction: true,
32
+ maxContextChars: 3200,
33
+ topKByScope: { user: 6, persona: 6, organization: 6 },
34
+ },
35
+ conservative: {
36
+ cadenceTurns: 8,
37
+ forceOnCompaction: false,
38
+ maxContextChars: 2200,
39
+ topKByScope: { user: 4, persona: 4, organization: 4 },
40
+ },
41
+ };
22
42
  function normalizeMode(value) {
23
43
  return (value || '').trim().toLowerCase();
24
44
  }
@@ -62,6 +82,208 @@ function renderPlainText(markdown) {
62
82
  text = text.replace(/\n{3,}/g, '\n\n');
63
83
  return text.trim();
64
84
  }
85
+ function normalizeTaskOutcomeOverride(customFlags) {
86
+ if (!customFlags)
87
+ return null;
88
+ const read = (keys) => {
89
+ for (const key of keys) {
90
+ if (Object.prototype.hasOwnProperty.call(customFlags, key))
91
+ return customFlags[key];
92
+ }
93
+ return undefined;
94
+ };
95
+ const raw = read(['taskOutcome', 'task_outcome', 'taskSuccess', 'task_success']);
96
+ if (typeof raw === 'boolean') {
97
+ return {
98
+ status: raw ? 'success' : 'failed',
99
+ score: raw ? 1 : 0,
100
+ reason: raw ? 'Caller marked task successful.' : 'Caller marked task failed.',
101
+ source: 'request_override',
102
+ };
103
+ }
104
+ if (typeof raw === 'number' && Number.isFinite(raw)) {
105
+ const score = Math.max(0, Math.min(1, raw));
106
+ return {
107
+ status: score >= 0.8 ? 'success' : score >= 0.4 ? 'partial' : 'failed',
108
+ score,
109
+ reason: 'Caller supplied numeric task outcome score.',
110
+ source: 'request_override',
111
+ };
112
+ }
113
+ if (typeof raw !== 'string' || !raw.trim())
114
+ return null;
115
+ const normalized = raw.trim().toLowerCase().replace(/\s+/g, '_').replace(/-/g, '_');
116
+ if (normalized === 'success' ||
117
+ normalized === 'succeeded' ||
118
+ normalized === 'done' ||
119
+ normalized === 'completed' ||
120
+ normalized === 'true') {
121
+ return {
122
+ status: 'success',
123
+ score: 1,
124
+ reason: 'Caller marked task successful.',
125
+ source: 'request_override',
126
+ };
127
+ }
128
+ if (normalized === 'partial' ||
129
+ normalized === 'incomplete' ||
130
+ normalized === 'needs_followup') {
131
+ return {
132
+ status: 'partial',
133
+ score: 0.5,
134
+ reason: 'Caller marked task partially completed.',
135
+ source: 'request_override',
136
+ };
137
+ }
138
+ if (normalized === 'failed' ||
139
+ normalized === 'failure' ||
140
+ normalized === 'error' ||
141
+ normalized === 'false') {
142
+ return {
143
+ status: 'failed',
144
+ score: 0,
145
+ reason: 'Caller marked task failed.',
146
+ source: 'request_override',
147
+ };
148
+ }
149
+ return null;
150
+ }
151
+ function normalizeRequestedToolFailureMode(customFlags) {
152
+ if (!customFlags)
153
+ return null;
154
+ const read = (keys) => {
155
+ for (const key of keys) {
156
+ if (Object.prototype.hasOwnProperty.call(customFlags, key))
157
+ return customFlags[key];
158
+ }
159
+ return undefined;
160
+ };
161
+ const raw = read(['toolFailureMode', 'tool_failure_mode', 'failureMode', 'failMode']);
162
+ if (typeof raw !== 'string')
163
+ return null;
164
+ const normalized = raw.trim().toLowerCase().replace(/\s+/g, '_').replace(/-/g, '_');
165
+ if (normalized === 'fail_open' || normalized === 'open')
166
+ return 'fail_open';
167
+ if (normalized === 'fail_closed' || normalized === 'closed')
168
+ return 'fail_closed';
169
+ return null;
170
+ }
171
+ function clampInteger(value, fallback, min, max) {
172
+ const num = Number(value);
173
+ if (!Number.isFinite(num))
174
+ return fallback;
175
+ return Math.max(min, Math.min(max, Math.trunc(num)));
176
+ }
177
+ function normalizeOrganizationId(value) {
178
+ if (typeof value !== 'string')
179
+ return undefined;
180
+ const trimmed = value.trim();
181
+ return trimmed || undefined;
182
+ }
183
+ function resolveLongTermMemoryRecallConfig(config) {
184
+ const profile = config?.profile === 'balanced' || config?.profile === 'conservative'
185
+ ? config.profile
186
+ : 'aggressive';
187
+ const defaults = RECALL_PROFILE_DEFAULTS[profile];
188
+ return {
189
+ profile,
190
+ cadenceTurns: clampInteger(config?.cadenceTurns, defaults.cadenceTurns, 1, 100),
191
+ forceOnCompaction: typeof config?.forceOnCompaction === 'boolean'
192
+ ? config.forceOnCompaction
193
+ : defaults.forceOnCompaction,
194
+ maxContextChars: clampInteger(config?.maxContextChars, defaults.maxContextChars, 300, 12000),
195
+ topKByScope: {
196
+ user: clampInteger(config?.topKByScope?.user, defaults.topKByScope.user, 1, 50),
197
+ persona: clampInteger(config?.topKByScope?.persona, defaults.topKByScope.persona, 1, 50),
198
+ organization: clampInteger(config?.topKByScope?.organization, defaults.topKByScope.organization, 1, 50),
199
+ },
200
+ };
201
+ }
202
+ function resolveTenantRoutingConfig(config) {
203
+ return {
204
+ mode: config?.mode === 'single_tenant' ? 'single_tenant' : 'multi_tenant',
205
+ defaultOrganizationId: normalizeOrganizationId(config?.defaultOrganizationId),
206
+ strictOrganizationIsolation: Boolean(config?.strictOrganizationIsolation),
207
+ };
208
+ }
209
+ function resolveTaskOutcomeTelemetryConfig(config) {
210
+ const scope = config?.scope === 'global' || config?.scope === 'organization'
211
+ ? config.scope
212
+ : 'organization_persona';
213
+ return {
214
+ enabled: config?.enabled !== false,
215
+ rollingWindowSize: clampInteger(config?.rollingWindowSize, 100, 5, 5000),
216
+ scope,
217
+ emitAlerts: config?.emitAlerts !== false,
218
+ alertBelowWeightedSuccessRate: Math.max(0, Math.min(1, Number(config?.alertBelowWeightedSuccessRate ?? 0.55))),
219
+ alertMinSamples: clampInteger(config?.alertMinSamples, 8, 1, 10000),
220
+ alertCooldownMs: clampInteger(config?.alertCooldownMs, 60000, 0, 86400000),
221
+ };
222
+ }
223
+ function resolveAdaptiveExecutionConfig(config) {
224
+ return {
225
+ enabled: config?.enabled !== false,
226
+ minSamples: clampInteger(config?.minSamples, 5, 1, 1000),
227
+ minWeightedSuccessRate: Math.max(0, Math.min(1, Number(config?.minWeightedSuccessRate ?? 0.7))),
228
+ forceAllToolsWhenDegraded: config?.forceAllToolsWhenDegraded !== false,
229
+ forceFailOpenWhenDegraded: config?.forceFailOpenWhenDegraded !== false,
230
+ };
231
+ }
232
+ function sanitizeKpiEntry(raw) {
233
+ const status = raw?.status;
234
+ const validStatus = status === 'success' || status === 'partial' || status === 'failed' ? status : null;
235
+ if (!validStatus)
236
+ return null;
237
+ const scoreNum = Number(raw?.score);
238
+ const timestampNum = Number(raw?.timestamp);
239
+ if (!Number.isFinite(scoreNum) || !Number.isFinite(timestampNum))
240
+ return null;
241
+ return {
242
+ status: validStatus,
243
+ score: Math.max(0, Math.min(1, scoreNum)),
244
+ timestamp: Math.max(0, Math.trunc(timestampNum)),
245
+ };
246
+ }
247
+ function evaluateTaskOutcome(args) {
248
+ const override = normalizeTaskOutcomeOverride(args.customFlags);
249
+ if (override)
250
+ return override;
251
+ if (args.didForceTerminate || args.finalOutput.error) {
252
+ return {
253
+ status: 'failed',
254
+ score: 0,
255
+ reason: args.didForceTerminate
256
+ ? 'Turn force-terminated due to iteration cap.'
257
+ : 'Final response contains an error payload.',
258
+ source: 'heuristic',
259
+ };
260
+ }
261
+ const text = typeof args.finalOutput.responseText === 'string'
262
+ ? args.finalOutput.responseText.trim()
263
+ : '';
264
+ if (text.length >= 48) {
265
+ return {
266
+ status: 'success',
267
+ score: args.degraded ? 0.85 : 0.95,
268
+ reason: 'Final response was produced without terminal errors.',
269
+ source: 'heuristic',
270
+ };
271
+ }
272
+ if (text.length > 0 || (args.finalOutput.toolCalls?.length ?? 0) > 0) {
273
+ return {
274
+ status: 'partial',
275
+ score: args.degraded ? 0.5 : 0.6,
276
+ reason: 'Turn completed but produced a limited final response.',
277
+ source: 'heuristic',
278
+ };
279
+ }
280
+ return {
281
+ status: 'failed',
282
+ score: 0.1,
283
+ reason: 'No usable final response was produced.',
284
+ source: 'heuristic',
285
+ };
286
+ }
65
287
  /**
66
288
  * @class AgentOSOrchestrator
67
289
  * @description
@@ -74,6 +296,8 @@ function renderPlainText(markdown) {
74
296
  export class AgentOSOrchestrator {
75
297
  constructor() {
76
298
  this.initialized = false;
299
+ this.taskOutcomeKpiWindows = new Map();
300
+ this.taskOutcomeAlertState = new Map();
77
301
  /**
78
302
  * A map to hold ongoing stream contexts.
79
303
  * Key: streamId (generated by orchestrator for this interaction flow).
@@ -114,8 +338,35 @@ export class AgentOSOrchestrator {
114
338
  rollingSummaryCompactionProfilesConfig: config.rollingSummaryCompactionProfilesConfig ?? null,
115
339
  rollingSummarySystemPrompt: config.rollingSummarySystemPrompt ?? '',
116
340
  rollingSummaryStateKey: config.rollingSummaryStateKey ?? 'rollingSummaryState',
341
+ longTermMemoryRecall: resolveLongTermMemoryRecallConfig(config.longTermMemoryRecall),
342
+ tenantRouting: resolveTenantRoutingConfig(config.tenantRouting),
343
+ taskOutcomeTelemetry: resolveTaskOutcomeTelemetryConfig(config.taskOutcomeTelemetry),
344
+ adaptiveExecution: resolveAdaptiveExecutionConfig(config.adaptiveExecution),
117
345
  };
346
+ this.taskOutcomeKpiWindows.clear();
347
+ this.taskOutcomeAlertState.clear();
118
348
  this.dependencies = dependencies;
349
+ if (dependencies.taskOutcomeTelemetryStore && this.config.taskOutcomeTelemetry.enabled) {
350
+ try {
351
+ const persisted = await dependencies.taskOutcomeTelemetryStore.loadWindows();
352
+ const cap = this.config.taskOutcomeTelemetry.rollingWindowSize;
353
+ for (const [scopeKey, rawEntries] of Object.entries(persisted ?? {})) {
354
+ if (!Array.isArray(rawEntries))
355
+ continue;
356
+ const normalized = rawEntries
357
+ .map((entry) => sanitizeKpiEntry(entry))
358
+ .filter((entry) => Boolean(entry))
359
+ .sort((a, b) => a.timestamp - b.timestamp);
360
+ if (normalized.length === 0)
361
+ continue;
362
+ const trimmed = normalized.slice(Math.max(0, normalized.length - cap));
363
+ this.taskOutcomeKpiWindows.set(scopeKey, trimmed);
364
+ }
365
+ }
366
+ catch (error) {
367
+ console.warn('AgentOSOrchestrator: Failed to load persisted task outcome telemetry windows; continuing with empty windows.', error);
368
+ }
369
+ }
119
370
  this.initialized = true;
120
371
  console.log('AgentOSOrchestrator initialized.');
121
372
  }
@@ -129,6 +380,209 @@ export class AgentOSOrchestrator {
129
380
  throw new GMIError('AgentOSOrchestrator is not initialized. Call initialize() first.', GMIErrorCode.NOT_INITIALIZED);
130
381
  }
131
382
  }
383
+ resolveOrganizationContext(inputOrganizationId) {
384
+ const inbound = normalizeOrganizationId(inputOrganizationId);
385
+ const tenantConfig = this.config.tenantRouting;
386
+ if (tenantConfig.mode === 'single_tenant') {
387
+ const fallback = tenantConfig.defaultOrganizationId;
388
+ if (tenantConfig.strictOrganizationIsolation &&
389
+ inbound &&
390
+ fallback &&
391
+ inbound !== fallback) {
392
+ throw new GMIError(`organizationId '${inbound}' does not match configured single-tenant org '${fallback}'.`, GMIErrorCode.VALIDATION_ERROR, {
393
+ mode: tenantConfig.mode,
394
+ inboundOrganizationId: inbound,
395
+ configuredOrganizationId: fallback,
396
+ });
397
+ }
398
+ const resolved = inbound ?? fallback;
399
+ if (tenantConfig.strictOrganizationIsolation && !resolved) {
400
+ throw new GMIError('Single-tenant mode requires an organizationId or tenantRouting.defaultOrganizationId.', GMIErrorCode.VALIDATION_ERROR, { mode: tenantConfig.mode });
401
+ }
402
+ return resolved;
403
+ }
404
+ return inbound;
405
+ }
406
+ resolveTaskOutcomeScopeKey(args) {
407
+ const scope = this.config.taskOutcomeTelemetry.scope;
408
+ const org = normalizeOrganizationId(args.organizationId) ?? 'none';
409
+ const persona = normalizeOrganizationId(args.personaId) ?? 'unknown';
410
+ if (scope === 'global')
411
+ return 'global';
412
+ if (scope === 'organization')
413
+ return `org:${org}`;
414
+ return `org:${org}|persona:${persona}`;
415
+ }
416
+ updateTaskOutcomeKpi(args) {
417
+ const telemetry = this.config.taskOutcomeTelemetry;
418
+ if (!telemetry.enabled)
419
+ return null;
420
+ const scopeKey = this.resolveTaskOutcomeScopeKey({
421
+ organizationId: args.organizationId,
422
+ personaId: args.personaId,
423
+ });
424
+ const now = Date.now();
425
+ const window = this.taskOutcomeKpiWindows.get(scopeKey) ?? [];
426
+ window.push({
427
+ status: args.outcome.status,
428
+ score: Math.max(0, Math.min(1, Number(args.outcome.score) || 0)),
429
+ timestamp: now,
430
+ });
431
+ const cap = telemetry.rollingWindowSize;
432
+ if (window.length > cap) {
433
+ window.splice(0, window.length - cap);
434
+ }
435
+ this.taskOutcomeKpiWindows.set(scopeKey, window);
436
+ if (this.dependencies.taskOutcomeTelemetryStore) {
437
+ const snapshot = window.map((entry) => ({ ...entry }));
438
+ void this.dependencies.taskOutcomeTelemetryStore
439
+ .saveWindow(scopeKey, snapshot)
440
+ .catch((error) => {
441
+ console.warn(`AgentOSOrchestrator: Failed to persist task outcome telemetry window for scope '${scopeKey}'.`, error);
442
+ });
443
+ }
444
+ return this.summarizeTaskOutcomeWindow(scopeKey);
445
+ }
446
+ getCurrentTaskOutcomeKpi(args) {
447
+ if (!this.config.taskOutcomeTelemetry.enabled)
448
+ return null;
449
+ const scopeKey = this.resolveTaskOutcomeScopeKey({
450
+ organizationId: args.organizationId,
451
+ personaId: args.personaId,
452
+ });
453
+ return this.summarizeTaskOutcomeWindow(scopeKey);
454
+ }
455
+ summarizeTaskOutcomeWindow(scopeKey) {
456
+ const telemetry = this.config.taskOutcomeTelemetry;
457
+ const window = this.taskOutcomeKpiWindows.get(scopeKey) ?? [];
458
+ if (window.length === 0)
459
+ return null;
460
+ const now = Date.now();
461
+ let successCount = 0;
462
+ let partialCount = 0;
463
+ let failedCount = 0;
464
+ let scoreSum = 0;
465
+ for (const entry of window) {
466
+ if (entry.status === 'success')
467
+ successCount += 1;
468
+ else if (entry.status === 'partial')
469
+ partialCount += 1;
470
+ else
471
+ failedCount += 1;
472
+ scoreSum += entry.score;
473
+ }
474
+ const sampleCount = window.length;
475
+ const successRate = sampleCount > 0 ? successCount / sampleCount : 0;
476
+ const averageScore = sampleCount > 0 ? scoreSum / sampleCount : 0;
477
+ return {
478
+ scopeKey,
479
+ scopeMode: telemetry.scope,
480
+ windowSize: telemetry.rollingWindowSize,
481
+ sampleCount,
482
+ successCount,
483
+ partialCount,
484
+ failedCount,
485
+ successRate,
486
+ averageScore,
487
+ weightedSuccessRate: averageScore,
488
+ timestamp: new Date(now).toISOString(),
489
+ };
490
+ }
491
+ maybeApplyAdaptiveExecutionPolicy(args) {
492
+ const adaptive = this.config.adaptiveExecution;
493
+ if (!adaptive.enabled || !args.turnPlan)
494
+ return { applied: false };
495
+ const kpi = this.getCurrentTaskOutcomeKpi({
496
+ organizationId: args.organizationId,
497
+ personaId: args.personaId,
498
+ });
499
+ if (!kpi)
500
+ return { applied: false, kpi };
501
+ if (kpi.sampleCount < adaptive.minSamples)
502
+ return { applied: false, kpi };
503
+ if (kpi.weightedSuccessRate >= adaptive.minWeightedSuccessRate)
504
+ return { applied: false, kpi };
505
+ const reasons = [
506
+ `weightedSuccessRate=${kpi.weightedSuccessRate.toFixed(3)} below threshold=${adaptive.minWeightedSuccessRate.toFixed(3)}`,
507
+ ];
508
+ let forcedToolSelectionMode = false;
509
+ let forcedToolFailureMode = false;
510
+ let preservedRequestedFailClosed = false;
511
+ if (adaptive.forceAllToolsWhenDegraded &&
512
+ args.turnPlan.policy.toolSelectionMode === 'discovered') {
513
+ args.turnPlan.policy.toolSelectionMode = 'all';
514
+ forcedToolSelectionMode = true;
515
+ reasons.push('toolSelectionMode switched discovered -> all');
516
+ }
517
+ if (adaptive.forceFailOpenWhenDegraded && args.turnPlan.policy.toolFailureMode !== 'fail_open') {
518
+ const requestedFailureMode = normalizeRequestedToolFailureMode(args.requestCustomFlags);
519
+ if (requestedFailureMode === 'fail_closed') {
520
+ preservedRequestedFailClosed = true;
521
+ reasons.push('preserved explicit request override toolFailureMode=fail_closed');
522
+ }
523
+ else {
524
+ const before = args.turnPlan.policy.toolFailureMode;
525
+ args.turnPlan.policy.toolFailureMode = 'fail_open';
526
+ forcedToolFailureMode = true;
527
+ reasons.push(`toolFailureMode switched ${before} -> fail_open`);
528
+ }
529
+ }
530
+ if (!forcedToolSelectionMode && !forcedToolFailureMode) {
531
+ return {
532
+ applied: false,
533
+ reason: preservedRequestedFailClosed
534
+ ? 'Adaptive execution detected degraded KPI but preserved explicit fail-closed request override.'
535
+ : undefined,
536
+ kpi,
537
+ actions: preservedRequestedFailClosed
538
+ ? {
539
+ preservedRequestedFailClosed: true,
540
+ }
541
+ : undefined,
542
+ };
543
+ }
544
+ args.turnPlan.capability.fallbackApplied = true;
545
+ args.turnPlan.capability.fallbackReason = `Adaptive fallback applied: ${reasons.join('; ')}.`;
546
+ args.turnPlan.diagnostics.usedFallback = true;
547
+ return {
548
+ applied: true,
549
+ reason: args.turnPlan.capability.fallbackReason,
550
+ kpi,
551
+ actions: {
552
+ forcedToolSelectionMode,
553
+ forcedToolFailureMode,
554
+ preservedRequestedFailClosed: preservedRequestedFailClosed || undefined,
555
+ },
556
+ };
557
+ }
558
+ maybeBuildTaskOutcomeAlert(kpi) {
559
+ const telemetry = this.config.taskOutcomeTelemetry;
560
+ if (!telemetry.enabled || !telemetry.emitAlerts || !kpi)
561
+ return null;
562
+ if (kpi.sampleCount < telemetry.alertMinSamples)
563
+ return null;
564
+ if (kpi.weightedSuccessRate >= telemetry.alertBelowWeightedSuccessRate)
565
+ return null;
566
+ const now = Date.now();
567
+ const lastAlertAt = this.taskOutcomeAlertState.get(kpi.scopeKey) ?? 0;
568
+ if (telemetry.alertCooldownMs > 0 && now - lastAlertAt < telemetry.alertCooldownMs) {
569
+ return null;
570
+ }
571
+ this.taskOutcomeAlertState.set(kpi.scopeKey, now);
572
+ const severity = kpi.weightedSuccessRate < telemetry.alertBelowWeightedSuccessRate * 0.6
573
+ ? 'critical'
574
+ : 'warning';
575
+ return {
576
+ scopeKey: kpi.scopeKey,
577
+ severity,
578
+ reason: `Weighted success rate ${kpi.weightedSuccessRate.toFixed(3)} below alert threshold ` +
579
+ `${telemetry.alertBelowWeightedSuccessRate.toFixed(3)}.`,
580
+ threshold: telemetry.alertBelowWeightedSuccessRate,
581
+ value: kpi.weightedSuccessRate,
582
+ sampleCount: kpi.sampleCount,
583
+ timestamp: new Date(now).toISOString(),
584
+ };
585
+ }
132
586
  /**
133
587
  * Helper method to create and push response chunks via StreamingManager.
134
588
  * @private
@@ -286,6 +740,18 @@ export class AgentOSOrchestrator {
286
740
  await this.pushChunkToStream(streamId, AgentOSResponseChunkType.ERROR, gmiInstanceId, personaId, true, // Errors are usually final for the current operation
287
741
  { code: code.toString(), message, details });
288
742
  }
743
+ async emitExecutionLifecycleUpdate(args) {
744
+ await this.pushChunkToStream(args.streamId, AgentOSResponseChunkType.METADATA_UPDATE, args.gmiInstanceId, args.personaId, false, {
745
+ updates: {
746
+ executionLifecycle: {
747
+ phase: args.phase,
748
+ status: args.status,
749
+ timestamp: new Date().toISOString(),
750
+ ...(args.details ? { details: args.details } : null),
751
+ },
752
+ },
753
+ });
754
+ }
289
755
  /**
290
756
  * Orchestrates a full logical turn for a user request.
291
757
  * This involves managing GMI interaction, tool calls, and streaming responses.
@@ -352,6 +818,7 @@ export class AgentOSOrchestrator {
352
818
  const turnStartedAt = Date.now();
353
819
  let turnMetricsStatus = 'ok';
354
820
  let turnMetricsPersonaId = input.selectedPersonaId;
821
+ let turnMetricsTaskOutcome;
355
822
  let turnMetricsUsage;
356
823
  const selectedPersonaId = input.selectedPersonaId;
357
824
  let gmi;
@@ -361,6 +828,7 @@ export class AgentOSOrchestrator {
361
828
  let organizationIdForMemory;
362
829
  let longTermMemoryPolicy = null;
363
830
  let didForceTerminate = false;
831
+ let lifecycleDegraded = false;
364
832
  try {
365
833
  if (!selectedPersonaId) {
366
834
  throw new GMIError('AgentOSOrchestrator requires a selectedPersonaId on AgentOSInput.', GMIErrorCode.VALIDATION_ERROR);
@@ -389,12 +857,78 @@ export class AgentOSOrchestrator {
389
857
  this.activeStreamContexts.set(agentOSStreamId, streamContext);
390
858
  await this.pushChunkToStream(agentOSStreamId, AgentOSResponseChunkType.SYSTEM_PROGRESS, gmiInstanceIdForChunks, currentPersonaId, false, { message: `Initializing persona ${currentPersonaId}... GMI: ${gmiInstanceIdForChunks}`, progressPercentage: 10 });
391
859
  const gmiInput = this.constructGMITurnInput(agentOSStreamId, input, streamContext);
860
+ let turnPlan = null;
861
+ const resolvedOrganizationId = this.resolveOrganizationContext(input.organizationId);
862
+ if (this.dependencies.turnPlanner) {
863
+ const planningMessage = gmiInput.type === GMIInteractionType.TEXT && typeof gmiInput.content === 'string'
864
+ ? gmiInput.content
865
+ : gmiInput.type === GMIInteractionType.MULTIMODAL_CONTENT
866
+ ? JSON.stringify(gmiInput.content)
867
+ : '';
868
+ try {
869
+ turnPlan = await this.dependencies.turnPlanner.planTurn({
870
+ userId: input.userId,
871
+ organizationId: resolvedOrganizationId,
872
+ sessionId: input.sessionId,
873
+ conversationId: input.conversationId,
874
+ persona: gmi.getPersona(),
875
+ userMessage: planningMessage,
876
+ options: input.options,
877
+ });
878
+ }
879
+ catch (planningError) {
880
+ throw new GMIError(`Turn planning failed before execution: ${planningError?.message || String(planningError)}`, GMIErrorCode.PROCESSING_ERROR, { streamId: agentOSStreamId, planningError });
881
+ }
882
+ }
883
+ const adaptiveExecution = this.maybeApplyAdaptiveExecutionPolicy({
884
+ turnPlan,
885
+ organizationId: resolvedOrganizationId,
886
+ personaId: currentPersonaId,
887
+ requestCustomFlags: input.options?.customFlags,
888
+ });
889
+ const adaptiveExecutionPayload = adaptiveExecution.applied || adaptiveExecution.kpi || adaptiveExecution.actions
890
+ ? {
891
+ applied: adaptiveExecution.applied,
892
+ reason: adaptiveExecution.reason,
893
+ kpi: adaptiveExecution.kpi,
894
+ actions: adaptiveExecution.actions,
895
+ }
896
+ : undefined;
897
+ await this.emitExecutionLifecycleUpdate({
898
+ streamId: agentOSStreamId,
899
+ gmiInstanceId: gmiInstanceIdForChunks,
900
+ personaId: currentPersonaId,
901
+ phase: 'planned',
902
+ status: 'ok',
903
+ details: turnPlan
904
+ ? {
905
+ plannerVersion: turnPlan.policy.plannerVersion,
906
+ toolFailureMode: turnPlan.policy.toolFailureMode,
907
+ toolSelectionMode: turnPlan.policy.toolSelectionMode,
908
+ adaptiveExecution: adaptiveExecutionPayload,
909
+ }
910
+ : { plannerVersion: 'none' },
911
+ });
912
+ if (turnPlan?.capability.fallbackApplied || adaptiveExecution.applied) {
913
+ lifecycleDegraded = true;
914
+ await this.emitExecutionLifecycleUpdate({
915
+ streamId: agentOSStreamId,
916
+ gmiInstanceId: gmiInstanceIdForChunks,
917
+ personaId: currentPersonaId,
918
+ phase: 'degraded',
919
+ status: 'degraded',
920
+ details: {
921
+ reason: turnPlan?.capability.fallbackReason || adaptiveExecution.reason || 'fallback applied',
922
+ discoveryAttempts: turnPlan?.diagnostics.discoveryAttempts,
923
+ adaptiveExecution: adaptiveExecutionPayload,
924
+ },
925
+ });
926
+ }
392
927
  // --- Org context + long-term memory policy (persisted per conversation) ---
928
+ organizationIdForMemory = resolvedOrganizationId;
393
929
  if (conversationContext) {
394
- const inboundOrg = typeof input.organizationId === 'string' ? input.organizationId.trim() : '';
395
930
  // SECURITY NOTE: do not persist organizationId in conversation metadata. The org context
396
931
  // should be asserted by the trusted caller each request (after membership checks).
397
- organizationIdForMemory = inboundOrg || undefined;
398
932
  const rawPrevPolicy = conversationContext.getMetadata(LONG_TERM_MEMORY_POLICY_METADATA_KEY);
399
933
  const prevPolicy = rawPrevPolicy && typeof rawPrevPolicy === 'object'
400
934
  ? rawPrevPolicy
@@ -411,12 +945,14 @@ export class AgentOSOrchestrator {
411
945
  }
412
946
  }
413
947
  else {
414
- organizationIdForMemory =
415
- typeof input.organizationId === 'string' ? input.organizationId.trim() : undefined;
416
948
  longTermMemoryPolicy = resolveLongTermMemoryPolicy({
417
949
  defaults: DEFAULT_LONG_TERM_MEMORY_POLICY,
418
950
  });
419
951
  }
952
+ if (turnPlan) {
953
+ (gmiInput.metadata ?? (gmiInput.metadata = {})).executionPolicy = turnPlan.policy;
954
+ gmiInput.metadata.capabilityDiscovery = turnPlan.capability;
955
+ }
420
956
  (gmiInput.metadata ?? (gmiInput.metadata = {})).organizationId = organizationIdForMemory ?? null;
421
957
  gmiInput.metadata.longTermMemoryPolicy = longTermMemoryPolicy;
422
958
  // Persist inbound user/system message to ConversationContext BEFORE any LLM call so persona switches
@@ -571,6 +1107,8 @@ export class AgentOSOrchestrator {
571
1107
  // --- Long-term memory retrieval (user/persona/org) ---
572
1108
  let longTermMemoryContextText = null;
573
1109
  let longTermMemoryRetrievalDiagnostics;
1110
+ let longTermMemoryShouldReview = false;
1111
+ let longTermMemoryReviewReason = null;
574
1112
  if (conversationContext &&
575
1113
  this.dependencies.longTermMemoryRetriever &&
576
1114
  Boolean(longTermMemoryPolicy?.enabled) &&
@@ -584,21 +1122,32 @@ export class AgentOSOrchestrator {
584
1122
  ? JSON.stringify(gmiInput.content).trim()
585
1123
  : '';
586
1124
  const userTurnCount = conversationContext.getAllMessages().filter((m) => m?.role === MessageRole.USER).length;
587
- const cadenceTurns = typeof this.config.promptProfileConfig?.routing?.reviewEveryNTurns === 'number'
588
- ? Number(this.config.promptProfileConfig.routing.reviewEveryNTurns)
589
- : 6;
590
- const forceOnCompaction = typeof this.config.promptProfileConfig?.routing?.forceReviewOnCompaction === 'boolean'
591
- ? Boolean(this.config.promptProfileConfig.routing.forceReviewOnCompaction)
592
- : true;
1125
+ const recallConfig = this.config.longTermMemoryRecall;
1126
+ const cadenceTurns = recallConfig.cadenceTurns;
1127
+ const forceOnCompaction = recallConfig.forceOnCompaction;
593
1128
  const rawState = conversationContext.getMetadata('longTermMemoryRetrievalState');
594
1129
  const prevState = rawState &&
595
1130
  typeof rawState === 'object' &&
596
1131
  typeof rawState.lastReviewedUserTurn === 'number'
597
1132
  ? rawState
598
1133
  : null;
599
- const shouldReview = !prevState ||
600
- (cadenceTurns > 0 && userTurnCount - prevState.lastReviewedUserTurn >= cadenceTurns) ||
601
- (forceOnCompaction && Boolean(rollingSummaryResult?.didCompact));
1134
+ const turnsSinceReview = prevState
1135
+ ? Math.max(0, userTurnCount - prevState.lastReviewedUserTurn)
1136
+ : Number.POSITIVE_INFINITY;
1137
+ const dueToCadence = !prevState || turnsSinceReview >= cadenceTurns;
1138
+ const dueToCompaction = forceOnCompaction && Boolean(rollingSummaryResult?.didCompact);
1139
+ const shouldReview = dueToCadence || dueToCompaction;
1140
+ longTermMemoryShouldReview = shouldReview;
1141
+ if (shouldReview) {
1142
+ longTermMemoryReviewReason = !prevState
1143
+ ? 'initial_review'
1144
+ : dueToCompaction
1145
+ ? 'forced_on_compaction'
1146
+ : 'cadence_due';
1147
+ }
1148
+ else {
1149
+ longTermMemoryReviewReason = 'cadence_not_due';
1150
+ }
602
1151
  if (shouldReview && queryText.length > 0) {
603
1152
  const retrievalResult = await this.dependencies.longTermMemoryRetriever.retrieveLongTermMemory({
604
1153
  userId: streamContext.userId,
@@ -608,8 +1157,8 @@ export class AgentOSOrchestrator {
608
1157
  mode: modeForRouting,
609
1158
  queryText,
610
1159
  memoryPolicy: longTermMemoryPolicy ?? DEFAULT_LONG_TERM_MEMORY_POLICY,
611
- maxContextChars: 2800,
612
- topKByScope: { user: 6, persona: 6, organization: 6 },
1160
+ maxContextChars: recallConfig.maxContextChars,
1161
+ topKByScope: recallConfig.topKByScope,
613
1162
  });
614
1163
  if (retrievalResult?.contextText && retrievalResult.contextText.trim()) {
615
1164
  longTermMemoryContextText = retrievalResult.contextText.trim();
@@ -620,11 +1169,18 @@ export class AgentOSOrchestrator {
620
1169
  lastReviewedAt: Date.now(),
621
1170
  });
622
1171
  }
1172
+ else if (shouldReview && queryText.length === 0) {
1173
+ longTermMemoryReviewReason = 'empty_query';
1174
+ }
623
1175
  }
624
1176
  catch (retrievalError) {
625
1177
  console.warn(`AgentOSOrchestrator: Long-term memory retrieval failed for stream ${agentOSStreamId} (continuing without it).`, retrievalError);
1178
+ longTermMemoryReviewReason = 'retrieval_error';
626
1179
  }
627
1180
  }
1181
+ else {
1182
+ longTermMemoryReviewReason = 'retriever_not_applicable';
1183
+ }
628
1184
  gmiInput.metadata.longTermMemoryContext =
629
1185
  typeof longTermMemoryContextText === 'string' && longTermMemoryContextText.length > 0
630
1186
  ? longTermMemoryContextText
@@ -717,14 +1273,44 @@ export class AgentOSOrchestrator {
717
1273
  updates: {
718
1274
  promptProfile: promptProfileSelection,
719
1275
  organizationId: organizationIdForMemory ?? null,
1276
+ tenantRouting: {
1277
+ mode: this.config.tenantRouting.mode,
1278
+ strictOrganizationIsolation: this.config.tenantRouting.strictOrganizationIsolation,
1279
+ defaultOrganizationId: this.config.tenantRouting.defaultOrganizationId ?? null,
1280
+ },
720
1281
  longTermMemoryPolicy,
1282
+ longTermMemoryRecall: this.config.longTermMemoryRecall,
1283
+ taskOutcomeTelemetry: this.config.taskOutcomeTelemetry,
1284
+ adaptiveExecution: this.config.adaptiveExecution,
1285
+ turnPlanning: turnPlan
1286
+ ? {
1287
+ policy: turnPlan.policy,
1288
+ diagnostics: turnPlan.diagnostics,
1289
+ adaptiveExecution: adaptiveExecutionPayload ?? null,
1290
+ discovery: {
1291
+ enabled: turnPlan.capability.enabled,
1292
+ kind: turnPlan.capability.kind,
1293
+ selectedToolNames: turnPlan.capability.selectedToolNames,
1294
+ fallbackApplied: turnPlan.capability.fallbackApplied,
1295
+ fallbackReason: turnPlan.capability.fallbackReason,
1296
+ tokenEstimate: turnPlan.capability.result?.tokenEstimate,
1297
+ diagnostics: turnPlan.capability.result?.diagnostics,
1298
+ },
1299
+ }
1300
+ : null,
721
1301
  longTermMemoryRetrieval: longTermMemoryContextText
722
1302
  ? {
1303
+ shouldReview: longTermMemoryShouldReview,
1304
+ reviewReason: longTermMemoryReviewReason,
723
1305
  didRetrieve: true,
724
1306
  contextChars: longTermMemoryContextText.length,
725
1307
  diagnostics: longTermMemoryRetrievalDiagnostics,
726
1308
  }
727
- : { didRetrieve: false },
1309
+ : {
1310
+ shouldReview: longTermMemoryShouldReview,
1311
+ reviewReason: longTermMemoryReviewReason,
1312
+ didRetrieve: false,
1313
+ },
728
1314
  rollingSummary: rollingSummaryResult
729
1315
  ? {
730
1316
  profileId: rollingSummaryProfileId,
@@ -742,6 +1328,16 @@ export class AgentOSOrchestrator {
742
1328
  let currentToolCallIteration = 0;
743
1329
  let continueProcessing = true;
744
1330
  let lastGMIOutput; // To store the result from handleToolResult or final processTurnStream result
1331
+ await this.emitExecutionLifecycleUpdate({
1332
+ streamId: agentOSStreamId,
1333
+ gmiInstanceId: gmiInstanceIdForChunks,
1334
+ personaId: currentPersonaId,
1335
+ phase: 'executing',
1336
+ status: lifecycleDegraded ? 'degraded' : 'ok',
1337
+ details: {
1338
+ maxToolCallIterations: this.config.maxToolCallIterations,
1339
+ },
1340
+ });
745
1341
  while (continueProcessing && currentToolCallIteration < this.config.maxToolCallIterations) {
746
1342
  currentToolCallIteration++;
747
1343
  if (lastGMIOutput?.toolCalls && lastGMIOutput.toolCalls.length > 0) {
@@ -832,6 +1428,67 @@ export class AgentOSOrchestrator {
832
1428
  if (didForceTerminate || Boolean(finalGMIStateForResponse.error)) {
833
1429
  turnMetricsStatus = 'error';
834
1430
  }
1431
+ const taskOutcome = evaluateTaskOutcome({
1432
+ finalOutput: finalGMIStateForResponse,
1433
+ didForceTerminate,
1434
+ degraded: lifecycleDegraded,
1435
+ customFlags: input.options?.customFlags,
1436
+ });
1437
+ turnMetricsTaskOutcome = taskOutcome;
1438
+ const taskOutcomeKpi = this.updateTaskOutcomeKpi({
1439
+ outcome: taskOutcome,
1440
+ organizationId: organizationIdForMemory,
1441
+ personaId: currentPersonaId,
1442
+ });
1443
+ const taskOutcomeAlert = this.maybeBuildTaskOutcomeAlert(taskOutcomeKpi);
1444
+ await this.pushChunkToStream(agentOSStreamId, AgentOSResponseChunkType.METADATA_UPDATE, gmiInstanceIdForChunks, currentPersonaId, false, {
1445
+ updates: {
1446
+ taskOutcome,
1447
+ taskOutcomeKpi,
1448
+ taskOutcomeAlert,
1449
+ },
1450
+ });
1451
+ if (turnMetricsStatus === 'error') {
1452
+ await this.emitExecutionLifecycleUpdate({
1453
+ streamId: agentOSStreamId,
1454
+ gmiInstanceId: gmiInstanceIdForChunks,
1455
+ personaId: currentPersonaId,
1456
+ phase: 'errored',
1457
+ status: 'error',
1458
+ details: {
1459
+ didForceTerminate,
1460
+ hasFinalError: Boolean(finalGMIStateForResponse.error),
1461
+ taskOutcomeStatus: taskOutcome.status,
1462
+ taskOutcomeScore: taskOutcome.score,
1463
+ },
1464
+ });
1465
+ }
1466
+ else {
1467
+ if (lifecycleDegraded) {
1468
+ await this.emitExecutionLifecycleUpdate({
1469
+ streamId: agentOSStreamId,
1470
+ gmiInstanceId: gmiInstanceIdForChunks,
1471
+ personaId: currentPersonaId,
1472
+ phase: 'recovered',
1473
+ status: 'ok',
1474
+ details: {
1475
+ recovery: 'Turn completed with fallback path.',
1476
+ },
1477
+ });
1478
+ }
1479
+ await this.emitExecutionLifecycleUpdate({
1480
+ streamId: agentOSStreamId,
1481
+ gmiInstanceId: gmiInstanceIdForChunks,
1482
+ personaId: currentPersonaId,
1483
+ phase: 'completed',
1484
+ status: 'ok',
1485
+ details: {
1486
+ toolIterations: currentToolCallIteration,
1487
+ taskOutcomeStatus: taskOutcome.status,
1488
+ taskOutcomeScore: taskOutcome.score,
1489
+ },
1490
+ });
1491
+ }
835
1492
  // Persist assistant output into ConversationContext for durable memory / prompt reconstruction.
836
1493
  if (this.config.enableConversationalPersistence && conversationContext) {
837
1494
  const persistContext = conversationContext;
@@ -883,7 +1540,39 @@ export class AgentOSOrchestrator {
883
1540
  recordExceptionOnActiveSpan(error, `Error in orchestrateTurn for stream ${agentOSStreamId}`);
884
1541
  const gmiErr = GMIError.wrap?.(error, GMIErrorCode.GMI_PROCESSING_ERROR, `Error in orchestrateTurn for stream ${agentOSStreamId}`) ||
885
1542
  new GMIError(`Error in orchestrateTurn for stream ${agentOSStreamId}: ${error.message}`, GMIErrorCode.GMI_PROCESSING_ERROR, error);
1543
+ turnMetricsTaskOutcome = {
1544
+ status: 'failed',
1545
+ score: 0,
1546
+ reason: `Exception before completion: ${gmiErr.code}`,
1547
+ source: 'heuristic',
1548
+ };
1549
+ const taskOutcomeKpi = this.updateTaskOutcomeKpi({
1550
+ outcome: turnMetricsTaskOutcome,
1551
+ organizationId: organizationIdForMemory,
1552
+ personaId: currentPersonaId,
1553
+ });
1554
+ const taskOutcomeAlert = this.maybeBuildTaskOutcomeAlert(taskOutcomeKpi);
886
1555
  console.error(`AgentOSOrchestrator: Error during _processTurnInternal for stream ${agentOSStreamId}:`, gmiErr);
1556
+ await this.pushChunkToStream(agentOSStreamId, AgentOSResponseChunkType.METADATA_UPDATE, gmiInstanceIdForChunks, currentPersonaId ?? 'unknown_persona', false, {
1557
+ updates: {
1558
+ taskOutcome: turnMetricsTaskOutcome,
1559
+ taskOutcomeKpi,
1560
+ taskOutcomeAlert,
1561
+ },
1562
+ });
1563
+ await this.emitExecutionLifecycleUpdate({
1564
+ streamId: agentOSStreamId,
1565
+ gmiInstanceId: gmiInstanceIdForChunks,
1566
+ personaId: currentPersonaId ?? 'unknown_persona',
1567
+ phase: 'errored',
1568
+ status: 'error',
1569
+ details: {
1570
+ code: gmiErr.code,
1571
+ message: gmiErr.message,
1572
+ taskOutcomeStatus: turnMetricsTaskOutcome.status,
1573
+ taskOutcomeScore: turnMetricsTaskOutcome.score,
1574
+ },
1575
+ });
887
1576
  await this.pushErrorChunk(agentOSStreamId, currentPersonaId ?? 'unknown_persona', gmiInstanceIdForChunks, gmiErr.code, gmiErr.message, gmiErr.details);
888
1577
  await this.dependencies.streamingManager.closeStream(agentOSStreamId, "Error during turn processing.");
889
1578
  }
@@ -893,6 +1582,8 @@ export class AgentOSOrchestrator {
893
1582
  status: turnMetricsStatus,
894
1583
  personaId: turnMetricsPersonaId,
895
1584
  usage: turnMetricsUsage,
1585
+ taskOutcomeStatus: turnMetricsTaskOutcome?.status,
1586
+ taskOutcomeScore: turnMetricsTaskOutcome?.score,
896
1587
  });
897
1588
  // Stream is closed explicitly in the success/error paths; this finally block always
898
1589
  // clears internal state to avoid leaks.
@@ -1244,6 +1935,8 @@ export class AgentOSOrchestrator {
1244
1935
  }
1245
1936
  }
1246
1937
  this.activeStreamContexts.clear();
1938
+ this.taskOutcomeKpiWindows.clear();
1939
+ this.taskOutcomeAlertState.clear();
1247
1940
  this.initialized = false;
1248
1941
  console.log('AgentOSOrchestrator: Shutdown complete.');
1249
1942
  }