@roj-ai/sdk 0.1.27 → 0.1.29
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 +2 -0
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/core/sessions/session.d.ts.map +1 -1
- package/dist/core/sessions/session.js +6 -2
- package/dist/core/sessions/session.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/plugins/resources/index.d.ts +1 -1
- package/dist/plugins/resources/index.d.ts.map +1 -1
- package/dist/plugins/resources/plugin.d.ts +7 -1
- package/dist/plugins/resources/plugin.d.ts.map +1 -1
- package/dist/plugins/resources/plugin.js +25 -5
- package/dist/plugins/resources/plugin.js.map +1 -1
- package/dist/plugins/resources/resources.integration.test.d.ts +2 -0
- package/dist/plugins/resources/resources.integration.test.d.ts.map +1 -0
- package/dist/plugins/resources/resources.integration.test.js +62 -0
- package/dist/plugins/resources/resources.integration.test.js.map +1 -0
- package/dist/plugins/resources/state.d.ts +2 -0
- package/dist/plugins/resources/state.d.ts.map +1 -1
- package/dist/plugins/resources/state.js +1 -0
- package/dist/plugins/resources/state.js.map +1 -1
- package/dist/plugins/services/plugin.d.ts +4 -0
- package/dist/plugins/services/plugin.d.ts.map +1 -1
- package/dist/plugins/services/plugin.js +38 -10
- package/dist/plugins/services/plugin.js.map +1 -1
- package/dist/plugins/services/prompt.d.ts.map +1 -1
- package/dist/plugins/services/prompt.js +3 -0
- package/dist/plugins/services/prompt.js.map +1 -1
- package/dist/plugins/services/schema.d.ts +46 -4
- package/dist/plugins/services/schema.d.ts.map +1 -1
- package/dist/plugins/services/service.d.ts +11 -1
- package/dist/plugins/services/service.d.ts.map +1 -1
- package/dist/plugins/services/service.js +182 -46
- package/dist/plugins/services/service.js.map +1 -1
- package/dist/plugins/services/services.integration.test.js +240 -3
- package/dist/plugins/services/services.integration.test.js.map +1 -1
- package/dist/testing/preset-helpers.d.ts +1 -0
- package/dist/testing/preset-helpers.d.ts.map +1 -1
- package/dist/testing/preset-helpers.js +1 -0
- package/dist/testing/preset-helpers.js.map +1 -1
- package/package.json +2 -2
- package/src/core/sessions/session.ts +7 -3
- package/src/index.ts +8 -2
- package/src/plugins/resources/index.ts +1 -1
- package/src/plugins/resources/plugin.ts +35 -6
- package/src/plugins/resources/resources.integration.test.ts +68 -0
- package/src/plugins/resources/state.ts +2 -0
- package/src/plugins/services/plugin.ts +36 -10
- package/src/plugins/services/prompt.ts +3 -0
- package/src/plugins/services/schema.ts +48 -4
- package/src/plugins/services/service.ts +202 -52
- package/src/plugins/services/services.integration.test.ts +272 -4
- package/src/testing/preset-helpers.ts +2 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it } from 'bun:test'
|
|
2
|
-
import { readFile, rm } from 'node:fs/promises'
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm } from 'node:fs/promises'
|
|
3
3
|
import { tmpdir } from 'node:os'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
import { MemoryEventStore } from '~/core/events/memory.js'
|
|
@@ -13,7 +13,8 @@ import { createTestPreset, TestHarness } from '~/testing/index.js'
|
|
|
13
13
|
import { serviceEvents, servicePlugin } from './plugin.js'
|
|
14
14
|
import type { ServiceAgentConfig, ServicePluginConfig } from './plugin.js'
|
|
15
15
|
import { PortPool } from './port-pool.js'
|
|
16
|
-
import
|
|
16
|
+
import { buildServiceStatusMessage } from './prompt.js'
|
|
17
|
+
import type { ServiceCommandArgs, ServiceConfig, ServiceCwdArgs, ServiceEntry, ServiceStatus } from './schema.js'
|
|
17
18
|
import { ServiceExecutor } from './service.js'
|
|
18
19
|
|
|
19
20
|
// ============================================================================
|
|
@@ -278,6 +279,84 @@ describe('services plugin', () => {
|
|
|
278
279
|
const serviceState = selectPluginState<Map<string, ServiceEntry>>(session.state, 'services')?.get('callback-service')
|
|
279
280
|
expect(serviceState!.port).toBeGreaterThanOrEqual(10000)
|
|
280
281
|
})
|
|
282
|
+
|
|
283
|
+
it('resolves cwd, command, and env callbacks with runtime context', async () => {
|
|
284
|
+
const workspaceDir = await mkdtemp(join(tmpdir(), 'roj-svc-runtime-'))
|
|
285
|
+
const webDir = join(workspaceDir, 'packages', 'web')
|
|
286
|
+
await mkdir(webDir, { recursive: true })
|
|
287
|
+
|
|
288
|
+
let cwdArgs: ServiceCwdArgs | undefined
|
|
289
|
+
let commandArgs: ServiceCommandArgs | undefined
|
|
290
|
+
let envArgs: ServiceCommandArgs | undefined
|
|
291
|
+
|
|
292
|
+
const runtimeService: ServiceConfig = {
|
|
293
|
+
type: 'runtime',
|
|
294
|
+
description: 'Service with runtime resolvers',
|
|
295
|
+
cwd: (args) => {
|
|
296
|
+
cwdArgs = args
|
|
297
|
+
return 'packages/web'
|
|
298
|
+
},
|
|
299
|
+
command: async (args) => {
|
|
300
|
+
commandArgs = args
|
|
301
|
+
return `printf "%s" "$RUNTIME_MARKER" > marker.txt && echo "READY ${args.port}" && sleep 60`
|
|
302
|
+
},
|
|
303
|
+
env: (args) => {
|
|
304
|
+
envArgs = args
|
|
305
|
+
return {
|
|
306
|
+
RUNTIME_MARKER: `${args.sessionId}|${args.workspaceDir}|${args.cwd}|${args.port}`,
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
readyPattern: 'READY',
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
const portPool = new PortPool()
|
|
314
|
+
const harness = createServicesHarness({
|
|
315
|
+
presets: [createServicesPreset([runtimeService], ['runtime'], portPool, { workspaceDir })],
|
|
316
|
+
llmProvider: MockLLMProvider.withFixedResponse({ content: 'Ok', toolCalls: [] }),
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
const session = await harness.createSession('test')
|
|
320
|
+
const entryAgentId = session.getEntryAgentId()!
|
|
321
|
+
const startResult = await session.callPluginMethod('services.start', {
|
|
322
|
+
sessionId: String(session.sessionId),
|
|
323
|
+
agentId: String(entryAgentId),
|
|
324
|
+
serviceType: 'runtime',
|
|
325
|
+
})
|
|
326
|
+
expect(startResult.ok).toBe(true)
|
|
327
|
+
await waitForServiceStatus(session, 'runtime', 'ready')
|
|
328
|
+
|
|
329
|
+
const serviceState = selectPluginState<Map<string, ServiceEntry>>(session.state, 'services')?.get('runtime')
|
|
330
|
+
expect(cwdArgs?.workspaceDir).toBe(workspaceDir)
|
|
331
|
+
expect(cwdArgs?.sessionId).toBe(String(session.sessionId))
|
|
332
|
+
expect(commandArgs?.cwd).toBe(webDir)
|
|
333
|
+
expect(commandArgs?.workspaceDir).toBe(workspaceDir)
|
|
334
|
+
expect(envArgs?.cwd).toBe(webDir)
|
|
335
|
+
expect(serviceState?.cwd).toBe(webDir)
|
|
336
|
+
expect(serviceState?.command).toContain('RUNTIME_MARKER')
|
|
337
|
+
|
|
338
|
+
const marker = await readFile(join(webDir, 'marker.txt'), 'utf-8')
|
|
339
|
+
expect(marker).toContain(String(session.sessionId))
|
|
340
|
+
expect(marker).toContain(workspaceDir)
|
|
341
|
+
expect(marker).toContain(webDir)
|
|
342
|
+
|
|
343
|
+
const statusResult = await session.callPluginMethod('services.status', {
|
|
344
|
+
sessionId: String(session.sessionId),
|
|
345
|
+
agentId: String(entryAgentId),
|
|
346
|
+
serviceType: 'runtime',
|
|
347
|
+
})
|
|
348
|
+
expect(statusResult).toMatchObject({
|
|
349
|
+
ok: true,
|
|
350
|
+
value: {
|
|
351
|
+
status: 'ready',
|
|
352
|
+
cwd: webDir,
|
|
353
|
+
command: serviceState?.command,
|
|
354
|
+
},
|
|
355
|
+
})
|
|
356
|
+
} finally {
|
|
357
|
+
await rm(workspaceDir, { recursive: true, force: true })
|
|
358
|
+
}
|
|
359
|
+
})
|
|
281
360
|
})
|
|
282
361
|
|
|
283
362
|
// =========================================================================
|
|
@@ -379,6 +458,35 @@ describe('services plugin', () => {
|
|
|
379
458
|
const autoEvents = events.filter((e) => e.serviceType === 'auto-start')
|
|
380
459
|
expect(autoEvents.length).toBeGreaterThanOrEqual(1)
|
|
381
460
|
})
|
|
461
|
+
|
|
462
|
+
it('availableWhen false skips auto-start and explicit start returns an error', async () => {
|
|
463
|
+
const unavailableService: ServiceConfig = {
|
|
464
|
+
type: 'unavailable',
|
|
465
|
+
description: 'Unavailable service',
|
|
466
|
+
command: 'sleep 60',
|
|
467
|
+
autoStart: true,
|
|
468
|
+
availableWhen: () => false,
|
|
469
|
+
}
|
|
470
|
+
const portPool = new PortPool()
|
|
471
|
+
const harness = createServicesHarness({
|
|
472
|
+
presets: [createServicesPreset([unavailableService], ['unavailable'], portPool)],
|
|
473
|
+
llmProvider: MockLLMProvider.withFixedResponse({ content: 'Ok', toolCalls: [] }),
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
const session = await harness.createSession('test')
|
|
477
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
478
|
+
|
|
479
|
+
const events = await session.getEventsByType(serviceEvents, 'service_status_changed')
|
|
480
|
+
expect(events.some((event) => event.serviceType === 'unavailable')).toBe(false)
|
|
481
|
+
|
|
482
|
+
const entryAgentId = session.getEntryAgentId()!
|
|
483
|
+
const result = await session.callPluginMethod('services.start', {
|
|
484
|
+
sessionId: String(session.sessionId),
|
|
485
|
+
agentId: String(entryAgentId),
|
|
486
|
+
serviceType: 'unavailable',
|
|
487
|
+
})
|
|
488
|
+
expect(result.ok).toBe(false)
|
|
489
|
+
})
|
|
382
490
|
})
|
|
383
491
|
|
|
384
492
|
// =========================================================================
|
|
@@ -439,6 +547,38 @@ describe('services plugin', () => {
|
|
|
439
547
|
const serviceState = selectPluginState<Map<string, ServiceEntry>>(session.state, 'services')?.get('ready-service')
|
|
440
548
|
expect(serviceState!.status).toBe('ready')
|
|
441
549
|
})
|
|
550
|
+
|
|
551
|
+
it('readyWhen can mark a service ready without output pattern', async () => {
|
|
552
|
+
let checks = 0
|
|
553
|
+
const readyWhenService: ServiceConfig = {
|
|
554
|
+
type: 'ready-when',
|
|
555
|
+
description: 'Service with callback readiness',
|
|
556
|
+
command: 'sleep 60',
|
|
557
|
+
readyWhen: () => {
|
|
558
|
+
checks += 1
|
|
559
|
+
return checks > 1
|
|
560
|
+
},
|
|
561
|
+
readyCheckIntervalMs: 20,
|
|
562
|
+
startupTimeoutMs: 1000,
|
|
563
|
+
}
|
|
564
|
+
const portPool = new PortPool()
|
|
565
|
+
const harness = createServicesHarness({
|
|
566
|
+
presets: [createServicesPreset([readyWhenService], ['ready-when'], portPool)],
|
|
567
|
+
llmProvider: MockLLMProvider.withFixedResponse({ content: 'Ok', toolCalls: [] }),
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
const session = await harness.createSession('test')
|
|
571
|
+
const entryAgentId = session.getEntryAgentId()!
|
|
572
|
+
const result = await session.callPluginMethod('services.start', {
|
|
573
|
+
sessionId: String(session.sessionId),
|
|
574
|
+
agentId: String(entryAgentId),
|
|
575
|
+
serviceType: 'ready-when',
|
|
576
|
+
})
|
|
577
|
+
expect(result.ok).toBe(true)
|
|
578
|
+
|
|
579
|
+
await waitForServiceStatus(session, 'ready-when', 'ready')
|
|
580
|
+
expect(checks).toBeGreaterThan(1)
|
|
581
|
+
})
|
|
442
582
|
})
|
|
443
583
|
|
|
444
584
|
// =========================================================================
|
|
@@ -658,8 +798,8 @@ describe('services plugin', () => {
|
|
|
658
798
|
}
|
|
659
799
|
|
|
660
800
|
const observed: Array<{ status: ServiceStatus; port?: number }> = []
|
|
661
|
-
executor.onStatusChanged = (_sessionId, _serviceType, status,
|
|
662
|
-
observed.push({ status, port })
|
|
801
|
+
executor.onStatusChanged = (_sessionId, _serviceType, status, details) => {
|
|
802
|
+
observed.push({ status, port: details.port })
|
|
663
803
|
}
|
|
664
804
|
|
|
665
805
|
try {
|
|
@@ -682,4 +822,132 @@ describe('services plugin', () => {
|
|
|
682
822
|
}
|
|
683
823
|
})
|
|
684
824
|
})
|
|
825
|
+
|
|
826
|
+
// =========================================================================
|
|
827
|
+
// agentVisible: false — infrastructure services hidden from agents
|
|
828
|
+
// =========================================================================
|
|
829
|
+
|
|
830
|
+
describe('agentVisible: false services', () => {
|
|
831
|
+
const hiddenService: ServiceConfig = {
|
|
832
|
+
type: 'hidden-svc',
|
|
833
|
+
description: 'Infra service driven by the platform, not agents',
|
|
834
|
+
command: 'sleep 60',
|
|
835
|
+
agentVisible: false,
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
it('auto-wired agent visibility excludes it: no tool mention, no status line, still method-controllable', async () => {
|
|
839
|
+
const portPool = new PortPool()
|
|
840
|
+
const base = createTestPreset({
|
|
841
|
+
plugins: [servicePlugin.configure({ services: [quickService, hiddenService], portPool })],
|
|
842
|
+
})
|
|
843
|
+
// Services attached on the agent definition (defineAgent-style auto-wiring),
|
|
844
|
+
// NOT via explicit configureAgent — exercises the getAgentConfig filter.
|
|
845
|
+
const preset = { ...base, orchestrator: { ...base.orchestrator, services: [quickService, hiddenService] } }
|
|
846
|
+
const harness = createServicesHarness({
|
|
847
|
+
presets: [preset],
|
|
848
|
+
llmProvider: MockLLMProvider.withFixedResponse({ content: 'Ok', toolCalls: [] }),
|
|
849
|
+
})
|
|
850
|
+
|
|
851
|
+
const session = await harness.createSession('test')
|
|
852
|
+
await session.sendAndWaitForIdle('Hi')
|
|
853
|
+
|
|
854
|
+
// Service tools exist (quick is visible) but never mention the hidden service
|
|
855
|
+
const lastRequest = harness.llmProvider.getLastRequest()
|
|
856
|
+
const serviceStart = lastRequest?.tools?.find((t) => t.name === 'service_start')
|
|
857
|
+
expect(serviceStart).toBeDefined()
|
|
858
|
+
expect(serviceStart?.description).toContain('quick')
|
|
859
|
+
expect(serviceStart?.description).not.toContain('hidden-svc')
|
|
860
|
+
|
|
861
|
+
// The session-context status block skips it too
|
|
862
|
+
const serialized = JSON.stringify(lastRequest?.messages ?? [])
|
|
863
|
+
expect(serialized).not.toContain('hidden-svc')
|
|
864
|
+
|
|
865
|
+
// Still fully controllable at the session level via services.* methods
|
|
866
|
+
const entryAgentId = session.getEntryAgentId()!
|
|
867
|
+
const startResult = await session.callPluginMethod('services.start', {
|
|
868
|
+
sessionId: String(session.sessionId),
|
|
869
|
+
agentId: String(entryAgentId),
|
|
870
|
+
serviceType: 'hidden-svc',
|
|
871
|
+
})
|
|
872
|
+
expect(startResult).toMatchObject({ ok: true })
|
|
873
|
+
await waitForServiceStatus(session, 'hidden-svc', 'ready')
|
|
874
|
+
|
|
875
|
+
const statusResult = await session.callPluginMethod('services.status', {
|
|
876
|
+
sessionId: String(session.sessionId),
|
|
877
|
+
agentId: String(entryAgentId),
|
|
878
|
+
serviceType: 'hidden-svc',
|
|
879
|
+
})
|
|
880
|
+
expect(statusResult).toMatchObject({ ok: true, value: { status: 'ready' } })
|
|
881
|
+
})
|
|
882
|
+
})
|
|
883
|
+
|
|
884
|
+
// =========================================================================
|
|
885
|
+
// Real host paths must not leak into agent-facing surfaces
|
|
886
|
+
// =========================================================================
|
|
887
|
+
|
|
888
|
+
describe('host path hygiene', () => {
|
|
889
|
+
it('status block omits cwd even when the entry tracks one', () => {
|
|
890
|
+
const message = buildServiceStatusMessage(
|
|
891
|
+
[{
|
|
892
|
+
serviceType: 'quick',
|
|
893
|
+
status: 'ready',
|
|
894
|
+
port: 4321,
|
|
895
|
+
cwd: '/home/user/sessions/0000-real-host-path/packages/web',
|
|
896
|
+
}],
|
|
897
|
+
[quickService],
|
|
898
|
+
)
|
|
899
|
+
expect(message).toContain('quick')
|
|
900
|
+
expect(message).toContain('port 4321')
|
|
901
|
+
expect(message).not.toContain('real-host-path')
|
|
902
|
+
expect(message).not.toContain('cwd')
|
|
903
|
+
})
|
|
904
|
+
|
|
905
|
+
it('service_status tool output strips cwd', async () => {
|
|
906
|
+
const portPool = new PortPool()
|
|
907
|
+
// Absolute cwd so the tracked entry actually carries a host path to leak.
|
|
908
|
+
const cwdService: ServiceConfig = {
|
|
909
|
+
type: 'cwd-svc',
|
|
910
|
+
description: 'Service with a resolved host cwd',
|
|
911
|
+
command: 'sleep 60',
|
|
912
|
+
cwd: tmpdir(),
|
|
913
|
+
}
|
|
914
|
+
const harness = createServicesHarness({
|
|
915
|
+
presets: [createServicesPreset([cwdService], ['cwd-svc'], portPool)],
|
|
916
|
+
llmProvider: MockLLMProvider.withSequence([
|
|
917
|
+
{
|
|
918
|
+
toolCalls: [{
|
|
919
|
+
id: ToolCallId('tc-start'),
|
|
920
|
+
name: 'service_start',
|
|
921
|
+
input: { serviceType: 'cwd-svc' },
|
|
922
|
+
}],
|
|
923
|
+
},
|
|
924
|
+
{
|
|
925
|
+
toolCalls: [{
|
|
926
|
+
id: ToolCallId('tc-status'),
|
|
927
|
+
name: 'service_status',
|
|
928
|
+
input: { serviceType: 'cwd-svc' },
|
|
929
|
+
}],
|
|
930
|
+
},
|
|
931
|
+
{ content: 'Done', toolCalls: [] },
|
|
932
|
+
]),
|
|
933
|
+
})
|
|
934
|
+
|
|
935
|
+
const session = await harness.createSession('test')
|
|
936
|
+
await session.sendAndWaitForIdle('Check the service')
|
|
937
|
+
await waitForServiceStatus(session, 'cwd-svc', 'ready')
|
|
938
|
+
|
|
939
|
+
// The service_status tool result (fed back as a message) must not carry cwd,
|
|
940
|
+
// while the session-level method keeps returning it for platform callers.
|
|
941
|
+
const serialized = JSON.stringify(harness.llmProvider.getLastRequest()?.messages ?? [])
|
|
942
|
+
expect(serialized).not.toContain('"cwd"')
|
|
943
|
+
|
|
944
|
+
const entryAgentId = session.getEntryAgentId()!
|
|
945
|
+
const statusResult = await session.callPluginMethod('services.status', {
|
|
946
|
+
sessionId: String(session.sessionId),
|
|
947
|
+
agentId: String(entryAgentId),
|
|
948
|
+
serviceType: 'cwd-svc',
|
|
949
|
+
})
|
|
950
|
+
expect(statusResult).toMatchObject({ ok: true, value: { cwd: expect.any(String) } })
|
|
951
|
+
})
|
|
952
|
+
})
|
|
685
953
|
})
|
|
@@ -12,6 +12,7 @@ export type TestAgentDefinition<TInput = unknown> = Omit<AgentDefinition<TInput>
|
|
|
12
12
|
*/
|
|
13
13
|
export function createTestPreset(overrides?: {
|
|
14
14
|
id?: string
|
|
15
|
+
workspaceDir?: string
|
|
15
16
|
orchestratorSystem?: string
|
|
16
17
|
agents?: TestAgentDefinition[]
|
|
17
18
|
plugins?: SessionPluginConfig[]
|
|
@@ -22,6 +23,7 @@ export function createTestPreset(overrides?: {
|
|
|
22
23
|
return {
|
|
23
24
|
id: overrides?.id ?? 'test',
|
|
24
25
|
name: 'Test Preset',
|
|
26
|
+
workspaceDir: overrides?.workspaceDir,
|
|
25
27
|
orchestrator: {
|
|
26
28
|
system: overrides?.orchestratorSystem ?? 'You are a test agent.',
|
|
27
29
|
model: ModelId('mock'),
|