zora-agent 0.9.4 → 0.9.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1 -1
- package/README.md +103 -92
- package/dist/cli/audit-commands.d.ts.map +1 -1
- package/dist/cli/audit-commands.js +3 -1
- package/dist/cli/audit-commands.js.map +1 -1
- package/dist/cli/daemon.js +86 -28
- package/dist/cli/daemon.js.map +1 -1
- package/dist/cli/edit-commands.d.ts.map +1 -1
- package/dist/cli/edit-commands.js +3 -1
- package/dist/cli/edit-commands.js.map +1 -1
- package/dist/cli/hook-commands.d.ts +9 -0
- package/dist/cli/hook-commands.d.ts.map +1 -0
- package/dist/cli/hook-commands.js +106 -0
- package/dist/cli/hook-commands.js.map +1 -0
- package/dist/cli/index.js +87 -35
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init-command.d.ts.map +1 -1
- package/dist/cli/init-command.js +108 -9
- package/dist/cli/init-command.js.map +1 -1
- package/dist/cli/memory-commands.d.ts +1 -1
- package/dist/cli/memory-commands.d.ts.map +1 -1
- package/dist/cli/memory-commands.js +213 -1
- package/dist/cli/memory-commands.js.map +1 -1
- package/dist/cli/presets.d.ts.map +1 -1
- package/dist/cli/presets.js +2 -1
- package/dist/cli/presets.js.map +1 -1
- package/dist/cli/skill-commands.d.ts.map +1 -1
- package/dist/cli/skill-commands.js +4 -2
- package/dist/cli/skill-commands.js.map +1 -1
- package/dist/cli/steer-commands.d.ts.map +1 -1
- package/dist/cli/steer-commands.js +6 -4
- package/dist/cli/steer-commands.js.map +1 -1
- package/dist/cli/team-commands.d.ts.map +1 -1
- package/dist/cli/team-commands.js +3 -1
- package/dist/cli/team-commands.js.map +1 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +12 -2
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/loader.d.ts +23 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +64 -3
- package/dist/config/loader.js.map +1 -1
- package/dist/config/policy-loader.d.ts +14 -0
- package/dist/config/policy-loader.d.ts.map +1 -1
- package/dist/config/policy-loader.js +33 -0
- package/dist/config/policy-loader.js.map +1 -1
- package/dist/dashboard/frontend/dist/assets/index-BcOGj1EF.css +1 -0
- package/dist/dashboard/frontend/dist/assets/index-BtiFO9YN.js +261 -0
- package/dist/dashboard/frontend/dist/assets/index-Cfjy5acU.css +1 -0
- package/dist/dashboard/frontend/dist/assets/index-D41hcjgc.js +253 -0
- package/dist/dashboard/frontend/dist/assets/index-D83BawFd.css +1 -0
- package/dist/dashboard/frontend/dist/assets/index-DAODjoxu.css +1 -0
- package/dist/dashboard/frontend/dist/assets/index-DB-Eu5oV.js +253 -0
- package/dist/dashboard/frontend/dist/assets/index-W0VVEDu6.js +253 -0
- package/dist/dashboard/frontend/dist/index.html +17 -0
- package/dist/dashboard/server.d.ts +19 -2
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +121 -20
- package/dist/dashboard/server.js.map +1 -1
- package/dist/hooks/hook-runner.d.ts +55 -0
- package/dist/hooks/hook-runner.d.ts.map +1 -0
- package/dist/hooks/hook-runner.js +120 -0
- package/dist/hooks/hook-runner.js.map +1 -0
- package/dist/hooks/hook-types.d.ts +82 -0
- package/dist/hooks/hook-types.d.ts.map +1 -0
- package/dist/hooks/hook-types.js +20 -0
- package/dist/hooks/hook-types.js.map +1 -0
- package/dist/hooks/index.d.ts +6 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +6 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/memory/context-compressor.d.ts +108 -0
- package/dist/memory/context-compressor.d.ts.map +1 -0
- package/dist/memory/context-compressor.js +307 -0
- package/dist/memory/context-compressor.js.map +1 -0
- package/dist/memory/index.d.ts +1 -0
- package/dist/memory/index.d.ts.map +1 -1
- package/dist/memory/index.js +1 -0
- package/dist/memory/index.js.map +1 -1
- package/dist/memory/memory-manager.d.ts +88 -4
- package/dist/memory/memory-manager.d.ts.map +1 -1
- package/dist/memory/memory-manager.js +299 -7
- package/dist/memory/memory-manager.js.map +1 -1
- package/dist/memory/observation-store.d.ts +75 -0
- package/dist/memory/observation-store.d.ts.map +1 -0
- package/dist/memory/observation-store.js +162 -0
- package/dist/memory/observation-store.js.map +1 -0
- package/dist/memory/observer-worker.d.ts +34 -0
- package/dist/memory/observer-worker.d.ts.map +1 -0
- package/dist/memory/observer-worker.js +161 -0
- package/dist/memory/observer-worker.js.map +1 -0
- package/dist/memory/reflector-worker.d.ts +40 -0
- package/dist/memory/reflector-worker.d.ts.map +1 -0
- package/dist/memory/reflector-worker.js +185 -0
- package/dist/memory/reflector-worker.js.map +1 -0
- package/dist/memory/salience-scorer.d.ts +16 -6
- package/dist/memory/salience-scorer.d.ts.map +1 -1
- package/dist/memory/salience-scorer.js +42 -22
- package/dist/memory/salience-scorer.js.map +1 -1
- package/dist/memory/structured-memory.d.ts +36 -1
- package/dist/memory/structured-memory.d.ts.map +1 -1
- package/dist/memory/structured-memory.js +207 -8
- package/dist/memory/structured-memory.js.map +1 -1
- package/dist/memory/token-estimator.d.ts +31 -0
- package/dist/memory/token-estimator.d.ts.map +1 -0
- package/dist/memory/token-estimator.js +77 -0
- package/dist/memory/token-estimator.js.map +1 -0
- package/dist/memory/validation-pipeline.d.ts +37 -0
- package/dist/memory/validation-pipeline.d.ts.map +1 -0
- package/dist/memory/validation-pipeline.js +106 -0
- package/dist/memory/validation-pipeline.js.map +1 -0
- package/dist/orchestrator/auth-monitor.d.ts.map +1 -1
- package/dist/orchestrator/auth-monitor.js +3 -1
- package/dist/orchestrator/auth-monitor.js.map +1 -1
- package/dist/orchestrator/execution-loop.d.ts +23 -0
- package/dist/orchestrator/execution-loop.d.ts.map +1 -1
- package/dist/orchestrator/execution-loop.js +60 -19
- package/dist/orchestrator/execution-loop.js.map +1 -1
- package/dist/orchestrator/failover-controller.d.ts +26 -2
- package/dist/orchestrator/failover-controller.d.ts.map +1 -1
- package/dist/orchestrator/failover-controller.js +143 -23
- package/dist/orchestrator/failover-controller.js.map +1 -1
- package/dist/orchestrator/orchestrator.d.ts +70 -7
- package/dist/orchestrator/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/orchestrator.js +416 -92
- package/dist/orchestrator/orchestrator.js.map +1 -1
- package/dist/orchestrator/retry-queue.d.ts.map +1 -1
- package/dist/orchestrator/retry-queue.js +24 -9
- package/dist/orchestrator/retry-queue.js.map +1 -1
- package/dist/orchestrator/router.d.ts +16 -1
- package/dist/orchestrator/router.d.ts.map +1 -1
- package/dist/orchestrator/router.js +79 -20
- package/dist/orchestrator/router.js.map +1 -1
- package/dist/orchestrator/session-manager.d.ts +26 -1
- package/dist/orchestrator/session-manager.d.ts.map +1 -1
- package/dist/orchestrator/session-manager.js +88 -4
- package/dist/orchestrator/session-manager.js.map +1 -1
- package/dist/providers/circuit-breaker.d.ts +78 -0
- package/dist/providers/circuit-breaker.d.ts.map +1 -0
- package/dist/providers/circuit-breaker.js +129 -0
- package/dist/providers/circuit-breaker.js.map +1 -0
- package/dist/providers/claude-provider.d.ts +27 -11
- package/dist/providers/claude-provider.d.ts.map +1 -1
- package/dist/providers/claude-provider.js +161 -46
- package/dist/providers/claude-provider.js.map +1 -1
- package/dist/providers/gemini-provider.d.ts +9 -1
- package/dist/providers/gemini-provider.d.ts.map +1 -1
- package/dist/providers/gemini-provider.js +97 -48
- package/dist/providers/gemini-provider.js.map +1 -1
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +1 -0
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/ollama-provider.d.ts +7 -0
- package/dist/providers/ollama-provider.d.ts.map +1 -1
- package/dist/providers/ollama-provider.js +89 -18
- package/dist/providers/ollama-provider.js.map +1 -1
- package/dist/routines/heartbeat.d.ts +10 -2
- package/dist/routines/heartbeat.d.ts.map +1 -1
- package/dist/routines/heartbeat.js +42 -5
- package/dist/routines/heartbeat.js.map +1 -1
- package/dist/routines/routine-manager.d.ts.map +1 -1
- package/dist/routines/routine-manager.js +22 -15
- package/dist/routines/routine-manager.js.map +1 -1
- package/dist/security/audit-logger.d.ts.map +1 -1
- package/dist/security/audit-logger.js +5 -7
- package/dist/security/audit-logger.js.map +1 -1
- package/dist/security/policy-engine.d.ts +28 -17
- package/dist/security/policy-engine.d.ts.map +1 -1
- package/dist/security/policy-engine.js +42 -185
- package/dist/security/policy-engine.js.map +1 -1
- package/dist/security/policy-serializer.d.ts +19 -0
- package/dist/security/policy-serializer.d.ts.map +1 -0
- package/dist/security/policy-serializer.js +100 -0
- package/dist/security/policy-serializer.js.map +1 -0
- package/dist/security/shell-validator.d.ts +42 -0
- package/dist/security/shell-validator.d.ts.map +1 -0
- package/dist/security/shell-validator.js +231 -0
- package/dist/security/shell-validator.js.map +1 -0
- package/dist/skills/index.d.ts +1 -0
- package/dist/skills/index.d.ts.map +1 -1
- package/dist/skills/index.js +1 -0
- package/dist/skills/index.js.map +1 -1
- package/dist/skills/skill-loader.d.ts +38 -2
- package/dist/skills/skill-loader.d.ts.map +1 -1
- package/dist/skills/skill-loader.js +83 -2
- package/dist/skills/skill-loader.js.map +1 -1
- package/dist/skills/subagent-loader.d.ts +66 -0
- package/dist/skills/subagent-loader.d.ts.map +1 -0
- package/dist/skills/subagent-loader.js +143 -0
- package/dist/skills/subagent-loader.js.map +1 -0
- package/dist/steering/flag-manager.d.ts +20 -0
- package/dist/steering/flag-manager.d.ts.map +1 -1
- package/dist/steering/flag-manager.js +94 -11
- package/dist/steering/flag-manager.js.map +1 -1
- package/dist/steering/steering-manager.d.ts +11 -0
- package/dist/steering/steering-manager.d.ts.map +1 -1
- package/dist/steering/steering-manager.js +23 -0
- package/dist/steering/steering-manager.js.map +1 -1
- package/dist/steering/telegram-gateway.d.ts +4 -1
- package/dist/steering/telegram-gateway.d.ts.map +1 -1
- package/dist/steering/telegram-gateway.js +49 -10
- package/dist/steering/telegram-gateway.js.map +1 -1
- package/dist/teams/bridge-watchdog.d.ts.map +1 -1
- package/dist/teams/bridge-watchdog.js +5 -3
- package/dist/teams/bridge-watchdog.js.map +1 -1
- package/dist/teams/gemini-bridge.d.ts.map +1 -1
- package/dist/teams/gemini-bridge.js +9 -4
- package/dist/teams/gemini-bridge.js.map +1 -1
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +2 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/memory-tools.d.ts +16 -0
- package/dist/tools/memory-tools.d.ts.map +1 -0
- package/dist/tools/memory-tools.js +207 -0
- package/dist/tools/memory-tools.js.map +1 -0
- package/dist/tools/notifications.d.ts.map +1 -1
- package/dist/tools/notifications.js +3 -1
- package/dist/tools/notifications.js.map +1 -1
- package/dist/tools/tool-factory.d.ts +36 -0
- package/dist/tools/tool-factory.d.ts.map +1 -0
- package/dist/tools/tool-factory.js +55 -0
- package/dist/tools/tool-factory.js.map +1 -0
- package/dist/types.d.ts +205 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +47 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/errors.d.ts +21 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +29 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/event-filter.d.ts +25 -0
- package/dist/utils/event-filter.d.ts.map +1 -0
- package/dist/utils/event-filter.js +61 -0
- package/dist/utils/event-filter.js.map +1 -0
- package/dist/utils/logger.d.ts +33 -36
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +60 -130
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/validate-job-id.d.ts +6 -0
- package/dist/utils/validate-job-id.d.ts.map +1 -0
- package/dist/utils/validate-job-id.js +10 -0
- package/dist/utils/validate-job-id.js.map +1 -0
- package/package.json +12 -3
|
@@ -13,19 +13,29 @@ import crypto from 'node:crypto';
|
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import os from 'node:os';
|
|
15
15
|
import fs from 'node:fs';
|
|
16
|
+
import { HookRunner } from '../hooks/hook-runner.js';
|
|
16
17
|
import { Router } from './router.js';
|
|
17
18
|
import { FailoverController } from './failover-controller.js';
|
|
18
19
|
import { RetryQueue } from './retry-queue.js';
|
|
19
20
|
import { AuthMonitor } from './auth-monitor.js';
|
|
20
|
-
import { SessionManager } from './session-manager.js';
|
|
21
|
-
import { ExecutionLoop } from './execution-loop.js';
|
|
21
|
+
import { SessionManager, BufferedSessionWriter } from './session-manager.js';
|
|
22
|
+
import { ExecutionLoop, defaultTransformContext } from './execution-loop.js';
|
|
22
23
|
import { SteeringManager } from '../steering/steering-manager.js';
|
|
23
24
|
import { MemoryManager } from '../memory/memory-manager.js';
|
|
25
|
+
import { ExtractionPipeline } from '../memory/extraction-pipeline.js';
|
|
26
|
+
import { createMemoryTools } from '../tools/memory-tools.js';
|
|
27
|
+
import { ValidationPipeline } from '../memory/validation-pipeline.js';
|
|
28
|
+
import { ContextCompressor } from '../memory/context-compressor.js';
|
|
29
|
+
import { ObservationStore } from '../memory/observation-store.js';
|
|
24
30
|
import { HeartbeatSystem } from '../routines/heartbeat.js';
|
|
25
31
|
import { RoutineManager } from '../routines/routine-manager.js';
|
|
26
32
|
import { NotificationTools } from '../tools/notifications.js';
|
|
27
33
|
import { PolicyEngine } from '../security/policy-engine.js';
|
|
28
34
|
import { IntentCapsuleManager } from '../security/intent-capsule.js';
|
|
35
|
+
import { LeakDetector } from '../security/leak-detector.js';
|
|
36
|
+
import { sanitizeInput } from '../security/prompt-defense.js';
|
|
37
|
+
import { createLogger } from '../utils/logger.js';
|
|
38
|
+
const log = createLogger('orchestrator');
|
|
29
39
|
export class Orchestrator {
|
|
30
40
|
_config;
|
|
31
41
|
_policy;
|
|
@@ -43,12 +53,22 @@ export class Orchestrator {
|
|
|
43
53
|
_notifications;
|
|
44
54
|
// Security
|
|
45
55
|
_intentCapsuleManager;
|
|
56
|
+
_leakDetector;
|
|
46
57
|
// Background systems
|
|
47
58
|
_heartbeatSystem = null;
|
|
48
59
|
_routineManager = null;
|
|
60
|
+
// Memory tools
|
|
61
|
+
_validationPipeline;
|
|
62
|
+
// ORCH-12: Lifecycle hooks
|
|
63
|
+
_hookRunner = new HookRunner();
|
|
64
|
+
// ORCH-14: Context transform callback
|
|
65
|
+
_transformContext = defaultTransformContext;
|
|
66
|
+
// Context compression
|
|
67
|
+
_observationStore;
|
|
49
68
|
// Background intervals
|
|
50
69
|
_authCheckTimeout = null;
|
|
51
70
|
_retryPollTimeout = null;
|
|
71
|
+
_consolidationTimeout = null;
|
|
52
72
|
_booted = false;
|
|
53
73
|
constructor(options) {
|
|
54
74
|
this._config = options.config;
|
|
@@ -58,6 +78,20 @@ export class Orchestrator {
|
|
|
58
78
|
}
|
|
59
79
|
/**
|
|
60
80
|
* Boots all subsystems and starts background loops.
|
|
81
|
+
*
|
|
82
|
+
* Initialization order:
|
|
83
|
+
* 1. PolicyEngine + IntentCapsuleManager (security layer).
|
|
84
|
+
* 2. SessionManager (event persistence).
|
|
85
|
+
* 3. SteeringManager (human-in-the-loop).
|
|
86
|
+
* 4. MemoryManager (context injection).
|
|
87
|
+
* 5. Router (provider selection).
|
|
88
|
+
* 6. FailoverController (error recovery).
|
|
89
|
+
* 7. RetryQueue (deferred retry).
|
|
90
|
+
* 8. AuthMonitor (periodic auth checks every 5 min).
|
|
91
|
+
* 9. HeartbeatSystem + RoutineManager (scheduled tasks).
|
|
92
|
+
*
|
|
93
|
+
* Background loops use self-rescheduling setTimeout (not setInterval)
|
|
94
|
+
* to avoid overlapping async executions.
|
|
61
95
|
*/
|
|
62
96
|
async boot() {
|
|
63
97
|
if (this._booted)
|
|
@@ -69,11 +103,17 @@ export class Orchestrator {
|
|
|
69
103
|
// ASI01: Create IntentCapsuleManager with per-session signing key
|
|
70
104
|
this._intentCapsuleManager = new IntentCapsuleManager(crypto.randomBytes(32).toString('hex'));
|
|
71
105
|
this._policyEngine.setIntentCapsuleManager(this._intentCapsuleManager);
|
|
106
|
+
// SEC-03: Wire LeakDetector for scanning tool outputs
|
|
107
|
+
this._leakDetector = new LeakDetector();
|
|
72
108
|
this._sessionManager = new SessionManager(this._baseDir);
|
|
73
109
|
this._steeringManager = new SteeringManager(this._baseDir);
|
|
74
110
|
await this._steeringManager.init();
|
|
75
111
|
this._memoryManager = new MemoryManager(this._config.memory, this._baseDir);
|
|
76
112
|
await this._memoryManager.init();
|
|
113
|
+
this._validationPipeline = new ValidationPipeline();
|
|
114
|
+
// Initialize observation store for context compression
|
|
115
|
+
this._observationStore = new ObservationStore(path.join(this._baseDir, 'memory', 'observations'));
|
|
116
|
+
await this._observationStore.init();
|
|
77
117
|
// R2: Wire Router
|
|
78
118
|
this._router = new Router({
|
|
79
119
|
providers: this._providers,
|
|
@@ -99,7 +139,7 @@ export class Orchestrator {
|
|
|
99
139
|
await this._authMonitor.checkAll();
|
|
100
140
|
}
|
|
101
141
|
catch (err) {
|
|
102
|
-
|
|
142
|
+
log.error({ err }, 'AuthMonitor check failed');
|
|
103
143
|
}
|
|
104
144
|
scheduleAuthCheck();
|
|
105
145
|
}, 5 * 60 * 1000);
|
|
@@ -116,13 +156,13 @@ export class Orchestrator {
|
|
|
116
156
|
await this._retryQueue.remove(task.jobId);
|
|
117
157
|
}
|
|
118
158
|
catch (err) {
|
|
119
|
-
|
|
159
|
+
log.error({ jobId: task.jobId, err }, 'Retry failed');
|
|
120
160
|
// Leave task in queue for next poll cycle
|
|
121
161
|
}
|
|
122
162
|
}
|
|
123
163
|
}
|
|
124
164
|
catch (err) {
|
|
125
|
-
|
|
165
|
+
log.error({ err }, 'RetryQueue poll failed');
|
|
126
166
|
}
|
|
127
167
|
scheduleRetryPoll();
|
|
128
168
|
}, 30 * 1000);
|
|
@@ -148,6 +188,31 @@ export class Orchestrator {
|
|
|
148
188
|
maxCostTier: opts.maxCostTier,
|
|
149
189
|
}), this._baseDir);
|
|
150
190
|
await this._routineManager.init();
|
|
191
|
+
// Schedule daily note consolidation (check once per day)
|
|
192
|
+
const scheduleConsolidation = () => {
|
|
193
|
+
this._consolidationTimeout = setTimeout(async () => {
|
|
194
|
+
try {
|
|
195
|
+
const count = await this._memoryManager.consolidateDailyNotes(7);
|
|
196
|
+
if (count > 0) {
|
|
197
|
+
log.info({ consolidated: count }, 'Daily notes consolidated');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
log.warn({ err }, 'Daily note consolidation failed');
|
|
202
|
+
}
|
|
203
|
+
scheduleConsolidation();
|
|
204
|
+
}, 24 * 60 * 60 * 1000); // 24 hours
|
|
205
|
+
};
|
|
206
|
+
// Run first check shortly after boot (30 seconds), then daily
|
|
207
|
+
this._consolidationTimeout = setTimeout(async () => {
|
|
208
|
+
try {
|
|
209
|
+
await this._memoryManager.consolidateDailyNotes(7);
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
log.warn({ err }, 'Initial daily note consolidation failed');
|
|
213
|
+
}
|
|
214
|
+
scheduleConsolidation();
|
|
215
|
+
}, 30 * 1000);
|
|
151
216
|
this._booted = true;
|
|
152
217
|
}
|
|
153
218
|
/**
|
|
@@ -165,6 +230,10 @@ export class Orchestrator {
|
|
|
165
230
|
clearTimeout(this._retryPollTimeout);
|
|
166
231
|
this._retryPollTimeout = null;
|
|
167
232
|
}
|
|
233
|
+
if (this._consolidationTimeout) {
|
|
234
|
+
clearTimeout(this._consolidationTimeout);
|
|
235
|
+
this._consolidationTimeout = null;
|
|
236
|
+
}
|
|
168
237
|
// Stop heartbeat and routines
|
|
169
238
|
if (this._heartbeatSystem) {
|
|
170
239
|
this._heartbeatSystem.stop();
|
|
@@ -177,16 +246,34 @@ export class Orchestrator {
|
|
|
177
246
|
this._booted = false;
|
|
178
247
|
}
|
|
179
248
|
/**
|
|
180
|
-
* Submits a task through the full orchestration pipeline
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
249
|
+
* Submits a task through the full orchestration pipeline.
|
|
250
|
+
*
|
|
251
|
+
* Pipeline stages:
|
|
252
|
+
* 1. Load memory context from MemoryManager (daily notes, long-term items).
|
|
253
|
+
* 2. Load SOUL.md identity file and build the system prompt with policy awareness hints.
|
|
254
|
+
* 3. Create a signed intent capsule for goal drift detection (ASI01).
|
|
255
|
+
* 4. Classify the task by complexity and resource type for routing.
|
|
256
|
+
* 5. Route to the best available provider via the Router.
|
|
257
|
+
* 6. Execute via _executeWithProvider, which handles event persistence,
|
|
258
|
+
* steering injection, failover, and retry queueing.
|
|
259
|
+
*
|
|
260
|
+
* @returns The final text result from the provider's 'done' event.
|
|
261
|
+
* @throws If no provider is available or all failover attempts fail.
|
|
185
262
|
*/
|
|
186
263
|
async submitTask(options) {
|
|
187
264
|
const jobId = options.jobId ?? `job_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
188
|
-
//
|
|
189
|
-
|
|
265
|
+
// Reset per-task state: ValidationPipeline rate limit is per-session, not per-orchestrator-lifetime.
|
|
266
|
+
// Without this, after MAX_SAVES_PER_SESSION saves across all tasks, memory_save permanently blocks.
|
|
267
|
+
this._validationPipeline.resetSession();
|
|
268
|
+
// MEM-05 / ORCH-07: Progressive memory context — lightweight index, not full dump.
|
|
269
|
+
// The LLM uses memory_search / recall_context tools for on-demand retrieval.
|
|
270
|
+
let memoryContext = [];
|
|
271
|
+
try {
|
|
272
|
+
memoryContext = await this._memoryManager.loadContext();
|
|
273
|
+
}
|
|
274
|
+
catch (err) {
|
|
275
|
+
log.warn({ err, jobId }, 'Memory context injection failed, continuing without memory');
|
|
276
|
+
}
|
|
190
277
|
// Load SOUL.md for agent identity (fixes bug: file was created but never read)
|
|
191
278
|
const soulPath = this._config.agent.identity.soul_file.replace(/^~/, os.homedir());
|
|
192
279
|
let soulContent = '';
|
|
@@ -198,25 +285,57 @@ export class Orchestrator {
|
|
|
198
285
|
catch {
|
|
199
286
|
// SOUL.md missing or unreadable — use default identity
|
|
200
287
|
}
|
|
288
|
+
// Create per-task context compressor if compression is enabled
|
|
289
|
+
let compressor = null;
|
|
290
|
+
if (this._config.memory?.compression?.enabled) {
|
|
291
|
+
const compressFn = async (prompt) => {
|
|
292
|
+
const compressLoop = new ExecutionLoop({
|
|
293
|
+
systemPrompt: 'You are a conversation observer. Compress messages into concise, dated observations. Respond with ONLY the observations.',
|
|
294
|
+
permissionMode: 'default',
|
|
295
|
+
cwd: process.cwd(),
|
|
296
|
+
maxTurns: 1,
|
|
297
|
+
model: this._config.memory.compression.model,
|
|
298
|
+
});
|
|
299
|
+
return compressLoop.run(prompt);
|
|
300
|
+
};
|
|
301
|
+
compressor = new ContextCompressor(this._config.memory.compression, this._observationStore, compressFn, jobId);
|
|
302
|
+
await compressor.loadExisting();
|
|
303
|
+
}
|
|
304
|
+
// Build cross-session context from observations
|
|
305
|
+
const crossSessionContext = compressor
|
|
306
|
+
? compressor.buildContext().crossSessionContext
|
|
307
|
+
: '';
|
|
201
308
|
// Build system prompt with policy awareness
|
|
202
|
-
const
|
|
309
|
+
const systemPromptParts = [
|
|
203
310
|
soulContent || 'You are Zora, a helpful autonomous agent.',
|
|
204
311
|
'[SECURITY] You operate under a permission policy. Before planning any task,',
|
|
205
312
|
'use the check_permissions tool to verify you have access to the paths and',
|
|
206
313
|
'commands you need. If access is denied, tell the user what you need and why.',
|
|
207
314
|
'Do NOT attempt actions without checking first.',
|
|
208
315
|
...memoryContext,
|
|
209
|
-
]
|
|
316
|
+
];
|
|
317
|
+
// Append cross-session observations if available
|
|
318
|
+
if (crossSessionContext) {
|
|
319
|
+
systemPromptParts.push(`[PRIOR SESSION CONTEXT]:\n${crossSessionContext}`);
|
|
320
|
+
}
|
|
321
|
+
const systemPrompt = systemPromptParts.join('\n\n');
|
|
322
|
+
// SEC-03: Scan user prompt for injection patterns (warn but don't block by default)
|
|
323
|
+
const sanitizedPrompt = sanitizeInput(options.prompt);
|
|
324
|
+
if (sanitizedPrompt !== options.prompt) {
|
|
325
|
+
log.warn({ jobId }, 'Prompt injection pattern detected in user input — sanitized');
|
|
326
|
+
}
|
|
210
327
|
// ASI01: Create signed intent capsule for goal drift detection
|
|
211
328
|
if (this._intentCapsuleManager) {
|
|
212
|
-
this._intentCapsuleManager.createCapsule(
|
|
329
|
+
this._intentCapsuleManager.createCapsule(sanitizedPrompt);
|
|
213
330
|
}
|
|
214
331
|
// Classify task for routing
|
|
215
|
-
const classification = this._router.classifyTask(
|
|
332
|
+
const classification = this._router.classifyTask(sanitizedPrompt);
|
|
333
|
+
// Build custom tools (permissions + memory tools + recall_context)
|
|
334
|
+
const customTools = this._createCustomTools();
|
|
216
335
|
// Build task context
|
|
217
336
|
const taskContext = {
|
|
218
337
|
jobId,
|
|
219
|
-
task:
|
|
338
|
+
task: sanitizedPrompt,
|
|
220
339
|
requiredCapabilities: [],
|
|
221
340
|
complexity: classification.complexity,
|
|
222
341
|
resourceType: classification.resourceType,
|
|
@@ -226,117 +345,286 @@ export class Orchestrator {
|
|
|
226
345
|
modelPreference: options.model,
|
|
227
346
|
maxCostTier: options.maxCostTier,
|
|
228
347
|
maxTurns: options.maxTurns,
|
|
348
|
+
customTools,
|
|
229
349
|
canUseTool: this._policyEngine.createCanUseTool(),
|
|
230
350
|
};
|
|
351
|
+
// ORCH-12: Run onTaskStart hooks (can modify context before routing)
|
|
352
|
+
const hookedContext = await this._hookRunner.runOnTaskStart(taskContext);
|
|
231
353
|
// R2: Route to provider
|
|
232
354
|
let selectedProvider;
|
|
233
355
|
try {
|
|
234
|
-
selectedProvider = await this._router.selectProvider(
|
|
356
|
+
selectedProvider = await this._router.selectProvider(hookedContext);
|
|
235
357
|
}
|
|
236
358
|
catch (err) {
|
|
237
359
|
throw new Error(`No provider available: ${err instanceof Error ? err.message : String(err)}`);
|
|
238
360
|
}
|
|
239
|
-
// Execute with the selected provider
|
|
240
|
-
return this._executeWithProvider(selectedProvider,
|
|
361
|
+
// Execute with the selected provider (injectionDepth=0 for initial call)
|
|
362
|
+
return this._executeWithProvider(selectedProvider, hookedContext, options.onEvent, 0, 0, compressor);
|
|
241
363
|
}
|
|
242
|
-
/**
|
|
243
|
-
static
|
|
364
|
+
/** Tracks errors that have already been through the failover path */
|
|
365
|
+
static _failoverErrors = new WeakSet();
|
|
244
366
|
/** Maximum depth of failover recursion to prevent unbounded re-execution */
|
|
245
367
|
static MAX_FAILOVER_DEPTH = 3;
|
|
368
|
+
/** ORCH-16: Maximum depth of onTaskEnd follow-up injection loops */
|
|
369
|
+
static MAX_INJECTION_LOOPS = 3;
|
|
246
370
|
/**
|
|
247
371
|
* Executes a task with a specific provider, handling failover and event persistence.
|
|
372
|
+
*
|
|
373
|
+
* During execution, this method:
|
|
374
|
+
* - Persists every event to the SessionManager for crash recovery.
|
|
375
|
+
* - Polls SteeringManager after text/tool_result events, injecting any pending
|
|
376
|
+
* human steering messages into the event stream.
|
|
377
|
+
* - On error events: attempts failover via FailoverController. If failover
|
|
378
|
+
* succeeds, recurses with the new provider (incrementing failoverDepth).
|
|
379
|
+
* If failover fails, enqueues the task in the RetryQueue.
|
|
380
|
+
* - failoverDepth is capped at MAX_FAILOVER_DEPTH (3) to prevent unbounded recursion.
|
|
381
|
+
* - The _failoverErrors WeakSet prevents double-failover: errors already processed
|
|
382
|
+
* by the failover path are not re-triggered in the outer catch block.
|
|
248
383
|
*/
|
|
249
|
-
async _executeWithProvider(provider, taskContext, onEvent, failoverDepth = 0) {
|
|
384
|
+
async _executeWithProvider(provider, taskContext, onEvent, failoverDepth = 0, injectionDepth = 0, compressor) {
|
|
250
385
|
let result = '';
|
|
386
|
+
let eventsSinceLastTick = 0;
|
|
387
|
+
const TICK_INTERVAL = 10; // Check compression thresholds every N events
|
|
388
|
+
// Event batching: buffer session writes, flush every 500ms or on done/error.
|
|
389
|
+
// Wrapped in try/finally to ensure close() runs on ALL exit paths including failover.
|
|
390
|
+
const bufferedWriter = new BufferedSessionWriter(this._sessionManager, taskContext.jobId, 500);
|
|
251
391
|
try {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if (onEvent)
|
|
269
|
-
onEvent(steerEvent);
|
|
270
|
-
// Archive the processed message
|
|
271
|
-
await this._steeringManager.archiveMessage(taskContext.jobId, msg.id);
|
|
392
|
+
try {
|
|
393
|
+
// Execute via the provider's async generator
|
|
394
|
+
for await (const event of provider.execute(taskContext)) {
|
|
395
|
+
// R8: Persist events via buffered writer (batched disk I/O)
|
|
396
|
+
bufferedWriter.append(event);
|
|
397
|
+
// Feed events to context compressor for rolling compression
|
|
398
|
+
if (compressor) {
|
|
399
|
+
compressor.ingest(event);
|
|
400
|
+
eventsSinceLastTick++;
|
|
401
|
+
if (eventsSinceLastTick >= TICK_INTERVAL) {
|
|
402
|
+
eventsSinceLastTick = 0;
|
|
403
|
+
// tick() is async but we don't await — compression runs in background
|
|
404
|
+
compressor.tick().catch(err => {
|
|
405
|
+
log.warn({ err, jobId: taskContext.jobId }, 'Context compressor tick failed');
|
|
406
|
+
});
|
|
407
|
+
}
|
|
272
408
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
409
|
+
// SEC-03: Scan tool outputs for leaked secrets (warn, don't strip)
|
|
410
|
+
if (event.type === 'tool_result') {
|
|
411
|
+
const toolResultContent = event.content;
|
|
412
|
+
const resultText = typeof toolResultContent.result === 'string'
|
|
413
|
+
? toolResultContent.result
|
|
414
|
+
: JSON.stringify(toolResultContent.result ?? '');
|
|
415
|
+
const leaks = this._leakDetector.scan(resultText);
|
|
416
|
+
if (leaks.length > 0) {
|
|
417
|
+
log.warn({ jobId: taskContext.jobId, toolCallId: toolResultContent.toolCallId, leaks: leaks.map(l => ({ pattern: l.pattern, severity: l.severity })) }, 'Potential secret leak detected in tool output');
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// SEC-03: Scan tool call arguments for leaked secrets
|
|
421
|
+
if (event.type === 'tool_call') {
|
|
422
|
+
const toolCallContent = event.content;
|
|
423
|
+
const argsText = JSON.stringify(toolCallContent.arguments ?? {});
|
|
424
|
+
const leaks = this._leakDetector.scan(argsText);
|
|
425
|
+
if (leaks.length > 0) {
|
|
426
|
+
log.warn({ jobId: taskContext.jobId, tool: toolCallContent.tool, leaks: leaks.map(l => ({ pattern: l.pattern, severity: l.severity })) }, 'Potential secret leak detected in tool call arguments');
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// R7: Poll SteeringManager with debouncing (max once per 2 seconds)
|
|
430
|
+
if (event.type === 'text' || event.type === 'tool_result') {
|
|
431
|
+
const pendingMessages = await this._steeringManager.cachedGetPendingMessages(taskContext.jobId, 2000);
|
|
432
|
+
for (const msg of pendingMessages) {
|
|
433
|
+
// Inject steering as an event
|
|
434
|
+
const steerEvent = {
|
|
435
|
+
type: 'steering',
|
|
436
|
+
timestamp: new Date(),
|
|
437
|
+
content: { text: msg.type === 'steer' ? msg.message : `[${msg.type}]`, source: msg.source, author: msg.author },
|
|
438
|
+
};
|
|
439
|
+
bufferedWriter.append(steerEvent);
|
|
440
|
+
taskContext.history.push(steerEvent);
|
|
441
|
+
if (onEvent)
|
|
442
|
+
onEvent(steerEvent);
|
|
443
|
+
// Archive the processed message and invalidate cache
|
|
444
|
+
await this._steeringManager.archiveMessage(taskContext.jobId, msg.id);
|
|
445
|
+
this._steeringManager.invalidatePendingCache(taskContext.jobId);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
// Notify caller
|
|
449
|
+
if (onEvent)
|
|
450
|
+
onEvent(event);
|
|
451
|
+
// Track history for failover handoff
|
|
452
|
+
taskContext.history.push(event);
|
|
453
|
+
// Capture result text
|
|
454
|
+
if (event.type === 'done') {
|
|
455
|
+
result = event.content.text ?? '';
|
|
456
|
+
}
|
|
457
|
+
// Handle errors — trigger failover (R3)
|
|
458
|
+
if (event.type === 'error') {
|
|
459
|
+
const errorContent = event.content;
|
|
460
|
+
const error = new Error(errorContent.message ?? 'Unknown provider error');
|
|
461
|
+
// Guard: skip failover if depth exceeded
|
|
462
|
+
if (failoverDepth >= Orchestrator.MAX_FAILOVER_DEPTH) {
|
|
463
|
+
throw error;
|
|
464
|
+
}
|
|
465
|
+
// R3: Connect FailoverController to error path
|
|
466
|
+
const failoverResult = await this._failoverController.handleFailure(taskContext, provider, error);
|
|
467
|
+
if (failoverResult) {
|
|
468
|
+
// Re-execute with the failover provider (increment depth)
|
|
469
|
+
return this._executeWithProvider(failoverResult.nextProvider, taskContext, onEvent, failoverDepth + 1, injectionDepth, compressor);
|
|
470
|
+
}
|
|
471
|
+
// R5: Enqueue for retry if no failover available
|
|
472
|
+
try {
|
|
473
|
+
await this._retryQueue.enqueue(taskContext, error.message, this._config.failover.max_retries);
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
// Max retries exceeded or enqueue failed
|
|
477
|
+
}
|
|
478
|
+
// Mark so the outer catch doesn't re-trigger failover
|
|
479
|
+
Orchestrator._failoverErrors.add(error);
|
|
289
480
|
throw error;
|
|
290
481
|
}
|
|
291
|
-
|
|
292
|
-
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
catch (err) {
|
|
485
|
+
// Skip failover for errors already marked by the failover path
|
|
486
|
+
const isFailoverError = err instanceof Error && Orchestrator._failoverErrors.has(err);
|
|
487
|
+
if (!isFailoverError && err instanceof Error && failoverDepth < Orchestrator.MAX_FAILOVER_DEPTH) {
|
|
488
|
+
// R3: Try failover on execution exceptions
|
|
489
|
+
const failoverResult = await this._failoverController.handleFailure(taskContext, provider, err);
|
|
293
490
|
if (failoverResult) {
|
|
294
|
-
//
|
|
295
|
-
|
|
491
|
+
// Mark the error so downstream doesn't re-trigger failover
|
|
492
|
+
Orchestrator._failoverErrors.add(err);
|
|
493
|
+
return this._executeWithProvider(failoverResult.nextProvider, taskContext, onEvent, failoverDepth + 1, injectionDepth, compressor);
|
|
296
494
|
}
|
|
297
|
-
// R5: Enqueue for retry
|
|
495
|
+
// R5: Enqueue for retry
|
|
298
496
|
try {
|
|
299
|
-
await this._retryQueue.enqueue(taskContext,
|
|
497
|
+
await this._retryQueue.enqueue(taskContext, err.message, this._config.failover.max_retries);
|
|
300
498
|
}
|
|
301
499
|
catch {
|
|
302
|
-
// Max retries exceeded
|
|
500
|
+
// Max retries exceeded
|
|
303
501
|
}
|
|
304
|
-
// Mark so the outer catch doesn't re-trigger failover
|
|
305
|
-
error[Orchestrator._FAILOVER_SENTINEL] = true;
|
|
306
|
-
throw error;
|
|
307
502
|
}
|
|
503
|
+
throw err;
|
|
308
504
|
}
|
|
309
505
|
}
|
|
310
|
-
|
|
311
|
-
//
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
return this._executeWithProvider(failoverResult.nextProvider, taskContext, onEvent, failoverDepth + 1);
|
|
320
|
-
}
|
|
321
|
-
// R5: Enqueue for retry
|
|
322
|
-
try {
|
|
323
|
-
await this._retryQueue.enqueue(taskContext, err.message, this._config.failover.max_retries);
|
|
324
|
-
}
|
|
325
|
-
catch {
|
|
326
|
-
// Max retries exceeded
|
|
327
|
-
}
|
|
506
|
+
finally {
|
|
507
|
+
// Always close the buffered writer — flushes remaining events and stops the timer.
|
|
508
|
+
// This runs on all exit paths: success, throw, and failover returns.
|
|
509
|
+
await bufferedWriter.close();
|
|
510
|
+
// Flush context compressor — persist any remaining observations
|
|
511
|
+
if (compressor) {
|
|
512
|
+
await compressor.flush().catch(err => {
|
|
513
|
+
log.warn({ err, jobId: taskContext.jobId }, 'Context compressor flush failed');
|
|
514
|
+
});
|
|
328
515
|
}
|
|
329
|
-
throw err;
|
|
330
516
|
}
|
|
331
517
|
// Record completion in daily notes
|
|
332
518
|
await this._memoryManager.appendDailyNote(`Completed task: ${taskContext.task}`);
|
|
519
|
+
// MEM-09: Async memory extraction after successful job completion
|
|
520
|
+
if (this._config.memory.auto_extract) {
|
|
521
|
+
this._runExtractionAsync(taskContext).catch(err => {
|
|
522
|
+
log.warn({ err, jobId: taskContext.jobId }, 'Post-job memory extraction failed');
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
// ORCH-12: Run onTaskEnd hooks (can inspect result, optionally trigger follow-up)
|
|
526
|
+
// ORCH-16: Guard against infinite follow-up injection loops
|
|
527
|
+
const endResult = await this._hookRunner.runOnTaskEnd(taskContext, result);
|
|
528
|
+
if (endResult.followUp) {
|
|
529
|
+
if (injectionDepth >= Orchestrator.MAX_INJECTION_LOOPS) {
|
|
530
|
+
log.warn({ jobId: taskContext.jobId, depth: injectionDepth, maxDepth: Orchestrator.MAX_INJECTION_LOOPS }, 'onTaskEnd follow-up injection loop capped — skipping follow-up');
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
log.info({ jobId: taskContext.jobId, depth: injectionDepth + 1 }, 'onTaskEnd hook triggered follow-up task');
|
|
534
|
+
// Route through _executeWithProvider directly to preserve injectionDepth tracking.
|
|
535
|
+
// Re-route through the full submitTask pipeline except use incremented injectionDepth.
|
|
536
|
+
const followUpJobId = `${taskContext.jobId}_followup_${injectionDepth + 1}`;
|
|
537
|
+
// ORCH-14: Apply transformContext to prune history before follow-up
|
|
538
|
+
const transformedHistory = this._transformContext(taskContext.history, injectionDepth + 1);
|
|
539
|
+
const followUpCtx = {
|
|
540
|
+
...taskContext,
|
|
541
|
+
jobId: followUpJobId,
|
|
542
|
+
task: endResult.followUp,
|
|
543
|
+
history: transformedHistory,
|
|
544
|
+
};
|
|
545
|
+
const hookedFollowUp = await this._hookRunner.runOnTaskStart(followUpCtx);
|
|
546
|
+
const followUpProvider = await this._router.selectProvider(hookedFollowUp);
|
|
547
|
+
return this._executeWithProvider(followUpProvider, hookedFollowUp, onEvent, 0, injectionDepth + 1);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
333
550
|
return result;
|
|
334
551
|
}
|
|
552
|
+
/**
|
|
553
|
+
* MEM-09: Runs memory extraction asynchronously after job completion.
|
|
554
|
+
*
|
|
555
|
+
* Collects text events from the job history, passes them through
|
|
556
|
+
* ExtractionPipeline, deduplicates against existing items, and
|
|
557
|
+
* persists new items via StructuredMemory. Appends a daily note
|
|
558
|
+
* summarizing what was extracted.
|
|
559
|
+
*
|
|
560
|
+
* Runs fire-and-forget — errors are caught by the caller.
|
|
561
|
+
*/
|
|
562
|
+
async _runExtractionAsync(taskContext) {
|
|
563
|
+
// Collect conversation text from job history
|
|
564
|
+
const messages = taskContext.history
|
|
565
|
+
.filter(e => e.type === 'text' || e.type === 'done')
|
|
566
|
+
.map(e => {
|
|
567
|
+
const content = e.content;
|
|
568
|
+
return content.text;
|
|
569
|
+
})
|
|
570
|
+
.filter(Boolean);
|
|
571
|
+
if (messages.length === 0) {
|
|
572
|
+
return; // Nothing to extract from
|
|
573
|
+
}
|
|
574
|
+
// Get existing categories for context
|
|
575
|
+
const categories = await this._memoryManager.getCategories();
|
|
576
|
+
const categoryNames = categories.map(c => c.category);
|
|
577
|
+
// Create extraction pipeline using the first available provider as the LLM
|
|
578
|
+
const extractFn = async (prompt) => {
|
|
579
|
+
const extractLoop = new ExecutionLoop({
|
|
580
|
+
systemPrompt: 'You extract structured memory items from conversations. Respond with ONLY a JSON array.',
|
|
581
|
+
permissionMode: 'default',
|
|
582
|
+
cwd: process.cwd(),
|
|
583
|
+
maxTurns: 1,
|
|
584
|
+
});
|
|
585
|
+
return extractLoop.run(prompt);
|
|
586
|
+
};
|
|
587
|
+
const pipeline = new ExtractionPipeline(extractFn);
|
|
588
|
+
const result = await pipeline.extract(messages, categoryNames);
|
|
589
|
+
if (result.errors.length > 0) {
|
|
590
|
+
log.debug({ errors: result.errors, jobId: taskContext.jobId }, 'Extraction had errors');
|
|
591
|
+
}
|
|
592
|
+
if (result.items.length === 0) {
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
// Deduplicate against existing items
|
|
596
|
+
const existingItems = await this._memoryManager.structuredMemory.listItems();
|
|
597
|
+
const uniqueItems = pipeline.deduplicateItems(result.items, existingItems);
|
|
598
|
+
// Persist each new item
|
|
599
|
+
let savedCount = 0;
|
|
600
|
+
for (const item of uniqueItems) {
|
|
601
|
+
try {
|
|
602
|
+
await this._memoryManager.structuredMemory.createItem({
|
|
603
|
+
type: item.type,
|
|
604
|
+
summary: item.summary,
|
|
605
|
+
source: item.source || taskContext.jobId,
|
|
606
|
+
source_type: item.source_type,
|
|
607
|
+
tags: item.tags,
|
|
608
|
+
category: item.category,
|
|
609
|
+
});
|
|
610
|
+
savedCount++;
|
|
611
|
+
}
|
|
612
|
+
catch (err) {
|
|
613
|
+
log.debug({ err, item: item.summary }, 'Failed to save extracted memory item');
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
// Append daily note summarizing extraction
|
|
617
|
+
if (savedCount > 0) {
|
|
618
|
+
await this._memoryManager.appendDailyNote(`Extracted ${savedCount} memory item(s) from job ${taskContext.jobId}`);
|
|
619
|
+
}
|
|
620
|
+
log.info({ jobId: taskContext.jobId, extracted: result.items.length, saved: savedCount }, 'Memory extraction complete');
|
|
621
|
+
}
|
|
335
622
|
/**
|
|
336
623
|
* Creates custom tools available to the agent during execution.
|
|
624
|
+
* Includes: permission tools, memory tools (search/save/forget), recall_context.
|
|
337
625
|
*/
|
|
338
626
|
_createCustomTools() {
|
|
339
|
-
|
|
627
|
+
const permissionTools = [
|
|
340
628
|
{
|
|
341
629
|
name: 'check_permissions',
|
|
342
630
|
description: 'Check if you have access to specific paths or commands before executing. Use this during planning to verify your boundaries.',
|
|
@@ -383,9 +671,6 @@ export class Orchestrator {
|
|
|
383
671
|
}
|
|
384
672
|
}
|
|
385
673
|
}
|
|
386
|
-
// The actual approval flow happens outside (CLI prompt, dashboard, etc.)
|
|
387
|
-
// For now, return the request details so the caller can present them to the user.
|
|
388
|
-
// In the CLI, this will be intercepted by the onEvent callback.
|
|
389
674
|
return {
|
|
390
675
|
granted: false,
|
|
391
676
|
pending: true,
|
|
@@ -395,6 +680,33 @@ export class Orchestrator {
|
|
|
395
680
|
},
|
|
396
681
|
},
|
|
397
682
|
];
|
|
683
|
+
// Wire existing memory tools (memory_search, memory_save, memory_forget)
|
|
684
|
+
const memoryTools = createMemoryTools(this._memoryManager, this._validationPipeline);
|
|
685
|
+
// Add recall_context tool for daily notes retrieval
|
|
686
|
+
const recallContextTool = {
|
|
687
|
+
name: 'recall_context',
|
|
688
|
+
description: 'Retrieve recent daily notes (rolling conversation summaries). ' +
|
|
689
|
+
'Use this to get context from the past few days of agent activity.',
|
|
690
|
+
input_schema: {
|
|
691
|
+
type: 'object',
|
|
692
|
+
properties: {
|
|
693
|
+
days: {
|
|
694
|
+
type: 'number',
|
|
695
|
+
description: 'Number of recent days to retrieve (default: 3, max: 14).',
|
|
696
|
+
default: 3,
|
|
697
|
+
},
|
|
698
|
+
},
|
|
699
|
+
},
|
|
700
|
+
handler: async (input) => {
|
|
701
|
+
const days = Math.min(Math.max(input.days ?? 3, 1), 14);
|
|
702
|
+
const notes = await this._memoryManager.recallDailyNotes(days);
|
|
703
|
+
if (notes.length === 0) {
|
|
704
|
+
return { notes: [], message: 'No daily notes found for the requested period.' };
|
|
705
|
+
}
|
|
706
|
+
return { notes, count: notes.length, days };
|
|
707
|
+
},
|
|
708
|
+
};
|
|
709
|
+
return [...permissionTools, ...memoryTools, recallContextTool];
|
|
398
710
|
}
|
|
399
711
|
/**
|
|
400
712
|
* Parse interval strings like "30m", "1h" to minutes.
|
|
@@ -447,6 +759,18 @@ export class Orchestrator {
|
|
|
447
759
|
this._assertBooted();
|
|
448
760
|
return this._policyEngine;
|
|
449
761
|
}
|
|
762
|
+
/** ORCH-12: Access the hook runner for registering lifecycle hooks */
|
|
763
|
+
get hookRunner() {
|
|
764
|
+
return this._hookRunner;
|
|
765
|
+
}
|
|
766
|
+
/** ORCH-14: Set a custom context transform function */
|
|
767
|
+
set transformContext(fn) {
|
|
768
|
+
this._transformContext = fn;
|
|
769
|
+
}
|
|
770
|
+
/** ORCH-14: Get the current context transform function */
|
|
771
|
+
get transformContext() {
|
|
772
|
+
return this._transformContext;
|
|
773
|
+
}
|
|
450
774
|
get config() {
|
|
451
775
|
return this._config;
|
|
452
776
|
}
|