@roj-ai/sdk 0.1.32 → 0.1.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bootstrap.d.ts +3 -0
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +5 -0
- package/dist/bootstrap.js.map +1 -1
- package/dist/core/sessions/session-manager.d.ts +3 -0
- package/dist/core/sessions/session-manager.d.ts.map +1 -1
- package/dist/core/sessions/session-manager.js +3 -1
- package/dist/core/sessions/session-manager.js.map +1 -1
- package/dist/core/system.d.ts +2 -0
- package/dist/core/system.d.ts.map +1 -1
- package/dist/core/system.js +2 -1
- package/dist/core/system.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/plugins/services/pid-registry.d.ts +72 -0
- package/dist/plugins/services/pid-registry.d.ts.map +1 -0
- package/dist/plugins/services/pid-registry.js +171 -0
- package/dist/plugins/services/pid-registry.js.map +1 -0
- package/dist/plugins/services/pid-registry.test.d.ts +2 -0
- package/dist/plugins/services/pid-registry.test.d.ts.map +1 -0
- package/dist/plugins/services/pid-registry.test.js +161 -0
- package/dist/plugins/services/pid-registry.test.js.map +1 -0
- package/dist/plugins/services/plugin.d.ts +3 -0
- package/dist/plugins/services/plugin.d.ts.map +1 -1
- package/dist/plugins/services/plugin.js +5 -1
- package/dist/plugins/services/plugin.js.map +1 -1
- package/dist/plugins/services/service.d.ts +4 -0
- package/dist/plugins/services/service.d.ts.map +1 -1
- package/dist/plugins/services/service.js +8 -0
- package/dist/plugins/services/service.js.map +1 -1
- package/dist/plugins/session-stats/index.d.ts +1 -1
- package/dist/plugins/session-stats/index.d.ts.map +1 -1
- package/dist/plugins/session-stats/plugin.d.ts +16 -0
- package/dist/plugins/session-stats/plugin.d.ts.map +1 -1
- package/dist/plugins/session-stats/plugin.js +29 -2
- package/dist/plugins/session-stats/plugin.js.map +1 -1
- package/dist/plugins/session-stats/plugin.test.d.ts +2 -0
- package/dist/plugins/session-stats/plugin.test.d.ts.map +1 -0
- package/dist/plugins/session-stats/plugin.test.js +145 -0
- package/dist/plugins/session-stats/plugin.test.js.map +1 -0
- package/dist/transport/http/middleware/error-handler.d.ts +1 -1
- package/package.json +2 -2
- package/src/bootstrap.ts +7 -0
- package/src/core/sessions/session-manager.ts +5 -1
- package/src/core/system.ts +4 -0
- package/src/index.ts +1 -1
- package/src/plugins/services/pid-registry.test.ts +180 -0
- package/src/plugins/services/pid-registry.ts +191 -0
- package/src/plugins/services/plugin.ts +8 -1
- package/src/plugins/services/service.ts +12 -0
- package/src/plugins/session-stats/index.ts +1 -1
- package/src/plugins/session-stats/plugin.test.ts +160 -0
- package/src/plugins/session-stats/plugin.ts +48 -2
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
import { agentEvents } from '../../core/agents/state.js';
|
|
2
|
+
import { contextEvents } from '../../core/context/state.js';
|
|
2
3
|
import { llmEvents } from '../../core/llm/state.js';
|
|
3
4
|
import { definePlugin } from '../../core/plugins/plugin-builder.js';
|
|
4
5
|
import { selectPluginState } from '../../core/sessions/reducer.js';
|
|
5
6
|
import { toolEvents } from '../../core/tools/state.js';
|
|
7
|
+
import { userChatEvents } from '../../plugins/user-chat/plugin.js';
|
|
6
8
|
const createInitialStats = () => ({
|
|
7
9
|
totalTokens: 0,
|
|
8
10
|
promptTokens: 0,
|
|
9
11
|
completionTokens: 0,
|
|
12
|
+
cacheReadTokens: 0,
|
|
13
|
+
cacheWriteTokens: 0,
|
|
10
14
|
totalCost: 0,
|
|
11
15
|
llmCalls: 0,
|
|
12
16
|
llmErrors: 0,
|
|
13
17
|
toolCalls: 0,
|
|
14
18
|
toolErrors: 0,
|
|
19
|
+
userMessages: 0,
|
|
20
|
+
compactions: 0,
|
|
15
21
|
agentCount: 0,
|
|
16
22
|
firstEventAt: null,
|
|
17
23
|
lastEventAt: null,
|
|
18
24
|
byProvider: {},
|
|
25
|
+
byAgent: {},
|
|
19
26
|
});
|
|
20
27
|
const PLUGIN_STATE_KEY = 'sessionStats';
|
|
21
28
|
/**
|
|
@@ -28,11 +35,11 @@ export function selectSessionStats(sessionState) {
|
|
|
28
35
|
// Plugin
|
|
29
36
|
// ============================================================================
|
|
30
37
|
export const sessionStatsPlugin = definePlugin('session-stats')
|
|
31
|
-
.events([agentEvents, llmEvents, toolEvents])
|
|
38
|
+
.events([agentEvents, llmEvents, toolEvents, contextEvents, userChatEvents])
|
|
32
39
|
.state({
|
|
33
40
|
key: PLUGIN_STATE_KEY,
|
|
34
41
|
initial: createInitialStats,
|
|
35
|
-
reduce: (stats, event) => {
|
|
42
|
+
reduce: (stats, event, sessionState) => {
|
|
36
43
|
const now = event.timestamp;
|
|
37
44
|
const withTimestamp = (partial) => ({
|
|
38
45
|
...stats,
|
|
@@ -61,13 +68,29 @@ export const sessionStatsPlugin = definePlugin('session-stats')
|
|
|
61
68
|
},
|
|
62
69
|
}
|
|
63
70
|
: stats.byProvider;
|
|
71
|
+
// Core reducer runs before plugin slices, so the spawning agent is already in state.
|
|
72
|
+
const definitionName = sessionState.agents.get(event.agentId)?.definitionName;
|
|
73
|
+
const byAgent = definitionName
|
|
74
|
+
? {
|
|
75
|
+
...stats.byAgent,
|
|
76
|
+
[definitionName]: {
|
|
77
|
+
llmCalls: (stats.byAgent[definitionName]?.llmCalls ?? 0) + 1,
|
|
78
|
+
promptTokens: (stats.byAgent[definitionName]?.promptTokens ?? 0) + event.metrics.promptTokens,
|
|
79
|
+
completionTokens: (stats.byAgent[definitionName]?.completionTokens ?? 0) + event.metrics.completionTokens,
|
|
80
|
+
totalCost: (stats.byAgent[definitionName]?.totalCost ?? 0) + (event.metrics.cost ?? 0),
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
: stats.byAgent;
|
|
64
84
|
return withTimestamp({
|
|
65
85
|
llmCalls: stats.llmCalls + 1,
|
|
66
86
|
totalTokens: stats.totalTokens + event.metrics.totalTokens,
|
|
67
87
|
promptTokens: stats.promptTokens + event.metrics.promptTokens,
|
|
68
88
|
completionTokens: stats.completionTokens + event.metrics.completionTokens,
|
|
89
|
+
cacheReadTokens: stats.cacheReadTokens + (event.metrics.cachedTokens ?? 0),
|
|
90
|
+
cacheWriteTokens: stats.cacheWriteTokens + (event.metrics.cacheWriteTokens ?? 0),
|
|
69
91
|
totalCost: stats.totalCost + (event.metrics.cost ?? 0),
|
|
70
92
|
byProvider,
|
|
93
|
+
byAgent,
|
|
71
94
|
});
|
|
72
95
|
}
|
|
73
96
|
case 'inference_failed':
|
|
@@ -76,6 +99,10 @@ export const sessionStatsPlugin = definePlugin('session-stats')
|
|
|
76
99
|
return withTimestamp({ toolCalls: stats.toolCalls + 1 });
|
|
77
100
|
case 'tool_failed':
|
|
78
101
|
return withTimestamp({ toolErrors: stats.toolErrors + 1 });
|
|
102
|
+
case 'user_chat_message_received':
|
|
103
|
+
return withTimestamp({ userMessages: stats.userMessages + 1 });
|
|
104
|
+
case 'context_compacted':
|
|
105
|
+
return withTimestamp({ compactions: stats.compactions + 1 });
|
|
79
106
|
default:
|
|
80
107
|
return stats;
|
|
81
108
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../../src/plugins/session-stats/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AACpD,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAA;AAE9D,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../../src/plugins/session-stats/plugin.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.test.d.ts","sourceRoot":"","sources":["../../../src/plugins/session-stats/plugin.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { AgentId } from '../../core/agents/schema.js';
|
|
3
|
+
import { agentEvents } from '../../core/agents/state.js';
|
|
4
|
+
import { contextEvents } from '../../core/context/state.js';
|
|
5
|
+
import { withSessionId } from '../../core/events/test-helpers.js';
|
|
6
|
+
import { llmEvents } from '../../core/llm/state.js';
|
|
7
|
+
import { createApplyEvent } from '../../core/sessions/apply-event.js';
|
|
8
|
+
import { SessionId } from '../../core/sessions/schema.js';
|
|
9
|
+
import { createSessionState } from '../../core/sessions/state.js';
|
|
10
|
+
import { ToolCallId } from '../../core/tools/schema.js';
|
|
11
|
+
import { toolEvents } from '../../core/tools/state.js';
|
|
12
|
+
import { userChatEvents } from '../../plugins/user-chat/plugin.js';
|
|
13
|
+
import { ChatMessageId } from '../../plugins/user-chat/schema.js';
|
|
14
|
+
import { selectSessionStats, sessionStatsPlugin } from './plugin.js';
|
|
15
|
+
const sessionId = SessionId('session-stats-test');
|
|
16
|
+
const orchestrator = AgentId('orchestrator-1');
|
|
17
|
+
const worker = AgentId('worker-1');
|
|
18
|
+
const applyEvent = createApplyEvent([sessionStatsPlugin.create()]);
|
|
19
|
+
const metrics = (overrides = {}) => ({
|
|
20
|
+
promptTokens: 100,
|
|
21
|
+
completionTokens: 20,
|
|
22
|
+
totalTokens: 120,
|
|
23
|
+
latencyMs: 10,
|
|
24
|
+
model: 'test-model',
|
|
25
|
+
provider: 'anthropic',
|
|
26
|
+
cost: 0.5,
|
|
27
|
+
...overrides,
|
|
28
|
+
});
|
|
29
|
+
const spawn = (agentId, definitionName) => withSessionId(sessionId, agentEvents.create('agent_spawned', { agentId, definitionName, parentId: null }));
|
|
30
|
+
const inference = (agentId, m) => withSessionId(sessionId, llmEvents.create('inference_completed', {
|
|
31
|
+
agentId,
|
|
32
|
+
consumedMessageIds: [],
|
|
33
|
+
response: { content: 'ok', toolCalls: [] },
|
|
34
|
+
metrics: m,
|
|
35
|
+
}));
|
|
36
|
+
const reduce = (events) => selectSessionStats(events.reduce(applyEvent, createSessionState(sessionId, 'test-preset', 0)));
|
|
37
|
+
describe('session-stats plugin', () => {
|
|
38
|
+
it('starts from zeroed counters', () => {
|
|
39
|
+
const stats = reduce([]);
|
|
40
|
+
expect(stats.userMessages).toBe(0);
|
|
41
|
+
expect(stats.compactions).toBe(0);
|
|
42
|
+
expect(stats.cacheReadTokens).toBe(0);
|
|
43
|
+
expect(stats.cacheWriteTokens).toBe(0);
|
|
44
|
+
expect(stats.byAgent).toEqual({});
|
|
45
|
+
});
|
|
46
|
+
it('counts user chat messages', () => {
|
|
47
|
+
const stats = reduce([
|
|
48
|
+
spawn(orchestrator, 'orchestrator'),
|
|
49
|
+
withSessionId(sessionId, userChatEvents.create('user_chat_message_received', {
|
|
50
|
+
agentId: orchestrator,
|
|
51
|
+
messageId: ChatMessageId('m1'),
|
|
52
|
+
content: 'hello',
|
|
53
|
+
timestamp: 1,
|
|
54
|
+
})),
|
|
55
|
+
withSessionId(sessionId, userChatEvents.create('user_chat_message_received', {
|
|
56
|
+
agentId: orchestrator,
|
|
57
|
+
messageId: ChatMessageId('m2'),
|
|
58
|
+
content: 'again',
|
|
59
|
+
timestamp: 2,
|
|
60
|
+
})),
|
|
61
|
+
]);
|
|
62
|
+
expect(stats.userMessages).toBe(2);
|
|
63
|
+
});
|
|
64
|
+
it('counts context compactions', () => {
|
|
65
|
+
const stats = reduce([
|
|
66
|
+
spawn(orchestrator, 'orchestrator'),
|
|
67
|
+
withSessionId(sessionId, contextEvents.create('context_compacted', {
|
|
68
|
+
agentId: orchestrator,
|
|
69
|
+
compactedContent: 'summary',
|
|
70
|
+
newConversationHistory: [],
|
|
71
|
+
originalTokens: 1000,
|
|
72
|
+
compactedTokens: 100,
|
|
73
|
+
messagesRemoved: 8,
|
|
74
|
+
})),
|
|
75
|
+
]);
|
|
76
|
+
expect(stats.compactions).toBe(1);
|
|
77
|
+
});
|
|
78
|
+
it('sums cache read and write tokens across main and auxiliary inferences', () => {
|
|
79
|
+
const stats = reduce([
|
|
80
|
+
spawn(orchestrator, 'orchestrator'),
|
|
81
|
+
inference(orchestrator, metrics({ cachedTokens: 800, cacheWriteTokens: 200 })),
|
|
82
|
+
withSessionId(sessionId, llmEvents.create('auxiliary_inference_completed', {
|
|
83
|
+
agentId: orchestrator,
|
|
84
|
+
metrics: metrics({ cachedTokens: 50, cacheWriteTokens: 10 }),
|
|
85
|
+
})),
|
|
86
|
+
// Providers that report no cache usage must not break the totals.
|
|
87
|
+
inference(orchestrator, metrics()),
|
|
88
|
+
]);
|
|
89
|
+
expect(stats.cacheReadTokens).toBe(850);
|
|
90
|
+
expect(stats.cacheWriteTokens).toBe(210);
|
|
91
|
+
expect(stats.llmCalls).toBe(3);
|
|
92
|
+
});
|
|
93
|
+
it('breaks LLM usage down by agent definition name', () => {
|
|
94
|
+
const stats = reduce([
|
|
95
|
+
spawn(orchestrator, 'orchestrator'),
|
|
96
|
+
spawn(worker, 'coder'),
|
|
97
|
+
inference(orchestrator, metrics({ promptTokens: 100, completionTokens: 20, cost: 0.5 })),
|
|
98
|
+
inference(worker, metrics({ promptTokens: 300, completionTokens: 40, cost: 1.25 })),
|
|
99
|
+
inference(worker, metrics({ promptTokens: 200, completionTokens: 10, cost: 0.25 })),
|
|
100
|
+
withSessionId(sessionId, llmEvents.create('auxiliary_inference_completed', {
|
|
101
|
+
agentId: worker,
|
|
102
|
+
metrics: metrics({ promptTokens: 10, completionTokens: 1, cost: 0.05 }),
|
|
103
|
+
})),
|
|
104
|
+
]);
|
|
105
|
+
expect(stats.byAgent).toEqual({
|
|
106
|
+
orchestrator: { llmCalls: 1, promptTokens: 100, completionTokens: 20, totalCost: 0.5 },
|
|
107
|
+
coder: { llmCalls: 3, promptTokens: 510, completionTokens: 51, totalCost: 1.55 },
|
|
108
|
+
});
|
|
109
|
+
expect(stats.totalCost).toBeCloseTo(2.05, 10);
|
|
110
|
+
});
|
|
111
|
+
it('keeps existing counters working', () => {
|
|
112
|
+
const stats = reduce([
|
|
113
|
+
spawn(orchestrator, 'orchestrator'),
|
|
114
|
+
inference(orchestrator, metrics()),
|
|
115
|
+
withSessionId(sessionId, llmEvents.create('inference_failed', { agentId: orchestrator, error: 'boom' })),
|
|
116
|
+
withSessionId(sessionId, toolEvents.create('tool_started', {
|
|
117
|
+
agentId: orchestrator,
|
|
118
|
+
toolCallId: ToolCallId('tc-1'),
|
|
119
|
+
toolName: 'read_file',
|
|
120
|
+
input: {},
|
|
121
|
+
})),
|
|
122
|
+
withSessionId(sessionId, toolEvents.create('tool_failed', {
|
|
123
|
+
agentId: orchestrator,
|
|
124
|
+
toolCallId: ToolCallId('tc-1'),
|
|
125
|
+
error: 'ENOENT',
|
|
126
|
+
})),
|
|
127
|
+
]);
|
|
128
|
+
expect(stats.agentCount).toBe(1);
|
|
129
|
+
expect(stats.llmCalls).toBe(1);
|
|
130
|
+
expect(stats.llmErrors).toBe(1);
|
|
131
|
+
expect(stats.toolCalls).toBe(1);
|
|
132
|
+
expect(stats.toolErrors).toBe(1);
|
|
133
|
+
expect(stats.totalTokens).toBe(120);
|
|
134
|
+
expect(stats.promptTokens).toBe(100);
|
|
135
|
+
expect(stats.completionTokens).toBe(20);
|
|
136
|
+
expect(stats.byProvider.anthropic).toEqual({
|
|
137
|
+
llmCalls: 1,
|
|
138
|
+
totalTokens: 120,
|
|
139
|
+
promptTokens: 100,
|
|
140
|
+
completionTokens: 20,
|
|
141
|
+
totalCost: 0.5,
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
//# sourceMappingURL=plugin.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.test.js","sourceRoot":"","sources":["../../../src/plugins/session-stats/plugin.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -17,5 +17,5 @@ export declare const errorHandler: (error: Error, c: Context<AppEnv>) => Respons
|
|
|
17
17
|
type: string;
|
|
18
18
|
message: string;
|
|
19
19
|
};
|
|
20
|
-
}, 100 | 500 | 404 | 409 | 400 | 403 | -1 |
|
|
20
|
+
}, 100 | 500 | 404 | 409 | 400 | 403 | -1 | 200 | 429 | 401 | 300 | 501 | 201 | 510 | 102 | 103 | 202 | 203 | 206 | 207 | 208 | 226 | 301 | 302 | 303 | 305 | 306 | 307 | 308 | 402 | 405 | 406 | 407 | 408 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 431 | 451 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 511, "json">;
|
|
21
21
|
//# sourceMappingURL=error-handler.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roj-ai/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.34",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
"type-check": "tsc --noEmit"
|
|
136
136
|
},
|
|
137
137
|
"dependencies": {
|
|
138
|
-
"@roj-ai/transport": "^0.1.
|
|
138
|
+
"@roj-ai/transport": "^0.1.34",
|
|
139
139
|
"@hono/zod-validator": "0.7.6",
|
|
140
140
|
"hono": "4.12.5",
|
|
141
141
|
"ignore": "7.0.5",
|
package/src/bootstrap.ts
CHANGED
|
@@ -37,6 +37,7 @@ import { llmDebugPlugin } from './plugins/llm-debug/plugin.js'
|
|
|
37
37
|
import { logsPlugin } from './plugins/logs/index.js'
|
|
38
38
|
import { mailboxPlugin } from './plugins/mailbox/plugin.js'
|
|
39
39
|
import { servicePlugin } from './plugins/services/plugin.js'
|
|
40
|
+
import { ServicePidRegistry } from './plugins/services/pid-registry.js'
|
|
40
41
|
import { PortPool } from './plugins/services/port-pool.js'
|
|
41
42
|
import { presetsPlugin, sessionLifecyclePlugin } from './plugins/session-lifecycle/index.js'
|
|
42
43
|
import { sessionStatsPlugin } from './plugins/session-stats/index.js'
|
|
@@ -89,6 +90,8 @@ export interface Services {
|
|
|
89
90
|
config: Config
|
|
90
91
|
/** Global port pool shared across all sessions */
|
|
91
92
|
portPool: PortPool
|
|
93
|
+
/** Durable record of spawned service processes, swept once at boot. */
|
|
94
|
+
pidRegistry: ServicePidRegistry
|
|
92
95
|
/** Preprocessor registry for upload content extraction */
|
|
93
96
|
preprocessorRegistry: PreprocessorRegistry
|
|
94
97
|
/** Host-environment adapters (filesystem, process). */
|
|
@@ -118,6 +121,8 @@ export function bootstrap(config: Config, userConfig: RojConfig, platform: Platf
|
|
|
118
121
|
const toolExecutor = new ToolExecutorImpl(logger)
|
|
119
122
|
const dataFileStore = new SessionFileStore(config.dataPath, undefined, false, platform.fs, 'session')
|
|
120
123
|
const portPool = new PortPool()
|
|
124
|
+
// Swept by the server before it serves — see ServicePidRegistry.sweepOrphans.
|
|
125
|
+
const pidRegistry = new ServicePidRegistry(platform.fs, logger, config.dataPath)
|
|
121
126
|
|
|
122
127
|
const preprocessorRegistry = new PreprocessorRegistry()
|
|
123
128
|
const imageClassifierGate = new Semaphore(config.imageClassifierConcurrency ?? 10)
|
|
@@ -148,6 +153,7 @@ export function bootstrap(config: Config, userConfig: RojConfig, platform: Platf
|
|
|
148
153
|
dataFileStore,
|
|
149
154
|
config,
|
|
150
155
|
portPool,
|
|
156
|
+
pidRegistry,
|
|
151
157
|
preprocessorRegistry,
|
|
152
158
|
platform,
|
|
153
159
|
}
|
|
@@ -253,6 +259,7 @@ export function createSystemFromServices(
|
|
|
253
259
|
preprocessorRegistry: services.preprocessorRegistry,
|
|
254
260
|
llmLogger: services.llmLogger,
|
|
255
261
|
portPool: services.portPool,
|
|
262
|
+
pidRegistry: services.pidRegistry,
|
|
256
263
|
platform: services.platform,
|
|
257
264
|
})
|
|
258
265
|
}
|
|
@@ -34,6 +34,7 @@ import type { Result } from '~/lib/utils/result.js'
|
|
|
34
34
|
import { Err, Ok } from '~/lib/utils/result.js'
|
|
35
35
|
import type { SpawnableAgentInfo } from '~/plugins/agents/index.js'
|
|
36
36
|
import { getAgentUnconsumedMailbox, selectMailboxState } from '~/plugins/mailbox/query.js'
|
|
37
|
+
import type { ServicePidRegistry } from '~/plugins/services/pid-registry.js'
|
|
37
38
|
import type { PortPool } from '~/plugins/services/port-pool.js'
|
|
38
39
|
import type { ServiceConfig } from '~/plugins/services/schema.js'
|
|
39
40
|
import { selectSessionStats, type SessionStatsState } from '~/plugins/session-stats/index.js'
|
|
@@ -77,6 +78,7 @@ export interface SessionManagerOptions {
|
|
|
77
78
|
preprocessorRegistry?: PreprocessorRegistry
|
|
78
79
|
llmLogger?: LLMLogger
|
|
79
80
|
portPool?: PortPool
|
|
81
|
+
pidRegistry?: ServicePidRegistry
|
|
80
82
|
/** Host-environment adapters (filesystem, process). */
|
|
81
83
|
platform: Platform
|
|
82
84
|
systemPlugins?: readonly PluginDefinition<string, any, any, any, any>[]
|
|
@@ -108,6 +110,7 @@ export class SessionManager {
|
|
|
108
110
|
private readonly preprocessorRegistry?: PreprocessorRegistry
|
|
109
111
|
private readonly llmLogger?: LLMLogger
|
|
110
112
|
private readonly portPool?: PortPool
|
|
113
|
+
private readonly pidRegistry?: ServicePidRegistry
|
|
111
114
|
private readonly platform: Platform
|
|
112
115
|
private readonly systemPlugins: readonly PluginDefinition<string, any, any, any, any>[]
|
|
113
116
|
|
|
@@ -124,6 +127,7 @@ export class SessionManager {
|
|
|
124
127
|
this.preprocessorRegistry = options.preprocessorRegistry
|
|
125
128
|
this.llmLogger = options.llmLogger
|
|
126
129
|
this.portPool = options.portPool
|
|
130
|
+
this.pidRegistry = options.pidRegistry
|
|
127
131
|
this.platform = options.platform
|
|
128
132
|
this.systemPlugins = options.systemPlugins ?? []
|
|
129
133
|
this.managerMethods = this.collectManagerMethods()
|
|
@@ -774,7 +778,7 @@ export class SessionManager {
|
|
|
774
778
|
}
|
|
775
779
|
}
|
|
776
780
|
if (servicesByType.size > 0 && this.portPool) {
|
|
777
|
-
configs.set('services', { services: [...servicesByType.values()], portPool: this.portPool })
|
|
781
|
+
configs.set('services', { services: [...servicesByType.values()], portPool: this.portPool, pidRegistry: this.pidRegistry })
|
|
778
782
|
}
|
|
779
783
|
|
|
780
784
|
return configs
|
package/src/core/system.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import type z4 from 'zod/v4'
|
|
11
11
|
import type { Logger } from '~/lib/logger/logger.js'
|
|
12
12
|
import type { Platform } from '~/platform/index.js'
|
|
13
|
+
import type { ServicePidRegistry } from '~/plugins/services/pid-registry.js'
|
|
13
14
|
import type { PortPool } from '~/plugins/services/port-pool.js'
|
|
14
15
|
import type { PreprocessorRegistry } from '~/plugins/uploads/preprocessor.js'
|
|
15
16
|
import type { EventStore } from './events/event-store.js'
|
|
@@ -120,6 +121,7 @@ export interface CreateSystemOptions<TPlugins extends readonly PluginDefinition<
|
|
|
120
121
|
preprocessorRegistry?: PreprocessorRegistry
|
|
121
122
|
llmLogger?: LLMLogger
|
|
122
123
|
portPool?: PortPool
|
|
124
|
+
pidRegistry?: ServicePidRegistry
|
|
123
125
|
/** Host-environment adapters (filesystem, process). */
|
|
124
126
|
platform: Platform
|
|
125
127
|
}
|
|
@@ -145,6 +147,7 @@ export function createSystem<const TPlugins extends readonly PluginDefinition<st
|
|
|
145
147
|
preprocessorRegistry,
|
|
146
148
|
llmLogger,
|
|
147
149
|
portPool,
|
|
150
|
+
pidRegistry,
|
|
148
151
|
platform,
|
|
149
152
|
} = options
|
|
150
153
|
|
|
@@ -167,6 +170,7 @@ export function createSystem<const TPlugins extends readonly PluginDefinition<st
|
|
|
167
170
|
preprocessorRegistry,
|
|
168
171
|
llmLogger,
|
|
169
172
|
portPool,
|
|
173
|
+
pidRegistry,
|
|
170
174
|
platform,
|
|
171
175
|
systemPlugins: [...plugins],
|
|
172
176
|
})
|
package/src/index.ts
CHANGED
|
@@ -99,7 +99,7 @@ export type { EvictionAgentConfig, EvictionConfig } from '~/plugins/result-evict
|
|
|
99
99
|
export { servicePlugin } from '~/plugins/services/plugin.js'
|
|
100
100
|
export type { ServiceAgentConfig, ServiceStatusChangedEvent } from '~/plugins/services/plugin.js'
|
|
101
101
|
export { selectSessionStats, sessionStatsPlugin } from '~/plugins/session-stats/plugin.js'
|
|
102
|
-
export type { SessionStatsState } from '~/plugins/session-stats/plugin.js'
|
|
102
|
+
export type { AgentStats, ProviderStats, SessionStatsState } from '~/plugins/session-stats/plugin.js'
|
|
103
103
|
export { sessionStatePlugin } from '~/plugins/session-state/plugin.js'
|
|
104
104
|
export type { SessionStatePluginConfig } from '~/plugins/session-state/plugin.js'
|
|
105
105
|
export { presetsPlugin, sessionLifecyclePlugin } from '~/plugins/session-lifecycle/plugin.js'
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import { spawn } from 'node:child_process'
|
|
3
|
+
import { readdir, readFile, mkdtemp, rm, writeFile, mkdir } from 'node:fs/promises'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { silentLogger } from '~/lib/logger/logger.js'
|
|
7
|
+
import { createNodePlatform } from '~/testing/node-platform.js'
|
|
8
|
+
import { ServicePidRegistry } from './pid-registry.js'
|
|
9
|
+
import { getProcessStartTime } from './service.js'
|
|
10
|
+
|
|
11
|
+
const fs = createNodePlatform().fs
|
|
12
|
+
|
|
13
|
+
async function withDataDir<T>(fn: (dataPath: string) => Promise<T>): Promise<T> {
|
|
14
|
+
const dataPath = await mkdtemp(join(tmpdir(), 'pid-registry-'))
|
|
15
|
+
try {
|
|
16
|
+
return await fn(dataPath)
|
|
17
|
+
} finally {
|
|
18
|
+
await rm(dataPath, { recursive: true, force: true })
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A detached `sleep`, so `kill(-pid)` has a real process group to take down. */
|
|
23
|
+
function spawnVictim(): { pid: number; exited: Promise<void> } {
|
|
24
|
+
const child = spawn('/bin/sh', ['-c', 'sleep 60'], { detached: true, stdio: 'ignore' })
|
|
25
|
+
child.unref()
|
|
26
|
+
if (!child.pid) throw new Error('failed to spawn victim')
|
|
27
|
+
const exited = new Promise<void>(resolve => child.on('close', () => resolve()))
|
|
28
|
+
return { pid: child.pid, exited }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isAlive(pid: number): boolean {
|
|
32
|
+
try {
|
|
33
|
+
process.kill(pid, 0)
|
|
34
|
+
return true
|
|
35
|
+
} catch {
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A pid the kernel cannot have handed out, so it reads as a dead agent. */
|
|
41
|
+
async function unusablePid(): Promise<number> {
|
|
42
|
+
const max = Number((await readFile('/proc/sys/kernel/pid_max', 'utf-8')).trim())
|
|
43
|
+
return max + 1
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function writeRecord(dataPath: string, record: Record<string, unknown>): Promise<string> {
|
|
47
|
+
const dir = join(dataPath, 'service-pids')
|
|
48
|
+
await mkdir(dir, { recursive: true })
|
|
49
|
+
const path = join(dir, `${record.sessionId}__${record.serviceType}.json`)
|
|
50
|
+
await writeFile(path, JSON.stringify(record))
|
|
51
|
+
return path
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const makeRegistry = (dataPath: string) => new ServicePidRegistry(fs, silentLogger, dataPath)
|
|
55
|
+
|
|
56
|
+
describe('ServicePidRegistry', () => {
|
|
57
|
+
it('kills a service left behind by an agent that is gone, and drops the record', async () => {
|
|
58
|
+
await withDataDir(async dataPath => {
|
|
59
|
+
const victim = spawnVictim()
|
|
60
|
+
await writeRecord(dataPath, {
|
|
61
|
+
sessionId: 'sess-1',
|
|
62
|
+
serviceType: 'astro-dev',
|
|
63
|
+
pid: victim.pid,
|
|
64
|
+
pidStartTime: await getProcessStartTime(fs, victim.pid),
|
|
65
|
+
command: 'sleep 60',
|
|
66
|
+
agentPid: await unusablePid(),
|
|
67
|
+
agentStartTime: 12345,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
await makeRegistry(dataPath).sweepOrphans()
|
|
71
|
+
|
|
72
|
+
await victim.exited
|
|
73
|
+
expect(isAlive(victim.pid)).toBe(false)
|
|
74
|
+
expect(await readdir(join(dataPath, 'service-pids'))).toEqual([])
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('leaves a service whose agent is still running', async () => {
|
|
79
|
+
await withDataDir(async dataPath => {
|
|
80
|
+
const victim = spawnVictim()
|
|
81
|
+
try {
|
|
82
|
+
// This test process stands in for a second, still-live agent.
|
|
83
|
+
await writeRecord(dataPath, {
|
|
84
|
+
sessionId: 'sess-1',
|
|
85
|
+
serviceType: 'astro-dev',
|
|
86
|
+
pid: victim.pid,
|
|
87
|
+
pidStartTime: await getProcessStartTime(fs, victim.pid),
|
|
88
|
+
command: 'sleep 60',
|
|
89
|
+
agentPid: process.pid,
|
|
90
|
+
agentStartTime: await getProcessStartTime(fs, process.pid),
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
await makeRegistry(dataPath).sweepOrphans()
|
|
94
|
+
|
|
95
|
+
expect(isAlive(victim.pid)).toBe(true)
|
|
96
|
+
expect(await readdir(join(dataPath, 'service-pids'))).toHaveLength(1)
|
|
97
|
+
} finally {
|
|
98
|
+
process.kill(-victim.pid, 'SIGKILL')
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('refuses to kill when the pid has been recycled', async () => {
|
|
104
|
+
await withDataDir(async dataPath => {
|
|
105
|
+
const victim = spawnVictim()
|
|
106
|
+
try {
|
|
107
|
+
const actual = await getProcessStartTime(fs, victim.pid)
|
|
108
|
+
await writeRecord(dataPath, {
|
|
109
|
+
sessionId: 'sess-1',
|
|
110
|
+
serviceType: 'astro-dev',
|
|
111
|
+
pid: victim.pid,
|
|
112
|
+
// Deliberately not the process now holding this pid.
|
|
113
|
+
pidStartTime: (actual ?? 0) + 999,
|
|
114
|
+
command: 'sleep 60',
|
|
115
|
+
agentPid: await unusablePid(),
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
await makeRegistry(dataPath).sweepOrphans()
|
|
119
|
+
|
|
120
|
+
expect(isAlive(victim.pid)).toBe(true)
|
|
121
|
+
// The record still goes — whatever holds that pid now, it is not ours to track.
|
|
122
|
+
expect(await readdir(join(dataPath, 'service-pids'))).toEqual([])
|
|
123
|
+
} finally {
|
|
124
|
+
process.kill(-victim.pid, 'SIGKILL')
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('never sweeps what this very process started', async () => {
|
|
130
|
+
await withDataDir(async dataPath => {
|
|
131
|
+
const registry = makeRegistry(dataPath)
|
|
132
|
+
const victim = spawnVictim()
|
|
133
|
+
try {
|
|
134
|
+
await registry.record({
|
|
135
|
+
sessionId: 'sess-1',
|
|
136
|
+
serviceType: 'astro-dev',
|
|
137
|
+
pid: victim.pid,
|
|
138
|
+
pidStartTime: await getProcessStartTime(fs, victim.pid),
|
|
139
|
+
command: 'sleep 60',
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
await registry.sweepOrphans()
|
|
143
|
+
|
|
144
|
+
expect(isAlive(victim.pid)).toBe(true)
|
|
145
|
+
} finally {
|
|
146
|
+
process.kill(-victim.pid, 'SIGKILL')
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('drops an unreadable record instead of throwing', async () => {
|
|
152
|
+
await withDataDir(async dataPath => {
|
|
153
|
+
const dir = join(dataPath, 'service-pids')
|
|
154
|
+
await mkdir(dir, { recursive: true })
|
|
155
|
+
await writeFile(join(dir, 'sess-1__astro-dev.json'), 'not json')
|
|
156
|
+
|
|
157
|
+
await makeRegistry(dataPath).sweepOrphans()
|
|
158
|
+
|
|
159
|
+
expect(await readdir(dir)).toEqual([])
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('does nothing when no service has ever been recorded', async () => {
|
|
164
|
+
await withDataDir(async dataPath => {
|
|
165
|
+
await makeRegistry(dataPath).sweepOrphans()
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('forget removes the record for a service that has exited', async () => {
|
|
170
|
+
await withDataDir(async dataPath => {
|
|
171
|
+
const registry = makeRegistry(dataPath)
|
|
172
|
+
await registry.record({ sessionId: 'sess-1', serviceType: 'astro-dev', pid: 1, pidStartTime: 1, command: 'x' })
|
|
173
|
+
expect(await readdir(join(dataPath, 'service-pids'))).toHaveLength(1)
|
|
174
|
+
|
|
175
|
+
await registry.forget('sess-1', 'astro-dev')
|
|
176
|
+
|
|
177
|
+
expect(await readdir(join(dataPath, 'service-pids'))).toEqual([])
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
})
|