crewly 1.8.7 → 1.8.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backend/backend/src/constants.d.ts +12 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +12 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/browser/browser.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/browser/browser.controller.js +17 -0
- package/dist/backend/backend/src/controllers/browser/browser.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.js +8 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +15 -7
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-bridge.service.d.ts +7 -0
- package/dist/backend/backend/src/services/browser/browser-bridge.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-bridge.service.js +69 -12
- package/dist/backend/backend/src/services/browser/browser-bridge.service.js.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-proxy.service.d.ts +122 -1
- package/dist/backend/backend/src/services/browser/browser-proxy.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-proxy.service.js +252 -17
- package/dist/backend/backend/src/services/browser/browser-proxy.service.js.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-relay-adapter.service.d.ts +37 -3
- package/dist/backend/backend/src/services/browser/browser-relay-adapter.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-relay-adapter.service.js +140 -23
- package/dist/backend/backend/src/services/browser/browser-relay-adapter.service.js.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-client.service.d.ts +75 -0
- package/dist/backend/backend/src/services/cloud/cloud-client.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-client.service.js +164 -12
- package/dist/backend/backend/src/services/cloud/cloud-client.service.js.map +1 -1
- package/dist/cli/backend/src/constants.d.ts +12 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +12 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/cli/src/index.js +0 -0
- package/package.json +1 -1
- package/config/constants.d.ts.map +0 -1
- package/config/index.d.ts.map +0 -1
- package/dist/backend/backend/src/controllers/task-management/task-management.controller.d.ts +0 -169
- package/dist/backend/backend/src/controllers/task-management/task-management.controller.d.ts.map +0 -1
- package/dist/backend/backend/src/controllers/task-management/task-management.controller.js +0 -1779
- package/dist/backend/backend/src/controllers/task-management/task-management.controller.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/agent-runner.service.d.ts +0 -513
- package/dist/backend/backend/src/services/agent/crewly-agent/agent-runner.service.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/agent-runner.service.js +0 -1568
- package/dist/backend/backend/src/services/agent/crewly-agent/agent-runner.service.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/agent-worker.d.ts +0 -86
- package/dist/backend/backend/src/services/agent/crewly-agent/agent-worker.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/agent-worker.js +0 -147
- package/dist/backend/backend/src/services/agent/crewly-agent/agent-worker.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/api-client.d.ts +0 -68
- package/dist/backend/backend/src/services/agent/crewly-agent/api-client.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/api-client.js +0 -131
- package/dist/backend/backend/src/services/agent/crewly-agent/api-client.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/audit-log.service.d.ts +0 -130
- package/dist/backend/backend/src/services/agent/crewly-agent/audit-log.service.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/audit-log.service.js +0 -263
- package/dist/backend/backend/src/services/agent/crewly-agent/audit-log.service.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/audit-trail.service.d.ts +0 -74
- package/dist/backend/backend/src/services/agent/crewly-agent/audit-trail.service.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/audit-trail.service.js +0 -140
- package/dist/backend/backend/src/services/agent/crewly-agent/audit-trail.service.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/auditor-tools.d.ts +0 -29
- package/dist/backend/backend/src/services/agent/crewly-agent/auditor-tools.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/auditor-tools.js +0 -279
- package/dist/backend/backend/src/services/agent/crewly-agent/auditor-tools.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-runtime.service.d.ts +0 -340
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-runtime.service.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-runtime.service.js +0 -1176
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-runtime.service.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/deepseek-sse-transform.d.ts +0 -79
- package/dist/backend/backend/src/services/agent/crewly-agent/deepseek-sse-transform.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/deepseek-sse-transform.js +0 -145
- package/dist/backend/backend/src/services/agent/crewly-agent/deepseek-sse-transform.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/env-isolation.service.d.ts +0 -79
- package/dist/backend/backend/src/services/agent/crewly-agent/env-isolation.service.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/env-isolation.service.js +0 -218
- package/dist/backend/backend/src/services/agent/crewly-agent/env-isolation.service.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/index.d.ts +0 -16
- package/dist/backend/backend/src/services/agent/crewly-agent/index.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/index.js +0 -16
- package/dist/backend/backend/src/services/agent/crewly-agent/index.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/mcp-tool-bridge.d.ts +0 -135
- package/dist/backend/backend/src/services/agent/crewly-agent/mcp-tool-bridge.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/mcp-tool-bridge.js +0 -185
- package/dist/backend/backend/src/services/agent/crewly-agent/mcp-tool-bridge.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/model-manager.d.ts +0 -141
- package/dist/backend/backend/src/services/agent/crewly-agent/model-manager.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/model-manager.js +0 -310
- package/dist/backend/backend/src/services/agent/crewly-agent/model-manager.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/output-filter.service.d.ts +0 -91
- package/dist/backend/backend/src/services/agent/crewly-agent/output-filter.service.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/output-filter.service.js +0 -143
- package/dist/backend/backend/src/services/agent/crewly-agent/output-filter.service.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/prompt-guard.service.d.ts +0 -103
- package/dist/backend/backend/src/services/agent/crewly-agent/prompt-guard.service.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/prompt-guard.service.js +0 -256
- package/dist/backend/backend/src/services/agent/crewly-agent/prompt-guard.service.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/rate-limiter.d.ts +0 -143
- package/dist/backend/backend/src/services/agent/crewly-agent/rate-limiter.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/rate-limiter.js +0 -264
- package/dist/backend/backend/src/services/agent/crewly-agent/rate-limiter.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/smoke-test.d.ts +0 -13
- package/dist/backend/backend/src/services/agent/crewly-agent/smoke-test.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/smoke-test.js +0 -91
- package/dist/backend/backend/src/services/agent/crewly-agent/smoke-test.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/tool-registry.d.ts +0 -135
- package/dist/backend/backend/src/services/agent/crewly-agent/tool-registry.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/tool-registry.js +0 -1937
- package/dist/backend/backend/src/services/agent/crewly-agent/tool-registry.js.map +0 -1
- package/dist/backend/backend/src/services/autonomous/auto-assign.service.d.ts +0 -429
- package/dist/backend/backend/src/services/autonomous/auto-assign.service.d.ts.map +0 -1
- package/dist/backend/backend/src/services/autonomous/auto-assign.service.js +0 -852
- package/dist/backend/backend/src/services/autonomous/auto-assign.service.js.map +0 -1
- package/dist/backend/backend/src/services/project/task-tracking.service.d.ts +0 -171
- package/dist/backend/backend/src/services/project/task-tracking.service.d.ts.map +0 -1
- package/dist/backend/backend/src/services/project/task-tracking.service.js +0 -725
- package/dist/backend/backend/src/services/project/task-tracking.service.js.map +0 -1
- package/dist/backend/backend/src/services/v3/project-task-watcher.service.d.ts +0 -118
- package/dist/backend/backend/src/services/v3/project-task-watcher.service.d.ts.map +0 -1
- package/dist/backend/backend/src/services/v3/project-task-watcher.service.js +0 -326
- package/dist/backend/backend/src/services/v3/project-task-watcher.service.js.map +0 -1
- package/dist/backend/backend/src/services/wiki/wiki-chat-subscriber.service.d.ts +0 -74
- package/dist/backend/backend/src/services/wiki/wiki-chat-subscriber.service.d.ts.map +0 -1
- package/dist/backend/backend/src/services/wiki/wiki-chat-subscriber.service.js +0 -154
- package/dist/backend/backend/src/services/wiki/wiki-chat-subscriber.service.js.map +0 -1
- package/dist/backend/backend/src/types/auto-assign.types.d.ts +0 -271
- package/dist/backend/backend/src/types/auto-assign.types.d.ts.map +0 -1
- package/dist/backend/backend/src/types/auto-assign.types.js +0 -136
- package/dist/backend/backend/src/types/auto-assign.types.js.map +0 -1
- package/dist/backend/backend/src/utils/esm-require.utils.d.ts +0 -111
- package/dist/backend/backend/src/utils/esm-require.utils.d.ts.map +0 -1
- package/dist/backend/backend/src/utils/esm-require.utils.js +0 -124
- package/dist/backend/backend/src/utils/esm-require.utils.js.map +0 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/prompt-module.interface.d.ts +0 -220
- package/dist/cli/backend/src/services/ai/prompt-modules/prompt-module.interface.d.ts.map +0 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/prompt-module.interface.js +0 -37
- package/dist/cli/backend/src/services/ai/prompt-modules/prompt-module.interface.js.map +0 -1
- package/dist/cli/backend/src/services/knowledge/fts5-search-strategy.d.ts +0 -56
- package/dist/cli/backend/src/services/knowledge/fts5-search-strategy.d.ts.map +0 -1
- package/dist/cli/backend/src/services/knowledge/fts5-search-strategy.js +0 -91
- package/dist/cli/backend/src/services/knowledge/fts5-search-strategy.js.map +0 -1
- package/dist/cli/backend/src/services/knowledge/learnings-index.service.d.ts +0 -159
- package/dist/cli/backend/src/services/knowledge/learnings-index.service.d.ts.map +0 -1
- package/dist/cli/backend/src/services/knowledge/learnings-index.service.js +0 -304
- package/dist/cli/backend/src/services/knowledge/learnings-index.service.js.map +0 -1
- package/dist/cli/backend/src/services/knowledge/wiki-compiler.service.d.ts +0 -115
- package/dist/cli/backend/src/services/knowledge/wiki-compiler.service.d.ts.map +0 -1
- package/dist/cli/backend/src/services/knowledge/wiki-compiler.service.js +0 -215
- package/dist/cli/backend/src/services/knowledge/wiki-compiler.service.js.map +0 -1
- package/dist/cli/backend/src/services/memory/embedding-provider.d.ts +0 -78
- package/dist/cli/backend/src/services/memory/embedding-provider.d.ts.map +0 -1
- package/dist/cli/backend/src/services/memory/embedding-provider.js +0 -179
- package/dist/cli/backend/src/services/memory/embedding-provider.js.map +0 -1
- package/dist/cli/backend/src/services/memory/vector-store.service.d.ts +0 -331
- package/dist/cli/backend/src/services/memory/vector-store.service.d.ts.map +0 -1
- package/dist/cli/backend/src/services/memory/vector-store.service.js +0 -814
- package/dist/cli/backend/src/services/memory/vector-store.service.js.map +0 -1
- package/dist/cli/backend/src/services/project/task-tracking.service.d.ts +0 -171
- package/dist/cli/backend/src/services/project/task-tracking.service.d.ts.map +0 -1
- package/dist/cli/backend/src/services/project/task-tracking.service.js +0 -725
- package/dist/cli/backend/src/services/project/task-tracking.service.js.map +0 -1
- package/dist/cli/backend/src/types/auto-assign.types.d.ts +0 -271
- package/dist/cli/backend/src/types/auto-assign.types.d.ts.map +0 -1
- package/dist/cli/backend/src/types/auto-assign.types.js +0 -136
- package/dist/cli/backend/src/types/auto-assign.types.js.map +0 -1
package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-runtime.service.js
DELETED
|
@@ -1,1176 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Crewly Agent Runtime Service
|
|
3
|
-
*
|
|
4
|
-
* Concrete RuntimeAgentService subclass for the Crewly Agent.
|
|
5
|
-
* Supports two execution modes:
|
|
6
|
-
* - **In-process** (default): AgentRunner runs in the main Node.js process
|
|
7
|
-
* - **Worker process**: AgentRunner runs in a child process via fork(),
|
|
8
|
-
* enabling hot-reload and crash isolation
|
|
9
|
-
*
|
|
10
|
-
* @module services/agent/crewly-agent/crewly-agent-runtime.service
|
|
11
|
-
*/
|
|
12
|
-
import { promises as fs } from 'fs';
|
|
13
|
-
import * as path from 'path';
|
|
14
|
-
import { fork } from 'child_process';
|
|
15
|
-
// fileURLToPath used for ESM __dirname equivalent — lazy-loaded to avoid CJS issues
|
|
16
|
-
import { RuntimeAgentService } from '../runtime-agent.service.abstract.js';
|
|
17
|
-
import { AgentRunnerService } from './agent-runner.service.js';
|
|
18
|
-
import { RUNTIME_TYPES, CREWLY_CONSTANTS, ADDON_CONSTANTS } from '../../../constants.js';
|
|
19
|
-
import { homedir } from 'os';
|
|
20
|
-
import { CREWLY_AGENT_DEFAULTS } from './types.js';
|
|
21
|
-
import { InProcessLogBuffer } from './in-process-log-buffer.js';
|
|
22
|
-
import { RateLimiter } from './rate-limiter.js';
|
|
23
|
-
import { updateAgentHeartbeat } from '../agent-heartbeat.service.js';
|
|
24
|
-
import { PtyActivityTrackerService } from '../pty-activity-tracker.service.js';
|
|
25
|
-
import { TokenUsageService } from '../../monitoring/token-usage.service.js';
|
|
26
|
-
import { getSettingsService } from '../../settings/settings.service.js';
|
|
27
|
-
import { AgentStreamService } from './agent-stream.service.js';
|
|
28
|
-
import { getOnboardingBootstrapService } from '../../orchestrator/onboarding-bootstrap.service.js';
|
|
29
|
-
import { loadOnboardingPrompt } from '../../orchestrator/onboarding-mode-loader.js';
|
|
30
|
-
import { getStatePersistenceService } from '../../orchestrator/state-persistence.service.js';
|
|
31
|
-
/**
|
|
32
|
-
* Crewly Agent runtime with optional worker process isolation.
|
|
33
|
-
*
|
|
34
|
-
* Supports two modes:
|
|
35
|
-
* - **In-process** (default): AgentRunner runs directly in the main process
|
|
36
|
-
* - **Worker process** (`useWorkerProcess: true`): AgentRunner runs in a
|
|
37
|
-
* forked child process, enabling hot-reload and crash isolation
|
|
38
|
-
*
|
|
39
|
-
* @example
|
|
40
|
-
* ```typescript
|
|
41
|
-
* // In-process mode (default)
|
|
42
|
-
* const runtime = new CrewlyAgentRuntimeService(sessionHelper, projectRoot);
|
|
43
|
-
* await runtime.initializeInProcess('crewly-orc');
|
|
44
|
-
*
|
|
45
|
-
* // Worker process mode
|
|
46
|
-
* const runtime = new CrewlyAgentRuntimeService(sessionHelper, projectRoot);
|
|
47
|
-
* await runtime.initializeInProcess('crewly-orc', { useWorkerProcess: true });
|
|
48
|
-
*
|
|
49
|
-
* // Hot-reload: restart worker with fresh code, preserving session
|
|
50
|
-
* await runtime.hotReload();
|
|
51
|
-
* ```
|
|
52
|
-
*/
|
|
53
|
-
export class CrewlyAgentRuntimeService extends RuntimeAgentService {
|
|
54
|
-
agentRunner = null;
|
|
55
|
-
initialized = false;
|
|
56
|
-
currentSessionName = null;
|
|
57
|
-
currentMemberId;
|
|
58
|
-
currentModelString = 'unknown';
|
|
59
|
-
logBuffer;
|
|
60
|
-
rateLimiter;
|
|
61
|
-
heartbeatTimer = null;
|
|
62
|
-
/** AbortController for the currently executing message — enables external abort */
|
|
63
|
-
messageAbortController = null;
|
|
64
|
-
// ===== Worker process fields =====
|
|
65
|
-
/** Whether this instance uses a worker process instead of in-process execution */
|
|
66
|
-
useWorkerProcess = false;
|
|
67
|
-
/** The forked worker child process */
|
|
68
|
-
workerProcess = null;
|
|
69
|
-
/** Stored config for hot-reload — needed to re-init the worker */
|
|
70
|
-
storedConfig = null;
|
|
71
|
-
/** Pending promise resolver for the current worker run */
|
|
72
|
-
workerRunResolve = null;
|
|
73
|
-
/** Pending promise rejector for the current worker run */
|
|
74
|
-
workerRunReject = null;
|
|
75
|
-
/** Whether the worker is currently processing a message */
|
|
76
|
-
workerProcessing = false;
|
|
77
|
-
constructor(sessionHelper, projectRoot) {
|
|
78
|
-
super(sessionHelper, projectRoot);
|
|
79
|
-
this.logBuffer = InProcessLogBuffer.getInstance();
|
|
80
|
-
this.rateLimiter = new RateLimiter();
|
|
81
|
-
}
|
|
82
|
-
// ===== Abstract method implementations =====
|
|
83
|
-
/**
|
|
84
|
-
* Get the runtime type identifier.
|
|
85
|
-
*
|
|
86
|
-
* @returns 'crewly-agent' runtime type constant
|
|
87
|
-
*/
|
|
88
|
-
getRuntimeType() {
|
|
89
|
-
return RUNTIME_TYPES.CREWLY_AGENT;
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Detect if the Crewly Agent runtime is running.
|
|
93
|
-
* For in-process runtime, this checks if the AgentRunner is initialized.
|
|
94
|
-
*
|
|
95
|
-
* @param _sessionName - Session name (unused for in-process runtime)
|
|
96
|
-
* @returns True if the agent runner is initialized
|
|
97
|
-
*/
|
|
98
|
-
async detectRuntimeSpecific(_sessionName) {
|
|
99
|
-
if (this.useWorkerProcess) {
|
|
100
|
-
return this.initialized && this.workerProcess !== null && this.workerProcess.connected;
|
|
101
|
-
}
|
|
102
|
-
return this.initialized && this.agentRunner !== null && this.agentRunner.isInitialized();
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Get patterns that indicate the runtime is ready.
|
|
106
|
-
* For in-process runtime, there are no terminal patterns — readiness is checked programmatically.
|
|
107
|
-
*
|
|
108
|
-
* @returns Empty array (no terminal output to match)
|
|
109
|
-
*/
|
|
110
|
-
getRuntimeReadyPatterns() {
|
|
111
|
-
return ['Crewly Agent Ready'];
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Get patterns that indicate runtime errors.
|
|
115
|
-
*
|
|
116
|
-
* @returns Empty array (errors are thrown as exceptions, not terminal patterns)
|
|
117
|
-
*/
|
|
118
|
-
getRuntimeErrorPatterns() {
|
|
119
|
-
return [];
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Get patterns that indicate the runtime has exited.
|
|
123
|
-
*
|
|
124
|
-
* @returns Empty array (in-process runtime doesn't exit via terminal)
|
|
125
|
-
*/
|
|
126
|
-
getRuntimeExitPatterns() {
|
|
127
|
-
return [];
|
|
128
|
-
}
|
|
129
|
-
// ===== In-process lifecycle methods =====
|
|
130
|
-
/**
|
|
131
|
-
* Initialize the agent runtime.
|
|
132
|
-
*
|
|
133
|
-
* Loads the system prompt from config/roles/orchestrator/prompt.md,
|
|
134
|
-
* creates the AgentRunnerService (in-process or worker), and initializes the model.
|
|
135
|
-
*
|
|
136
|
-
* Onboarding v3 (B3): when the role is `'orchestrator'` and the cold-start
|
|
137
|
-
* detector ({@link OnboardingBootstrapService}) reports a fresh OSS install,
|
|
138
|
-
* the runtime swaps the role prompt for the onboarding-mode prompt
|
|
139
|
-
* ({@link loadOnboardingPrompt}) and flips persisted state to
|
|
140
|
-
* `mode: 'onboarding'`. The state-persistence checkpoint is saved before
|
|
141
|
-
* the AgentRunner initialises so a crash mid-init still resumes correctly.
|
|
142
|
-
*
|
|
143
|
-
* @param sessionName - Session name for this agent instance
|
|
144
|
-
* @param config - Optional partial config overrides. Set `useWorkerProcess: true` to run in a child process.
|
|
145
|
-
* @param roleName - Role name for system prompt lookup (default: 'orchestrator')
|
|
146
|
-
*/
|
|
147
|
-
async initializeInProcess(sessionName, config, roleName) {
|
|
148
|
-
this.currentSessionName = sessionName;
|
|
149
|
-
this.currentMemberId = config?.memberId;
|
|
150
|
-
this.useWorkerProcess = config?.useWorkerProcess ?? false;
|
|
151
|
-
const effectiveRoleName = roleName || 'orchestrator';
|
|
152
|
-
// Onboarding v3 (B3) — cold-start detection. Only the orc bootstrap
|
|
153
|
-
// path participates; subordinate agents always use their role prompt.
|
|
154
|
-
const onboardingMode = await this.detectOnboardingMode(effectiveRoleName);
|
|
155
|
-
// Build enhanced system prompt with skills and addon awareness, OR
|
|
156
|
-
// swap to the onboarding-mode prompt when cold-start fired.
|
|
157
|
-
const systemPrompt = onboardingMode
|
|
158
|
-
? await loadOnboardingPrompt()
|
|
159
|
-
: await this.buildEnhancedSystemPrompt(effectiveRoleName);
|
|
160
|
-
// Build full config with defaults
|
|
161
|
-
const fullConfig = {
|
|
162
|
-
model: config?.model || CREWLY_AGENT_DEFAULTS.DEFAULT_MODEL,
|
|
163
|
-
maxSteps: config?.maxSteps || CREWLY_AGENT_DEFAULTS.MAX_STEPS,
|
|
164
|
-
sessionName,
|
|
165
|
-
apiBaseUrl: config?.apiBaseUrl || CREWLY_AGENT_DEFAULTS.API_BASE_URL,
|
|
166
|
-
systemPrompt: config?.systemPrompt || systemPrompt,
|
|
167
|
-
maxHistoryMessages: config?.maxHistoryMessages || CREWLY_AGENT_DEFAULTS.MAX_HISTORY_MESSAGES,
|
|
168
|
-
compactionThreshold: config?.compactionThreshold || CREWLY_AGENT_DEFAULTS.COMPACTION_THRESHOLD,
|
|
169
|
-
projectPath: config?.projectPath,
|
|
170
|
-
};
|
|
171
|
-
// Store config for hot-reload
|
|
172
|
-
this.storedConfig = fullConfig;
|
|
173
|
-
if (this.useWorkerProcess) {
|
|
174
|
-
await this.initializeWorker(fullConfig);
|
|
175
|
-
}
|
|
176
|
-
else {
|
|
177
|
-
this.agentRunner = new AgentRunnerService(fullConfig);
|
|
178
|
-
try {
|
|
179
|
-
await this.agentRunner.initialize();
|
|
180
|
-
}
|
|
181
|
-
catch (error) {
|
|
182
|
-
// Clean up on initialization failure to prevent partial state
|
|
183
|
-
this.agentRunner = null;
|
|
184
|
-
this.currentSessionName = null;
|
|
185
|
-
throw error;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
this.initialized = true;
|
|
189
|
-
this.currentModelString = `${fullConfig.model.provider}/${fullConfig.model.modelId}`;
|
|
190
|
-
// Start periodic heartbeat to keep in-process agent marked active
|
|
191
|
-
this.startHeartbeat(sessionName);
|
|
192
|
-
// Register in-process session for frontend terminal visibility
|
|
193
|
-
this.logBuffer.registerSession(sessionName);
|
|
194
|
-
const mode = this.useWorkerProcess ? 'worker' : 'in-process';
|
|
195
|
-
this.logBuffer.append(sessionName, 'info', `Crewly Agent initialized [${mode}] (${this.currentModelString})`);
|
|
196
|
-
this.logger.info('Crewly Agent runtime initialized', {
|
|
197
|
-
sessionName,
|
|
198
|
-
mode,
|
|
199
|
-
model: `${fullConfig.model.provider}/${fullConfig.model.modelId}`,
|
|
200
|
-
maxSteps: fullConfig.maxSteps,
|
|
201
|
-
onboardingMode,
|
|
202
|
-
});
|
|
203
|
-
// Onboarding v3 (B3) — best-effort first-launch marker. Runs after
|
|
204
|
-
// bootstrap success regardless of mode; idempotent on repeated boots.
|
|
205
|
-
// Errors are swallowed by `markProjectAsLaunched` so a marker write
|
|
206
|
-
// hiccup never fails the demo path.
|
|
207
|
-
if (effectiveRoleName === 'orchestrator' && config?.projectPath) {
|
|
208
|
-
await this.markFirstLaunchForCwd(config.projectPath);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
/**
|
|
212
|
-
* Decide whether this orc bootstrap should enter onboarding mode.
|
|
213
|
-
*
|
|
214
|
-
* Returns false (no-op) for non-orchestrator roles, when the bootstrap
|
|
215
|
-
* service hasn't been wired yet, or when detection throws — fail-closed
|
|
216
|
-
* so transient errors never falsely trigger onboarding.
|
|
217
|
-
*
|
|
218
|
-
* Side effects: when true, flips the persisted orchestrator mode to
|
|
219
|
-
* `'onboarding'` and saves a checkpoint immediately, so a crash before
|
|
220
|
-
* the AgentRunner finishes initialising still resumes in onboarding mode.
|
|
221
|
-
*
|
|
222
|
-
* @param roleName - The resolved role name for this bootstrap
|
|
223
|
-
* @returns True iff cold-start fired and the prompt should be swapped
|
|
224
|
-
*/
|
|
225
|
-
async detectOnboardingMode(roleName) {
|
|
226
|
-
if (roleName !== 'orchestrator') {
|
|
227
|
-
return false;
|
|
228
|
-
}
|
|
229
|
-
const detector = getOnboardingBootstrapService();
|
|
230
|
-
if (!detector) {
|
|
231
|
-
this.logger.debug('OnboardingBootstrapService not wired; skipping cold-start probe');
|
|
232
|
-
return false;
|
|
233
|
-
}
|
|
234
|
-
let isCold;
|
|
235
|
-
try {
|
|
236
|
-
isCold = await detector.shouldEnterOnboardingMode();
|
|
237
|
-
}
|
|
238
|
-
catch (err) {
|
|
239
|
-
this.logger.warn('Cold-start probe threw; defaulting to normal mode', {
|
|
240
|
-
error: err instanceof Error ? err.message : String(err),
|
|
241
|
-
});
|
|
242
|
-
return false;
|
|
243
|
-
}
|
|
244
|
-
if (!isCold) {
|
|
245
|
-
return false;
|
|
246
|
-
}
|
|
247
|
-
// Flip persisted state immediately so a crash between here and
|
|
248
|
-
// AgentRunner.initialize() still resumes in onboarding mode (B4).
|
|
249
|
-
try {
|
|
250
|
-
const stateService = getStatePersistenceService();
|
|
251
|
-
if (stateService.isInitialized()) {
|
|
252
|
-
stateService.setMode('onboarding');
|
|
253
|
-
await stateService.saveState('user_request');
|
|
254
|
-
}
|
|
255
|
-
else {
|
|
256
|
-
this.logger.debug('State-persistence not initialized; mode flip deferred to caller');
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
catch (err) {
|
|
260
|
-
// Don't abort the demo just because the checkpoint write failed.
|
|
261
|
-
this.logger.warn('Failed to persist onboarding mode flip; in-memory mode still applied', {
|
|
262
|
-
error: err instanceof Error ? err.message : String(err),
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
return true;
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* Best-effort `firstLaunchedAt` marker write for the project at
|
|
269
|
-
* `projectPath`.
|
|
270
|
-
*
|
|
271
|
-
* Resolves the project record by matching `Project.path` against the
|
|
272
|
-
* given path, then delegates to `OnboardingBootstrapService.markProjectAsLaunched`.
|
|
273
|
-
* No-op if the bootstrap service isn't wired or if no project record
|
|
274
|
-
* matches (fresh installs without an explicit project bind never reach
|
|
275
|
-
* this branch — the orc bootstrap doesn't pass `projectPath` then).
|
|
276
|
-
*
|
|
277
|
-
* @param projectPath - Absolute filesystem path of the bound project
|
|
278
|
-
*/
|
|
279
|
-
async markFirstLaunchForCwd(projectPath) {
|
|
280
|
-
const detector = getOnboardingBootstrapService();
|
|
281
|
-
if (!detector)
|
|
282
|
-
return;
|
|
283
|
-
// The detector's storage probe already resolves projects; reuse it
|
|
284
|
-
// here to avoid an extra import. Marker resolution is best-effort —
|
|
285
|
-
// any throw is logged at warn and swallowed.
|
|
286
|
-
try {
|
|
287
|
-
// Late-bind the storage probe via a fresh getProjects() call. The
|
|
288
|
-
// bootstrap service's internal storage was injected at wire time;
|
|
289
|
-
// this method is intentionally narrow (no extra coupling).
|
|
290
|
-
const { StorageService } = await import('../../core/storage.service.js');
|
|
291
|
-
const projects = await StorageService.getInstance().getProjects();
|
|
292
|
-
const target = projects.find((p) => p.path === projectPath);
|
|
293
|
-
if (target) {
|
|
294
|
-
await detector.markProjectAsLaunched(target.id);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
catch (err) {
|
|
298
|
-
this.logger.warn('First-launch marker write skipped', {
|
|
299
|
-
projectPath,
|
|
300
|
-
error: err instanceof Error ? err.message : String(err),
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
/**
|
|
305
|
-
* Handle an incoming message by routing it to the AgentRunner.
|
|
306
|
-
*
|
|
307
|
-
* This is the primary entry point for message delivery, replacing
|
|
308
|
-
* the PTY write path used by other runtimes.
|
|
309
|
-
*
|
|
310
|
-
* @param message - The message to process
|
|
311
|
-
* @param metadata - Optional metadata (e.g. Slack channelId, threadTs)
|
|
312
|
-
* @returns Agent run result with text response and tool call records
|
|
313
|
-
* @throws Error if the runtime is not initialized
|
|
314
|
-
*/
|
|
315
|
-
async handleMessage(message, metadata) {
|
|
316
|
-
if (!this.initialized) {
|
|
317
|
-
throw new Error('Crewly Agent runtime not initialized. Call initializeInProcess() first.');
|
|
318
|
-
}
|
|
319
|
-
if (!this.useWorkerProcess && !this.agentRunner) {
|
|
320
|
-
throw new Error('Crewly Agent runtime not initialized. Call initializeInProcess() first.');
|
|
321
|
-
}
|
|
322
|
-
if (this.useWorkerProcess && (!this.workerProcess || !this.workerProcess.connected)) {
|
|
323
|
-
throw new Error('Worker process not available. Call initializeInProcess() or hotReload() first.');
|
|
324
|
-
}
|
|
325
|
-
const session = this.currentSessionName;
|
|
326
|
-
// Extract conversationId from [CHAT:xxx] or [GCHAT:xxx ...] prefix if present
|
|
327
|
-
let conversationId;
|
|
328
|
-
let cleanMessage = message;
|
|
329
|
-
const chatPrefixMatch = message.match(/^\[(?:G?CHAT):([^\]\s]+)[^\]]*\]\s*/);
|
|
330
|
-
if (chatPrefixMatch) {
|
|
331
|
-
conversationId = chatPrefixMatch[1];
|
|
332
|
-
cleanMessage = message.slice(chatPrefixMatch[0].length);
|
|
333
|
-
this.logger.debug('Extracted conversationId from message prefix', {
|
|
334
|
-
sessionName: session,
|
|
335
|
-
conversationId,
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
const queueLen = this.rateLimiter.getQueueLength();
|
|
339
|
-
const msgPreview = cleanMessage.length <= 120
|
|
340
|
-
? `"${cleanMessage}"`
|
|
341
|
-
: `"${cleanMessage.substring(0, 50)}...${cleanMessage.substring(cleanMessage.length - 50)}"`;
|
|
342
|
-
this.logBuffer.append(session, 'info', `← Message received (${cleanMessage.length} chars${conversationId ? `, conv:${conversationId}` : ''}${queueLen > 0 ? `, queue:${queueLen}` : ''}): ${msgPreview}`);
|
|
343
|
-
if (!this.useWorkerProcess) {
|
|
344
|
-
this.logger.debug('Handling message via rate limiter', {
|
|
345
|
-
sessionName: session,
|
|
346
|
-
messageLength: cleanMessage.length,
|
|
347
|
-
historyLength: this.agentRunner.getHistoryLength(),
|
|
348
|
-
conversationId,
|
|
349
|
-
queueLength: queueLen,
|
|
350
|
-
requestsInWindow: this.rateLimiter.getRequestCountInWindow(),
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
// Route through rate limiter for throttling, coalescing, and 429 retry
|
|
354
|
-
const result = await this.rateLimiter.enqueue(cleanMessage, metadata, async (msg, meta) => {
|
|
355
|
-
if (this.useWorkerProcess) {
|
|
356
|
-
return this.executeMessageViaWorker(session, msg, conversationId, meta);
|
|
357
|
-
}
|
|
358
|
-
return this.executeMessage(session, msg, conversationId, meta);
|
|
359
|
-
});
|
|
360
|
-
return result;
|
|
361
|
-
}
|
|
362
|
-
/**
|
|
363
|
-
* Execute a single message against the AgentRunner.
|
|
364
|
-
*
|
|
365
|
-
* Separated from handleMessage to allow the rate limiter to wrap this
|
|
366
|
-
* with throttling, coalescing, and 429 retry logic.
|
|
367
|
-
*
|
|
368
|
-
* @param session - Session name
|
|
369
|
-
* @param cleanMessage - Message content (prefix already stripped)
|
|
370
|
-
* @param conversationId - Optional conversation ID
|
|
371
|
-
* @param metadata - Optional metadata
|
|
372
|
-
* @returns Agent run result
|
|
373
|
-
*/
|
|
374
|
-
async executeMessage(session, cleanMessage, conversationId, metadata) {
|
|
375
|
-
const SOFT_WARNING_MS = CREWLY_AGENT_DEFAULTS.MESSAGE_SOFT_WARNING_MS;
|
|
376
|
-
// I4 — per-model timeout: lookup the active model's modelId in
|
|
377
|
-
// MODEL_TIMEOUT_MS first; fall back to MESSAGE_TIMEOUT_MS default.
|
|
378
|
-
// Models like deepseek-reasoner need a longer ceiling than the 5min default
|
|
379
|
-
// (live smoke shows R1 multi-step + tool-calls regularly exceeds 6min).
|
|
380
|
-
const modelId = this.storedConfig?.model?.modelId;
|
|
381
|
-
const HARD_TIMEOUT_MS = (modelId && CREWLY_AGENT_DEFAULTS.MODEL_TIMEOUT_MS[modelId]) ||
|
|
382
|
-
CREWLY_AGENT_DEFAULTS.MESSAGE_TIMEOUT_MS;
|
|
383
|
-
// Execution tracking for diagnostics
|
|
384
|
-
const executionTracker = {
|
|
385
|
-
phase: 'queued',
|
|
386
|
-
currentTool: null,
|
|
387
|
-
toolCallsCompleted: [],
|
|
388
|
-
startedAt: new Date(),
|
|
389
|
-
lastActivityAt: new Date(),
|
|
390
|
-
messagePreview: cleanMessage.length <= 100
|
|
391
|
-
? cleanMessage
|
|
392
|
-
: `${cleanMessage.substring(0, 50)}...${cleanMessage.substring(cleanMessage.length - 50)}`,
|
|
393
|
-
};
|
|
394
|
-
// Soft warning timer — logs if processing exceeds threshold but does NOT kill it.
|
|
395
|
-
const warningTimer = setTimeout(() => {
|
|
396
|
-
executionTracker.lastActivityAt = new Date();
|
|
397
|
-
this.logger.warn(`Message processing exceeding ${SOFT_WARNING_MS / 1000}s (still running)`, {
|
|
398
|
-
sessionName: session,
|
|
399
|
-
phase: executionTracker.phase,
|
|
400
|
-
toolCallsCompleted: executionTracker.toolCallsCompleted.length,
|
|
401
|
-
messagePreview: cleanMessage.substring(0, 100),
|
|
402
|
-
});
|
|
403
|
-
}, SOFT_WARNING_MS);
|
|
404
|
-
// AbortController for external abort (abortCurrentRun) and repetition detection
|
|
405
|
-
const abortController = new AbortController();
|
|
406
|
-
this.messageAbortController = abortController;
|
|
407
|
-
// #198: Hard timeout — aborts the run if it exceeds MESSAGE_TIMEOUT_MS.
|
|
408
|
-
let hardTimeoutTriggered = false;
|
|
409
|
-
const hardTimeoutTimer = setTimeout(() => {
|
|
410
|
-
hardTimeoutTriggered = true;
|
|
411
|
-
this.logger.error(`Message processing hard timeout (${HARD_TIMEOUT_MS / 1000}s) reached — aborting`, {
|
|
412
|
-
sessionName: session,
|
|
413
|
-
phase: executionTracker.phase,
|
|
414
|
-
currentTool: executionTracker.currentTool,
|
|
415
|
-
toolCallsCompleted: executionTracker.toolCallsCompleted.length,
|
|
416
|
-
messagePreview: cleanMessage.substring(0, 100),
|
|
417
|
-
elapsedMs: Date.now() - executionTracker.startedAt.getTime(),
|
|
418
|
-
});
|
|
419
|
-
this.logBuffer.append(session, 'error', `⏱️ Hard timeout (${HARD_TIMEOUT_MS / 1000}s) reached — aborting message processing. `
|
|
420
|
-
+ `Phase: ${executionTracker.phase}, tools completed: ${executionTracker.toolCallsCompleted.length}`);
|
|
421
|
-
abortController.abort();
|
|
422
|
-
}, HARD_TIMEOUT_MS);
|
|
423
|
-
// Text chunk buffer — collects streaming text and flushes on step boundaries
|
|
424
|
-
let textChunkBuffer = '';
|
|
425
|
-
// Repetition/hallucination detection — tracks recent chunks to detect loops
|
|
426
|
-
const recentChunks = [];
|
|
427
|
-
const REPETITION_WINDOW = 20; // number of recent chunks to track
|
|
428
|
-
const REPETITION_THRESHOLD = 5; // consecutive repeated patterns to trigger abort
|
|
429
|
-
let repetitionDetected = false;
|
|
430
|
-
// Stream service for SSE broadcasting to frontend Dashboard
|
|
431
|
-
const agentStream = AgentStreamService.getInstance();
|
|
432
|
-
// Build streaming callbacks that write to InProcessLogBuffer in real-time
|
|
433
|
-
const streamingCallbacks = {
|
|
434
|
-
onTextChunk: (chunk) => {
|
|
435
|
-
if (chunk.length > 0) {
|
|
436
|
-
executionTracker.lastActivityAt = new Date();
|
|
437
|
-
executionTracker.phase = 'model-thinking';
|
|
438
|
-
textChunkBuffer += chunk;
|
|
439
|
-
// Broadcast to SSE subscribers for Dashboard streaming
|
|
440
|
-
agentStream.emitTextChunk(session, chunk);
|
|
441
|
-
// Repetition detection: track recent chunks and check for loops
|
|
442
|
-
const trimmed = chunk.trim();
|
|
443
|
-
if (trimmed.length > 0) {
|
|
444
|
-
recentChunks.push(trimmed);
|
|
445
|
-
if (recentChunks.length > REPETITION_WINDOW) {
|
|
446
|
-
recentChunks.shift();
|
|
447
|
-
}
|
|
448
|
-
// Check if the last REPETITION_THRESHOLD chunks are identical
|
|
449
|
-
if (recentChunks.length >= REPETITION_THRESHOLD) {
|
|
450
|
-
const tail = recentChunks.slice(-REPETITION_THRESHOLD);
|
|
451
|
-
const allSame = tail.every(c => c === tail[0]);
|
|
452
|
-
if (allSame && tail[0].length >= 3) {
|
|
453
|
-
repetitionDetected = true;
|
|
454
|
-
this.logBuffer.append(session, 'warn', `⚠️ Repetition loop detected: "${tail[0].substring(0, 80)}" repeated ${REPETITION_THRESHOLD}x — aborting generation`);
|
|
455
|
-
this.logger.warn('Repetition/hallucination loop detected, aborting', {
|
|
456
|
-
sessionName: session,
|
|
457
|
-
repeatedChunk: tail[0].substring(0, 100),
|
|
458
|
-
count: REPETITION_THRESHOLD,
|
|
459
|
-
});
|
|
460
|
-
abortController.abort();
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
},
|
|
466
|
-
onToolCallStart: (toolName, _args) => {
|
|
467
|
-
executionTracker.phase = 'tool-calling';
|
|
468
|
-
executionTracker.currentTool = toolName;
|
|
469
|
-
executionTracker.lastActivityAt = new Date();
|
|
470
|
-
agentStream.emitToolCallStart(session, toolName, _args);
|
|
471
|
-
},
|
|
472
|
-
onToolCallFinish: (toolName, args, result, _durationMs) => {
|
|
473
|
-
executionTracker.toolCallsCompleted.push(toolName);
|
|
474
|
-
executionTracker.currentTool = null;
|
|
475
|
-
executionTracker.lastActivityAt = new Date();
|
|
476
|
-
agentStream.emitToolCallFinish(session, toolName, args, result, _durationMs);
|
|
477
|
-
const argsPreview = JSON.stringify(args).substring(0, 120);
|
|
478
|
-
this.logBuffer.append(session, 'info', `🔧 ${toolName}(${argsPreview})`);
|
|
479
|
-
// For bash_exec, show the command as an extra log line for readability
|
|
480
|
-
if (toolName === 'bash_exec' && args.command) {
|
|
481
|
-
const cmdPreview = String(args.command).substring(0, 200);
|
|
482
|
-
this.logBuffer.append(session, 'info', ` $ ${cmdPreview}`);
|
|
483
|
-
}
|
|
484
|
-
const resultPreview = result ? JSON.stringify(result).substring(0, 200) : 'void';
|
|
485
|
-
this.logBuffer.append(session, 'debug', ` → ${resultPreview}`);
|
|
486
|
-
},
|
|
487
|
-
onStepFinish: (stepIndex, hasToolCalls) => {
|
|
488
|
-
executionTracker.lastActivityAt = new Date();
|
|
489
|
-
agentStream.emitStepFinish(session, stepIndex, hasToolCalls);
|
|
490
|
-
// Flush buffered text at each step boundary
|
|
491
|
-
if (textChunkBuffer.trim().length > 0) {
|
|
492
|
-
// Truncate very long text to keep logs readable
|
|
493
|
-
const text = textChunkBuffer.trim();
|
|
494
|
-
const preview = text.length > 500 ? text.substring(0, 500) + '...' : text;
|
|
495
|
-
this.logBuffer.append(session, 'info', `💬 ${preview}`);
|
|
496
|
-
textChunkBuffer = '';
|
|
497
|
-
}
|
|
498
|
-
if (!hasToolCalls) {
|
|
499
|
-
executionTracker.phase = 'model-thinking';
|
|
500
|
-
}
|
|
501
|
-
},
|
|
502
|
-
};
|
|
503
|
-
try {
|
|
504
|
-
executionTracker.phase = 'model-thinking';
|
|
505
|
-
const result = await this.agentRunner.run(cleanMessage, conversationId, metadata, {
|
|
506
|
-
abortSignal: abortController.signal,
|
|
507
|
-
streaming: streamingCallbacks,
|
|
508
|
-
});
|
|
509
|
-
clearTimeout(warningTimer);
|
|
510
|
-
clearTimeout(hardTimeoutTimer);
|
|
511
|
-
this.messageAbortController = null;
|
|
512
|
-
// If we got here after a repetition-triggered abort, treat as error
|
|
513
|
-
if (repetitionDetected) {
|
|
514
|
-
throw new Error('Generation aborted: repetition/hallucination loop detected. '
|
|
515
|
-
+ `Repeated pattern: "${recentChunks[recentChunks.length - 1]?.substring(0, 80)}"`);
|
|
516
|
-
}
|
|
517
|
-
// Flush any remaining buffered text after the run completes
|
|
518
|
-
if (textChunkBuffer.trim().length > 0) {
|
|
519
|
-
const text = textChunkBuffer.trim();
|
|
520
|
-
const preview = text.length > 500 ? text.substring(0, 500) + '...' : text;
|
|
521
|
-
this.logBuffer.append(session, 'info', `💬 ${preview}`);
|
|
522
|
-
textChunkBuffer = '';
|
|
523
|
-
}
|
|
524
|
-
// Tool calls already logged via streaming callbacks (onToolCallStart/Finish).
|
|
525
|
-
// Only log tool calls retroactively if generateText path was used (test mock).
|
|
526
|
-
if (this.agentRunner._generateTextFn) {
|
|
527
|
-
for (const tc of result.toolCalls) {
|
|
528
|
-
executionTracker.toolCallsCompleted.push(tc.toolName);
|
|
529
|
-
const argsPreview = JSON.stringify(tc.args).substring(0, 120);
|
|
530
|
-
this.logBuffer.append(session, 'info', `🔧 ${tc.toolName}(${argsPreview})`);
|
|
531
|
-
const resultPreview = tc.result ? JSON.stringify(tc.result).substring(0, 200) : 'void';
|
|
532
|
-
this.logBuffer.append(session, 'debug', ` → ${resultPreview}`);
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
// Log response summary
|
|
536
|
-
executionTracker.phase = 'complete';
|
|
537
|
-
const textPreview = result.text ? result.text.substring(0, 150) : '(no text)';
|
|
538
|
-
this.logBuffer.append(session, 'info', `→ Response (${result.steps} steps, ${result.toolCalls.length} tools): ${textPreview}`);
|
|
539
|
-
this.logBuffer.append(session, 'debug', ` Tokens: ${result.usage.input}in/${result.usage.output}out`);
|
|
540
|
-
this.logger.info('Message processed', {
|
|
541
|
-
sessionName: session,
|
|
542
|
-
steps: result.steps,
|
|
543
|
-
toolCalls: result.toolCalls.length,
|
|
544
|
-
usage: result.usage,
|
|
545
|
-
finishReason: result.finishReason,
|
|
546
|
-
});
|
|
547
|
-
// Record token usage when tracking is enabled
|
|
548
|
-
this.recordTokenUsageIfEnabled(session, result).catch(() => {
|
|
549
|
-
// Non-critical — don't let tracking errors affect message flow
|
|
550
|
-
});
|
|
551
|
-
return result;
|
|
552
|
-
}
|
|
553
|
-
catch (error) {
|
|
554
|
-
clearTimeout(warningTimer);
|
|
555
|
-
clearTimeout(hardTimeoutTimer);
|
|
556
|
-
this.messageAbortController = null;
|
|
557
|
-
// If this was a hard timeout abort, wrap with a clear error.
|
|
558
|
-
// logBuffer entry already written by the setTimeout callback — skip duplicate.
|
|
559
|
-
if (hardTimeoutTriggered) {
|
|
560
|
-
throw new Error(`Message processing timed out after ${HARD_TIMEOUT_MS / 1000}s. `
|
|
561
|
-
+ `Phase: ${executionTracker.phase}, tools completed: ${executionTracker.toolCallsCompleted.length}`);
|
|
562
|
-
}
|
|
563
|
-
// If this was a repetition-triggered abort, wrap with a clear error
|
|
564
|
-
if (repetitionDetected) {
|
|
565
|
-
const repErr = new Error('Generation aborted: repetition/hallucination loop detected. '
|
|
566
|
-
+ `Repeated pattern: "${recentChunks[recentChunks.length - 1]?.substring(0, 80)}"`);
|
|
567
|
-
this.logBuffer.append(session, 'error', `Agent error: ${repErr.message}`);
|
|
568
|
-
throw repErr;
|
|
569
|
-
}
|
|
570
|
-
const errMsg = error instanceof Error ? error.message : String(error);
|
|
571
|
-
this.logBuffer.append(session, 'error', `Agent error: ${errMsg}`);
|
|
572
|
-
throw error;
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
/**
|
|
576
|
-
* Record token usage to the TokenUsageService if tracking is enabled in settings.
|
|
577
|
-
*
|
|
578
|
-
* @param session - Session name
|
|
579
|
-
* @param result - Agent run result containing usage data
|
|
580
|
-
*/
|
|
581
|
-
async recordTokenUsageIfEnabled(session, result) {
|
|
582
|
-
const settings = await getSettingsService().getSettings();
|
|
583
|
-
if (!settings.general.tokenTracking)
|
|
584
|
-
return;
|
|
585
|
-
TokenUsageService.getInstance().recordUsage(session, session, result.usage.input, result.usage.output, this.currentModelString);
|
|
586
|
-
}
|
|
587
|
-
/**
|
|
588
|
-
* Check if the runtime is initialized and ready to handle messages.
|
|
589
|
-
*
|
|
590
|
-
* @returns True if initializeInProcess() has been called successfully
|
|
591
|
-
*/
|
|
592
|
-
isReady() {
|
|
593
|
-
if (this.useWorkerProcess) {
|
|
594
|
-
return this.initialized && this.workerProcess !== null && this.workerProcess.connected;
|
|
595
|
-
}
|
|
596
|
-
return this.initialized && this.agentRunner !== null && this.agentRunner.isInitialized();
|
|
597
|
-
}
|
|
598
|
-
/**
|
|
599
|
-
* Abort the currently executing message processing.
|
|
600
|
-
*
|
|
601
|
-
* Cancels the active model call, terminates running tool processes,
|
|
602
|
-
* and returns partial results where possible. Safe to call at any time —
|
|
603
|
-
* returns false if no run is in progress.
|
|
604
|
-
*
|
|
605
|
-
* @returns True if an active run was aborted, false if nothing was running
|
|
606
|
-
*/
|
|
607
|
-
abortCurrentRun() {
|
|
608
|
-
const session = this.currentSessionName;
|
|
609
|
-
if (this.useWorkerProcess) {
|
|
610
|
-
if (!this.workerProcessing || !this.workerProcess) {
|
|
611
|
-
return false;
|
|
612
|
-
}
|
|
613
|
-
this.sendToWorker({ type: 'abort' });
|
|
614
|
-
if (this.workerRunReject) {
|
|
615
|
-
this.workerRunReject(new Error('Run aborted by user'));
|
|
616
|
-
this.workerRunResolve = null;
|
|
617
|
-
this.workerRunReject = null;
|
|
618
|
-
}
|
|
619
|
-
this.workerProcessing = false;
|
|
620
|
-
if (session) {
|
|
621
|
-
this.logBuffer.append(session, 'warn', '⚠️ Run aborted by user');
|
|
622
|
-
}
|
|
623
|
-
this.logger.info('Agent run aborted (worker)', { sessionName: session });
|
|
624
|
-
return true;
|
|
625
|
-
}
|
|
626
|
-
if (!this.messageAbortController) {
|
|
627
|
-
return false;
|
|
628
|
-
}
|
|
629
|
-
this.messageAbortController.abort();
|
|
630
|
-
this.messageAbortController = null;
|
|
631
|
-
// Also tell the runner to abort (for cases where the runner has its own abort)
|
|
632
|
-
if (this.agentRunner) {
|
|
633
|
-
this.agentRunner.abortCurrentRun();
|
|
634
|
-
}
|
|
635
|
-
if (session) {
|
|
636
|
-
this.logBuffer.append(session, 'warn', '⚠️ Run aborted by user');
|
|
637
|
-
}
|
|
638
|
-
this.logger.info('Agent run aborted', { sessionName: session });
|
|
639
|
-
return true;
|
|
640
|
-
}
|
|
641
|
-
/**
|
|
642
|
-
* Get the current agent runner instance (for inspection/testing).
|
|
643
|
-
*
|
|
644
|
-
* @returns The AgentRunnerService instance, or null if not initialized
|
|
645
|
-
*/
|
|
646
|
-
getAgentRunner() {
|
|
647
|
-
return this.agentRunner;
|
|
648
|
-
}
|
|
649
|
-
/**
|
|
650
|
-
* Get the session name this runtime was initialized with.
|
|
651
|
-
*
|
|
652
|
-
* @returns Session name string, or null if not initialized
|
|
653
|
-
*/
|
|
654
|
-
getSessionName() {
|
|
655
|
-
return this.currentSessionName;
|
|
656
|
-
}
|
|
657
|
-
/**
|
|
658
|
-
* Shut down the in-process runtime.
|
|
659
|
-
* Clears the agent runner and resets state.
|
|
660
|
-
*/
|
|
661
|
-
shutdown() {
|
|
662
|
-
this.logger.info('Shutting down Crewly Agent runtime', {
|
|
663
|
-
sessionName: this.currentSessionName,
|
|
664
|
-
mode: this.useWorkerProcess ? 'worker' : 'in-process',
|
|
665
|
-
});
|
|
666
|
-
// Mark as not initialized first to reject new messages immediately
|
|
667
|
-
this.initialized = false;
|
|
668
|
-
// Stop heartbeat timer
|
|
669
|
-
this.stopHeartbeat();
|
|
670
|
-
// Shut down worker process if running
|
|
671
|
-
if (this.workerProcess) {
|
|
672
|
-
this.terminateWorker();
|
|
673
|
-
}
|
|
674
|
-
if (this.currentSessionName) {
|
|
675
|
-
this.logBuffer.append(this.currentSessionName, 'info', 'Crewly Agent shutting down');
|
|
676
|
-
this.logBuffer.removeSession(this.currentSessionName);
|
|
677
|
-
}
|
|
678
|
-
this.rateLimiter.reset();
|
|
679
|
-
this.agentRunner = null;
|
|
680
|
-
this.currentSessionName = null;
|
|
681
|
-
this.storedConfig = null;
|
|
682
|
-
}
|
|
683
|
-
// ===== Worker process methods =====
|
|
684
|
-
/**
|
|
685
|
-
* Hot-reload the worker process.
|
|
686
|
-
*
|
|
687
|
-
* Kills the existing worker and spawns a fresh one with the stored config.
|
|
688
|
-
* This allows updating agent code without restarting the main backend.
|
|
689
|
-
* Conversation state is reset — use this when deploying new agent logic.
|
|
690
|
-
*
|
|
691
|
-
* @throws Error if not in worker mode or config is missing
|
|
692
|
-
*/
|
|
693
|
-
async hotReload() {
|
|
694
|
-
if (!this.useWorkerProcess) {
|
|
695
|
-
throw new Error('hotReload() is only available in worker process mode');
|
|
696
|
-
}
|
|
697
|
-
if (!this.storedConfig) {
|
|
698
|
-
throw new Error('No stored config for hot-reload. Was initializeInProcess() called?');
|
|
699
|
-
}
|
|
700
|
-
const session = this.currentSessionName;
|
|
701
|
-
this.logBuffer.append(session, 'info', 'Hot-reloading worker process...');
|
|
702
|
-
this.logger.info('Hot-reloading worker process', { sessionName: session });
|
|
703
|
-
// Terminate old worker
|
|
704
|
-
this.terminateWorker();
|
|
705
|
-
// Spawn new worker with same config
|
|
706
|
-
await this.initializeWorker(this.storedConfig);
|
|
707
|
-
this.logBuffer.append(session, 'info', 'Worker hot-reload complete');
|
|
708
|
-
this.logger.info('Worker hot-reload complete', { sessionName: session });
|
|
709
|
-
}
|
|
710
|
-
/**
|
|
711
|
-
* Check if the runtime is using a worker process.
|
|
712
|
-
*
|
|
713
|
-
* @returns True if running in worker process mode
|
|
714
|
-
*/
|
|
715
|
-
isWorkerMode() {
|
|
716
|
-
return this.useWorkerProcess;
|
|
717
|
-
}
|
|
718
|
-
/**
|
|
719
|
-
* Get the worker process PID (for monitoring/debugging).
|
|
720
|
-
*
|
|
721
|
-
* @returns Worker PID, or null if not in worker mode or worker is not running
|
|
722
|
-
*/
|
|
723
|
-
getWorkerPid() {
|
|
724
|
-
return this.workerProcess?.pid ?? null;
|
|
725
|
-
}
|
|
726
|
-
/**
|
|
727
|
-
* Initialize a worker child process via fork().
|
|
728
|
-
*
|
|
729
|
-
* Forks the agent-worker.ts entry point and sends the init message
|
|
730
|
-
* with the agent config. Waits for the 'ready' response before resolving.
|
|
731
|
-
*
|
|
732
|
-
* @param config - Full agent config to send to the worker
|
|
733
|
-
* @throws Error if worker fails to initialize within timeout
|
|
734
|
-
*/
|
|
735
|
-
async initializeWorker(config) {
|
|
736
|
-
return new Promise((resolve, reject) => {
|
|
737
|
-
const workerPath = this.getWorkerEntryPath();
|
|
738
|
-
this.workerProcess = fork(workerPath, [], {
|
|
739
|
-
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
|
|
740
|
-
env: { ...process.env },
|
|
741
|
-
});
|
|
742
|
-
const initTimeout = setTimeout(() => {
|
|
743
|
-
this.terminateWorker();
|
|
744
|
-
reject(new Error('Worker initialization timed out (30s)'));
|
|
745
|
-
}, 30_000);
|
|
746
|
-
let initResolved = false;
|
|
747
|
-
this.workerProcess.on('message', (msg) => {
|
|
748
|
-
// Handle init response
|
|
749
|
-
if (!initResolved && msg.type === 'ready') {
|
|
750
|
-
initResolved = true;
|
|
751
|
-
clearTimeout(initTimeout);
|
|
752
|
-
resolve();
|
|
753
|
-
return;
|
|
754
|
-
}
|
|
755
|
-
if (!initResolved && msg.type === 'error' && msg.code === 'INIT_FAILED') {
|
|
756
|
-
initResolved = true;
|
|
757
|
-
clearTimeout(initTimeout);
|
|
758
|
-
reject(new Error(msg.error));
|
|
759
|
-
return;
|
|
760
|
-
}
|
|
761
|
-
// Handle runtime messages
|
|
762
|
-
this.handleWorkerMessage(msg);
|
|
763
|
-
});
|
|
764
|
-
this.workerProcess.on('exit', (code, signal) => {
|
|
765
|
-
this.logger.warn('Worker process exited', {
|
|
766
|
-
sessionName: this.currentSessionName,
|
|
767
|
-
code,
|
|
768
|
-
signal,
|
|
769
|
-
});
|
|
770
|
-
if (!initResolved) {
|
|
771
|
-
initResolved = true;
|
|
772
|
-
clearTimeout(initTimeout);
|
|
773
|
-
reject(new Error(`Worker exited during init (code=${code}, signal=${signal})`));
|
|
774
|
-
}
|
|
775
|
-
// Reject any pending run
|
|
776
|
-
if (this.workerRunReject) {
|
|
777
|
-
this.workerRunReject(new Error(`Worker process exited unexpectedly (code=${code}, signal=${signal})`));
|
|
778
|
-
this.workerRunResolve = null;
|
|
779
|
-
this.workerRunReject = null;
|
|
780
|
-
this.workerProcessing = false;
|
|
781
|
-
}
|
|
782
|
-
this.workerProcess = null;
|
|
783
|
-
if (this.currentSessionName) {
|
|
784
|
-
this.logBuffer.append(this.currentSessionName, 'warn', `Worker process exited (code=${code}, signal=${signal})`);
|
|
785
|
-
}
|
|
786
|
-
});
|
|
787
|
-
this.workerProcess.on('error', (err) => {
|
|
788
|
-
this.logger.error('Worker process error', {
|
|
789
|
-
sessionName: this.currentSessionName,
|
|
790
|
-
error: err.message,
|
|
791
|
-
});
|
|
792
|
-
if (!initResolved) {
|
|
793
|
-
initResolved = true;
|
|
794
|
-
clearTimeout(initTimeout);
|
|
795
|
-
reject(err);
|
|
796
|
-
}
|
|
797
|
-
});
|
|
798
|
-
// Capture worker stdout/stderr for debugging
|
|
799
|
-
this.workerProcess.stdout?.on('data', (data) => {
|
|
800
|
-
const text = data.toString().trim();
|
|
801
|
-
if (text && this.currentSessionName) {
|
|
802
|
-
this.logBuffer.append(this.currentSessionName, 'debug', `[worker stdout] ${text}`);
|
|
803
|
-
}
|
|
804
|
-
});
|
|
805
|
-
this.workerProcess.stderr?.on('data', (data) => {
|
|
806
|
-
const text = data.toString().trim();
|
|
807
|
-
if (text && this.currentSessionName) {
|
|
808
|
-
this.logBuffer.append(this.currentSessionName, 'warn', `[worker stderr] ${text}`);
|
|
809
|
-
}
|
|
810
|
-
});
|
|
811
|
-
// Send init message with config
|
|
812
|
-
this.sendToWorker({ type: 'init', config });
|
|
813
|
-
});
|
|
814
|
-
}
|
|
815
|
-
/**
|
|
816
|
-
* Execute a message via the worker process using IPC.
|
|
817
|
-
*
|
|
818
|
-
* Sends a 'run' message to the worker and waits for the 'result' or 'error'
|
|
819
|
-
* response. Streaming events are forwarded to the InProcessLogBuffer in real-time.
|
|
820
|
-
*
|
|
821
|
-
* @param session - Session name
|
|
822
|
-
* @param cleanMessage - Message content (prefix already stripped)
|
|
823
|
-
* @param conversationId - Optional conversation ID
|
|
824
|
-
* @param _metadata - Optional metadata (passed to worker)
|
|
825
|
-
* @returns Agent run result from the worker
|
|
826
|
-
*/
|
|
827
|
-
executeMessageViaWorker(session, cleanMessage, conversationId, _metadata) {
|
|
828
|
-
return new Promise((resolve, reject) => {
|
|
829
|
-
if (!this.workerProcess || !this.workerProcess.connected) {
|
|
830
|
-
reject(new Error('Worker process not available'));
|
|
831
|
-
return;
|
|
832
|
-
}
|
|
833
|
-
this.workerProcessing = true;
|
|
834
|
-
this.workerRunResolve = (result) => {
|
|
835
|
-
this.workerProcessing = false;
|
|
836
|
-
this.workerRunResolve = null;
|
|
837
|
-
this.workerRunReject = null;
|
|
838
|
-
// Log response summary
|
|
839
|
-
const textPreview = result.text ? result.text.substring(0, 150) : '(no text)';
|
|
840
|
-
this.logBuffer.append(session, 'info', `→ Response (${result.steps} steps, ${result.toolCalls.length} tools): ${textPreview}`);
|
|
841
|
-
this.logBuffer.append(session, 'debug', ` Tokens: ${result.usage.input}in/${result.usage.output}out`);
|
|
842
|
-
this.logger.info('Message processed (worker)', {
|
|
843
|
-
sessionName: session,
|
|
844
|
-
steps: result.steps,
|
|
845
|
-
toolCalls: result.toolCalls.length,
|
|
846
|
-
usage: result.usage,
|
|
847
|
-
finishReason: result.finishReason,
|
|
848
|
-
});
|
|
849
|
-
// Record token usage
|
|
850
|
-
this.recordTokenUsageIfEnabled(session, result).catch(() => { });
|
|
851
|
-
resolve(result);
|
|
852
|
-
};
|
|
853
|
-
this.workerRunReject = (error) => {
|
|
854
|
-
this.workerProcessing = false;
|
|
855
|
-
this.workerRunResolve = null;
|
|
856
|
-
this.workerRunReject = null;
|
|
857
|
-
this.logBuffer.append(session, 'error', `Agent error (worker): ${error.message}`);
|
|
858
|
-
reject(error);
|
|
859
|
-
};
|
|
860
|
-
this.sendToWorker({
|
|
861
|
-
type: 'run',
|
|
862
|
-
message: cleanMessage,
|
|
863
|
-
conversationId,
|
|
864
|
-
metadata: _metadata,
|
|
865
|
-
});
|
|
866
|
-
});
|
|
867
|
-
}
|
|
868
|
-
/**
|
|
869
|
-
* Handle messages received from the worker process.
|
|
870
|
-
*
|
|
871
|
-
* Routes streaming events to the InProcessLogBuffer and resolves/rejects
|
|
872
|
-
* pending run promises on result/error messages.
|
|
873
|
-
*
|
|
874
|
-
* @param msg - Worker message received via IPC
|
|
875
|
-
*/
|
|
876
|
-
handleWorkerMessage(msg) {
|
|
877
|
-
const session = this.currentSessionName;
|
|
878
|
-
switch (msg.type) {
|
|
879
|
-
case 'result':
|
|
880
|
-
if (this.workerRunResolve) {
|
|
881
|
-
this.workerRunResolve(msg.data);
|
|
882
|
-
}
|
|
883
|
-
break;
|
|
884
|
-
case 'error':
|
|
885
|
-
if (this.workerRunReject) {
|
|
886
|
-
this.workerRunReject(new Error(msg.error));
|
|
887
|
-
}
|
|
888
|
-
else if (session) {
|
|
889
|
-
// Error outside of a run (e.g. crash notification)
|
|
890
|
-
this.logBuffer.append(session, 'error', `Worker error: ${msg.error}`);
|
|
891
|
-
}
|
|
892
|
-
break;
|
|
893
|
-
case 'log':
|
|
894
|
-
if (session) {
|
|
895
|
-
this.logBuffer.append(session, msg.level, `[worker] ${msg.message}`);
|
|
896
|
-
}
|
|
897
|
-
break;
|
|
898
|
-
case 'stream':
|
|
899
|
-
if (!session)
|
|
900
|
-
break;
|
|
901
|
-
switch (msg.event) {
|
|
902
|
-
case 'text':
|
|
903
|
-
// Text chunks are buffered and flushed at step boundaries
|
|
904
|
-
break;
|
|
905
|
-
case 'toolStart':
|
|
906
|
-
// No-op: logged on finish
|
|
907
|
-
break;
|
|
908
|
-
case 'toolFinish': {
|
|
909
|
-
const { toolName, args, result } = msg.data;
|
|
910
|
-
const argsPreview = JSON.stringify(args).substring(0, 120);
|
|
911
|
-
this.logBuffer.append(session, 'info', `🔧 ${toolName}(${argsPreview})`);
|
|
912
|
-
if (toolName === 'bash_exec' && args.command) {
|
|
913
|
-
const cmdPreview = String(args.command).substring(0, 200);
|
|
914
|
-
this.logBuffer.append(session, 'info', ` $ ${cmdPreview}`);
|
|
915
|
-
}
|
|
916
|
-
const resultPreview = result ? JSON.stringify(result).substring(0, 200) : 'void';
|
|
917
|
-
this.logBuffer.append(session, 'debug', ` → ${resultPreview}`);
|
|
918
|
-
break;
|
|
919
|
-
}
|
|
920
|
-
case 'stepFinish':
|
|
921
|
-
// Step boundaries are tracked by the worker
|
|
922
|
-
break;
|
|
923
|
-
}
|
|
924
|
-
break;
|
|
925
|
-
case 'state':
|
|
926
|
-
// State query responses (used internally)
|
|
927
|
-
break;
|
|
928
|
-
default:
|
|
929
|
-
break;
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
/**
|
|
933
|
-
* Send a typed message to the worker process.
|
|
934
|
-
*
|
|
935
|
-
* @param msg - Parent message to send
|
|
936
|
-
*/
|
|
937
|
-
sendToWorker(msg) {
|
|
938
|
-
if (this.workerProcess && this.workerProcess.connected) {
|
|
939
|
-
this.workerProcess.send(msg);
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
/**
|
|
943
|
-
* Terminate the worker process gracefully, with a forced kill fallback.
|
|
944
|
-
*/
|
|
945
|
-
terminateWorker() {
|
|
946
|
-
if (!this.workerProcess)
|
|
947
|
-
return;
|
|
948
|
-
// Try graceful shutdown first
|
|
949
|
-
try {
|
|
950
|
-
if (this.workerProcess.connected) {
|
|
951
|
-
this.sendToWorker({ type: 'shutdown' });
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
catch {
|
|
955
|
-
// Ignore send errors during shutdown
|
|
956
|
-
}
|
|
957
|
-
// Force kill after 5s if still alive
|
|
958
|
-
const pid = this.workerProcess.pid;
|
|
959
|
-
const killTimer = setTimeout(() => {
|
|
960
|
-
try {
|
|
961
|
-
if (pid)
|
|
962
|
-
process.kill(pid, 'SIGKILL');
|
|
963
|
-
}
|
|
964
|
-
catch {
|
|
965
|
-
// Already dead
|
|
966
|
-
}
|
|
967
|
-
}, 5_000);
|
|
968
|
-
killTimer.unref();
|
|
969
|
-
this.workerProcess.removeAllListeners();
|
|
970
|
-
this.workerProcess = null;
|
|
971
|
-
this.workerProcessing = false;
|
|
972
|
-
// Reject any pending run
|
|
973
|
-
if (this.workerRunReject) {
|
|
974
|
-
this.workerRunReject(new Error('Worker terminated'));
|
|
975
|
-
this.workerRunResolve = null;
|
|
976
|
-
this.workerRunReject = null;
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
/**
|
|
980
|
-
* Get the path to the compiled worker entry file.
|
|
981
|
-
*
|
|
982
|
-
* @returns Absolute path to the agent-worker.js compiled file
|
|
983
|
-
*/
|
|
984
|
-
/**
|
|
985
|
-
* @internal Visible for testing — override to provide a custom worker path.
|
|
986
|
-
*/
|
|
987
|
-
_workerEntryPath = null;
|
|
988
|
-
getWorkerEntryPath() {
|
|
989
|
-
if (this._workerEntryPath)
|
|
990
|
-
return this._workerEntryPath;
|
|
991
|
-
// Resolve path relative to the compiled dist/ directory.
|
|
992
|
-
// The worker file is always alongside this file after tsc compilation.
|
|
993
|
-
// Use __dirname (available in both CJS and compiled ESM with tsconfig module: NodeNext).
|
|
994
|
-
const thisDir = path.dirname(__filename);
|
|
995
|
-
return path.join(thisDir, 'agent-worker.js');
|
|
996
|
-
}
|
|
997
|
-
// ===== Private helpers =====
|
|
998
|
-
/**
|
|
999
|
-
* Start periodic heartbeat to keep the in-process agent marked as active.
|
|
1000
|
-
*
|
|
1001
|
-
* Unlike PTY-based agents that get implicit heartbeats from every API call
|
|
1002
|
-
* via the middleware, in-process agents only touch the API during message
|
|
1003
|
-
* processing. Between messages, this timer ensures the agent stays registered
|
|
1004
|
-
* as active in teamAgentStatus.json and the PtyActivityTracker.
|
|
1005
|
-
*
|
|
1006
|
-
* @param sessionName - Session name to heartbeat for
|
|
1007
|
-
*/
|
|
1008
|
-
startHeartbeat(sessionName) {
|
|
1009
|
-
this.stopHeartbeat();
|
|
1010
|
-
const interval = setInterval(() => {
|
|
1011
|
-
if (!this.initialized) {
|
|
1012
|
-
this.stopHeartbeat();
|
|
1013
|
-
return;
|
|
1014
|
-
}
|
|
1015
|
-
// Update heartbeat in teamAgentStatus.json (fire-and-forget)
|
|
1016
|
-
// Pass memberId so the entry is keyed by member ID (not session name)
|
|
1017
|
-
updateAgentHeartbeat(sessionName, this.currentMemberId).catch((err) => {
|
|
1018
|
-
this.logger.debug('Heartbeat update failed (non-critical)', {
|
|
1019
|
-
sessionName,
|
|
1020
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1021
|
-
});
|
|
1022
|
-
});
|
|
1023
|
-
// Record API activity so PtyActivityTracker doesn't mark us idle
|
|
1024
|
-
try {
|
|
1025
|
-
PtyActivityTrackerService.getInstance().recordApiActivity(sessionName);
|
|
1026
|
-
}
|
|
1027
|
-
catch {
|
|
1028
|
-
// PtyActivityTracker may not be initialized yet
|
|
1029
|
-
}
|
|
1030
|
-
}, CREWLY_AGENT_DEFAULTS.HEARTBEAT_INTERVAL_MS);
|
|
1031
|
-
// Don't keep the process alive just for heartbeat
|
|
1032
|
-
interval.unref();
|
|
1033
|
-
this.heartbeatTimer = interval;
|
|
1034
|
-
this.logger.debug('In-process heartbeat started', {
|
|
1035
|
-
sessionName,
|
|
1036
|
-
intervalMs: CREWLY_AGENT_DEFAULTS.HEARTBEAT_INTERVAL_MS,
|
|
1037
|
-
});
|
|
1038
|
-
}
|
|
1039
|
-
/**
|
|
1040
|
-
* Stop the periodic heartbeat timer.
|
|
1041
|
-
*/
|
|
1042
|
-
stopHeartbeat() {
|
|
1043
|
-
if (this.heartbeatTimer) {
|
|
1044
|
-
clearInterval(this.heartbeatTimer);
|
|
1045
|
-
this.heartbeatTimer = null;
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
/**
|
|
1049
|
-
* Load the base system prompt for a given role from file.
|
|
1050
|
-
*
|
|
1051
|
-
* @param roleName - Role name (maps to config/roles/{roleName}/prompt.md)
|
|
1052
|
-
* @returns System prompt content, or a generic fallback if file is missing
|
|
1053
|
-
*/
|
|
1054
|
-
async loadSystemPrompt(roleName = 'orchestrator') {
|
|
1055
|
-
const promptPath = path.join(this.projectRoot, 'config', 'roles', roleName, 'prompt.md');
|
|
1056
|
-
try {
|
|
1057
|
-
const content = await fs.readFile(promptPath, 'utf8');
|
|
1058
|
-
this.logger.debug('System prompt loaded', {
|
|
1059
|
-
promptPath,
|
|
1060
|
-
length: content.length,
|
|
1061
|
-
});
|
|
1062
|
-
return content;
|
|
1063
|
-
}
|
|
1064
|
-
catch (error) {
|
|
1065
|
-
this.logger.warn('Failed to load system prompt, using fallback', {
|
|
1066
|
-
promptPath,
|
|
1067
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1068
|
-
});
|
|
1069
|
-
return 'You are the Crewly orchestrator agent. Manage teams and delegate tasks.';
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
/**
|
|
1073
|
-
* Build an enhanced system prompt that includes the base role prompt
|
|
1074
|
-
* plus awareness of available skills and installed addons.
|
|
1075
|
-
*
|
|
1076
|
-
* Sections appended:
|
|
1077
|
-
* 1. Available Skills - summary from AGENT_SKILLS_CATALOG.md
|
|
1078
|
-
* 2. Installed Addons - names and versions from each addon's manifest.json
|
|
1079
|
-
* 3. Instructions - basic behavioral guidance for the agent
|
|
1080
|
-
*
|
|
1081
|
-
* All file reads are wrapped in try/catch so missing files are gracefully skipped.
|
|
1082
|
-
*
|
|
1083
|
-
* @param roleName - Role name for the base prompt lookup
|
|
1084
|
-
* @returns Combined system prompt string
|
|
1085
|
-
*/
|
|
1086
|
-
async buildEnhancedSystemPrompt(roleName = 'orchestrator') {
|
|
1087
|
-
const basePrompt = await this.loadSystemPrompt(roleName);
|
|
1088
|
-
const sections = [basePrompt];
|
|
1089
|
-
// --- Available Skills ---
|
|
1090
|
-
const skillsSummary = await this.loadSkillsCatalogSummary();
|
|
1091
|
-
if (skillsSummary) {
|
|
1092
|
-
sections.push(`\n## Available Skills\n${skillsSummary}`);
|
|
1093
|
-
}
|
|
1094
|
-
// --- Installed Addons ---
|
|
1095
|
-
const addonsSection = await this.loadInstalledAddons();
|
|
1096
|
-
if (addonsSection) {
|
|
1097
|
-
sections.push(`\n## Installed Addons\n${addonsSection}`);
|
|
1098
|
-
}
|
|
1099
|
-
// --- Instructions ---
|
|
1100
|
-
sections.push('\n## Instructions\n'
|
|
1101
|
-
+ 'You have access to the above skills via bash. '
|
|
1102
|
-
+ 'When asked questions, use your tools to find answers. '
|
|
1103
|
-
+ 'Maintain conversation context across messages.\n\n'
|
|
1104
|
-
+ '**IMPORTANT — Output Requirements:**\n'
|
|
1105
|
-
+ '每次任务完成后,你**必须**用文字输出任务总结(包括发现、结果、遇到的问题),然后调用report-status技能汇报状态。'
|
|
1106
|
-
+ '不要只做工具调用而不输出文字总结。\n'
|
|
1107
|
-
+ 'After completing any task, you MUST output a text summary (findings, results, issues encountered), '
|
|
1108
|
-
+ 'then call report-status to report your status. Never finish with only tool calls and no text output.');
|
|
1109
|
-
return sections.join('\n');
|
|
1110
|
-
}
|
|
1111
|
-
/**
|
|
1112
|
-
* Load and summarize the agent skills catalog file.
|
|
1113
|
-
*
|
|
1114
|
-
* Reads ~/.crewly/skills/AGENT_SKILLS_CATALOG.md and extracts up to
|
|
1115
|
-
* the first 50 lines as a summary. Returns null if the file is missing.
|
|
1116
|
-
*
|
|
1117
|
-
* @returns Skills summary string, or null if unavailable
|
|
1118
|
-
*/
|
|
1119
|
-
async loadSkillsCatalogSummary() {
|
|
1120
|
-
const catalogPath = path.join(homedir(), CREWLY_CONSTANTS.PATHS.CREWLY_HOME, CREWLY_CONSTANTS.PATHS.SKILLS_DIR, CREWLY_CONSTANTS.PATHS.SKILLS_CATALOG_FILE);
|
|
1121
|
-
try {
|
|
1122
|
-
const content = await fs.readFile(catalogPath, 'utf8');
|
|
1123
|
-
const lines = content.split('\n');
|
|
1124
|
-
const maxLines = 50;
|
|
1125
|
-
const summary = lines.slice(0, maxLines).join('\n');
|
|
1126
|
-
const truncated = lines.length > maxLines ? `\n... (${lines.length - maxLines} more lines)` : '';
|
|
1127
|
-
this.logger.debug('Skills catalog loaded', { catalogPath, totalLines: lines.length });
|
|
1128
|
-
return summary + truncated;
|
|
1129
|
-
}
|
|
1130
|
-
catch {
|
|
1131
|
-
this.logger.debug('Skills catalog not found, skipping', { catalogPath });
|
|
1132
|
-
return null;
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
/**
|
|
1136
|
-
* Scan installed addons and build a summary list.
|
|
1137
|
-
*
|
|
1138
|
-
* Reads manifest.json from each subdirectory under the addons directory
|
|
1139
|
-
* and returns a markdown list of addon names, versions, and descriptions.
|
|
1140
|
-
* Returns null if no addons are installed.
|
|
1141
|
-
*
|
|
1142
|
-
* @returns Addon list string, or null if no addons found
|
|
1143
|
-
*/
|
|
1144
|
-
async loadInstalledAddons() {
|
|
1145
|
-
const addonsDir = path.join(homedir(), CREWLY_CONSTANTS.PATHS.CREWLY_HOME, ADDON_CONSTANTS.PATHS.ADDONS_DIR);
|
|
1146
|
-
let entries;
|
|
1147
|
-
try {
|
|
1148
|
-
entries = await fs.readdir(addonsDir);
|
|
1149
|
-
}
|
|
1150
|
-
catch {
|
|
1151
|
-
this.logger.debug('Addons directory not found, skipping', { addonsDir });
|
|
1152
|
-
return null;
|
|
1153
|
-
}
|
|
1154
|
-
const addonLines = [];
|
|
1155
|
-
for (const entry of entries) {
|
|
1156
|
-
const manifestPath = path.join(addonsDir, entry, ADDON_CONSTANTS.MANIFEST_FILE);
|
|
1157
|
-
try {
|
|
1158
|
-
const raw = await fs.readFile(manifestPath, 'utf8');
|
|
1159
|
-
const manifest = JSON.parse(raw);
|
|
1160
|
-
const name = manifest.name || entry;
|
|
1161
|
-
const version = manifest.version || 'unknown';
|
|
1162
|
-
const desc = manifest.description ? `: ${manifest.description}` : '';
|
|
1163
|
-
addonLines.push(`- ${name} v${version}${desc}`);
|
|
1164
|
-
}
|
|
1165
|
-
catch {
|
|
1166
|
-
// Skip directories without valid manifest
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
if (addonLines.length === 0) {
|
|
1170
|
-
return null;
|
|
1171
|
-
}
|
|
1172
|
-
this.logger.debug('Installed addons loaded', { count: addonLines.length });
|
|
1173
|
-
return addonLines.join('\n');
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1176
|
-
//# sourceMappingURL=crewly-agent-runtime.service.js.map
|