@dxos/agent-runtime 0.13.0 → 0.14.0
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/lib/chunk-AgentService.mjs +189 -93
- package/dist/lib/chunk-AgentService.mjs.map +1 -1
- package/dist/lib/testing.mjs +3 -3
- package/dist/lib/testing.mjs.map +1 -1
- package/dist/types/src/agent-service/AgentService.d.ts +9 -0
- package/dist/types/src/agent-service/AgentService.d.ts.map +1 -1
- package/dist/types/src/agent-service/agent-process.d.ts +23 -5
- package/dist/types/src/agent-service/agent-process.d.ts.map +1 -1
- package/dist/types/src/agent-service/delegation-strategy.d.ts +5 -4
- package/dist/types/src/agent-service/delegation-strategy.d.ts.map +1 -1
- package/dist/types/src/agent-service/index.d.ts +1 -1
- package/dist/types/src/agent-service/index.d.ts.map +1 -1
- package/dist/types/src/agent-service/process-selection.test.d.ts +2 -0
- package/dist/types/src/agent-service/process-selection.test.d.ts.map +1 -0
- package/dist/types/src/agent-service/skill-hooks.test.d.ts +2 -0
- package/dist/types/src/agent-service/skill-hooks.test.d.ts.map +1 -0
- package/dist/types/src/testing/assistant-test-layer.d.ts +2 -2
- package/dist/types/src/testing/assistant-test-layer.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +17 -17
- package/src/agent-service/AgentService.test.ts +4 -4
- package/src/agent-service/AgentService.ts +63 -24
- package/src/agent-service/README.md +10 -10
- package/src/agent-service/agent-process.ts +201 -100
- package/src/agent-service/delegation-scripted.test.ts +4 -4
- package/src/agent-service/delegation-strategy.ts +9 -4
- package/src/agent-service/index.ts +1 -0
- package/src/agent-service/process-selection.test.ts +135 -0
- package/src/agent-service/queue-scripted.test.ts +39 -0
- package/src/agent-service/redelivery.test.ts +23 -1
- package/src/agent-service/skill-hooks.test.ts +182 -0
- package/src/testing/assistant-test-layer.ts +3 -2
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, it } from '@effect/vitest';
|
|
6
|
+
import * as Effect from 'effect/Effect';
|
|
7
|
+
import * as Exit from 'effect/Exit';
|
|
8
|
+
import * as Option from 'effect/Option';
|
|
9
|
+
import * as Schema from 'effect/Schema';
|
|
10
|
+
import { expect } from 'vitest';
|
|
11
|
+
|
|
12
|
+
import { HarnessControl } from '@dxos/assistant';
|
|
13
|
+
import * as Chat from '@dxos/assistant/Chat';
|
|
14
|
+
import * as AgentService from '@dxos/compute/AgentService';
|
|
15
|
+
import * as Operation from '@dxos/compute/Operation';
|
|
16
|
+
import * as Process from '@dxos/compute/Process';
|
|
17
|
+
import { Annotation, Database, Feed, Obj, Ref } from '@dxos/echo';
|
|
18
|
+
import { TestHelpers } from '@dxos/effect/testing';
|
|
19
|
+
import { Message } from '@dxos/types';
|
|
20
|
+
|
|
21
|
+
import { AssistantTestLayer, waitForMessage } from '../testing/index.ts';
|
|
22
|
+
import { AGENT_PROCESS_KEY, AgentInput, type AgentProcessDefinition, makeInputMessage } from './agent-process.ts';
|
|
23
|
+
|
|
24
|
+
const ECHO_PROCESS_KEY = 'com.example.process.echo';
|
|
25
|
+
|
|
26
|
+
/** Answers each prompt with its own text, so a test can tell which process ran the chat. */
|
|
27
|
+
const EchoProcess: AgentProcessDefinition = Operation.makeDurable({
|
|
28
|
+
key: ECHO_PROCESS_KEY,
|
|
29
|
+
input: AgentInput,
|
|
30
|
+
output: Schema.Void,
|
|
31
|
+
types: [Chat.Chat, Feed.Feed, Message.Message],
|
|
32
|
+
services: [Database.Service],
|
|
33
|
+
rpcs: HarnessControl,
|
|
34
|
+
}).pipe(
|
|
35
|
+
Operation.withDurableHandler((ctx) =>
|
|
36
|
+
Effect.gen(function* () {
|
|
37
|
+
const chatDxn = Option.getOrThrow(Annotation.getDictionary(ctx.params.annotations, Process.TargetAnnotation));
|
|
38
|
+
const chat = yield* Database.resolve(chatDxn, Chat.Chat).pipe(Effect.orDie);
|
|
39
|
+
const feed = yield* Database.load(chat.feed).pipe(Effect.orDie);
|
|
40
|
+
const reply = (text: string) =>
|
|
41
|
+
Feed.append(feed, [
|
|
42
|
+
Message.make({ sender: { role: 'assistant' }, blocks: [{ _tag: 'text', text: `echo: ${text}` }] }),
|
|
43
|
+
]);
|
|
44
|
+
return {
|
|
45
|
+
rpcHandlers: yield* HarnessControl.toHandlers({
|
|
46
|
+
setAlarm: () => Effect.void,
|
|
47
|
+
enqueueMessage: () => Effect.void,
|
|
48
|
+
}),
|
|
49
|
+
onInput: (input) => reply(Message.extractText(makeInputMessage(input))),
|
|
50
|
+
};
|
|
51
|
+
}),
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const TestLayer = AssistantTestLayer({
|
|
56
|
+
types: [Feed.Feed],
|
|
57
|
+
agent: { processes: () => [EchoProcess] },
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const makeChat = Effect.fnUntraced(function* (process?: string) {
|
|
61
|
+
const feed = yield* Database.add(Feed.make());
|
|
62
|
+
return yield* Database.add(Chat.make({ feed: Ref.make(feed), ...(process ? { session: { process } } : {}) }));
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const runningKeys = Effect.gen(function* () {
|
|
66
|
+
const manager = yield* Process.ManagerService;
|
|
67
|
+
const processes = yield* manager.handles();
|
|
68
|
+
return processes.map((process) => process.key);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('AgentService process selection', () => {
|
|
72
|
+
it.effect(
|
|
73
|
+
'runs a chat on the process its session names',
|
|
74
|
+
Effect.fnUntraced(
|
|
75
|
+
function* (_) {
|
|
76
|
+
const chat = yield* makeChat(ECHO_PROCESS_KEY);
|
|
77
|
+
const session = yield* AgentService.getSession(chat);
|
|
78
|
+
yield* session.submitPrompt('hello');
|
|
79
|
+
const reply = yield* waitForMessage(session.feed, (message) => message.sender.role === 'assistant', {
|
|
80
|
+
timeout: 5_000,
|
|
81
|
+
});
|
|
82
|
+
expect(Message.extractText(reply)).toBe('echo: hello');
|
|
83
|
+
expect(yield* runningKeys).toEqual([ECHO_PROCESS_KEY]);
|
|
84
|
+
},
|
|
85
|
+
Effect.provide(TestLayer),
|
|
86
|
+
TestHelpers.provideTestContext,
|
|
87
|
+
),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
it.effect(
|
|
91
|
+
"runs a chat that names no process, or one nothing contributed, on the assistant's",
|
|
92
|
+
Effect.fnUntraced(
|
|
93
|
+
function* (_) {
|
|
94
|
+
yield* AgentService.getSession(yield* makeChat());
|
|
95
|
+
yield* AgentService.getSession(yield* makeChat('com.example.process.uninstalled'));
|
|
96
|
+
expect(yield* runningKeys).toEqual([AGENT_PROCESS_KEY, AGENT_PROCESS_KEY]);
|
|
97
|
+
},
|
|
98
|
+
Effect.provide(TestLayer),
|
|
99
|
+
TestHelpers.provideTestContext,
|
|
100
|
+
),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
it.effect(
|
|
104
|
+
'moves a chat to the process its session names when that changes',
|
|
105
|
+
Effect.fnUntraced(
|
|
106
|
+
function* (_) {
|
|
107
|
+
const chat = yield* makeChat();
|
|
108
|
+
yield* AgentService.getSession(chat);
|
|
109
|
+
Obj.update(chat, (chat) => {
|
|
110
|
+
chat.session = { ...chat.session, process: ECHO_PROCESS_KEY };
|
|
111
|
+
});
|
|
112
|
+
yield* AgentService.getSession(chat);
|
|
113
|
+
const manager = yield* Process.ManagerService;
|
|
114
|
+
const live = (yield* manager.handles()).filter((process) => process.status.state !== Process.State.TERMINATED);
|
|
115
|
+
expect(live.map((process) => process.key)).toEqual([ECHO_PROCESS_KEY]);
|
|
116
|
+
},
|
|
117
|
+
Effect.provide(TestLayer),
|
|
118
|
+
TestHelpers.provideTestContext,
|
|
119
|
+
),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
it.effect(
|
|
123
|
+
'refuses to run a contributed process on edge',
|
|
124
|
+
Effect.fnUntraced(
|
|
125
|
+
function* (_) {
|
|
126
|
+
const chat = yield* makeChat(ECHO_PROCESS_KEY);
|
|
127
|
+
const exit = yield* AgentService.getSession(chat, { location: 'edge' }).pipe(Effect.exit);
|
|
128
|
+
expect(Exit.isFailure(exit)).toBe(true);
|
|
129
|
+
expect(yield* runningKeys).toEqual([]);
|
|
130
|
+
},
|
|
131
|
+
Effect.provide(TestLayer),
|
|
132
|
+
TestHelpers.provideTestContext,
|
|
133
|
+
),
|
|
134
|
+
);
|
|
135
|
+
});
|
|
@@ -11,8 +11,11 @@ import { expect } from 'vitest';
|
|
|
11
11
|
|
|
12
12
|
import { ScriptedLanguageModel } from '@dxos/ai/testing';
|
|
13
13
|
import { Alarm, SessionStore, isConsumed, isQueued } from '@dxos/assistant';
|
|
14
|
+
import { ProcessManager } from '@dxos/compute-runtime';
|
|
15
|
+
import * as ComputeAgentService from '@dxos/compute/AgentService';
|
|
14
16
|
import * as Operation from '@dxos/compute/Operation';
|
|
15
17
|
import * as OperationHandlerSet from '@dxos/compute/OperationHandlerSet';
|
|
18
|
+
import * as Process from '@dxos/compute/Process';
|
|
16
19
|
import * as Skill from '@dxos/compute/Skill';
|
|
17
20
|
import { type Database, Feed, Filter, Obj } from '@dxos/echo';
|
|
18
21
|
import * as RuntimeProvider from '@dxos/effect/RuntimeProvider';
|
|
@@ -21,6 +24,7 @@ import { DXN, EntityId } from '@dxos/keys';
|
|
|
21
24
|
import { Message } from '@dxos/types';
|
|
22
25
|
|
|
23
26
|
import { AssistantTestLayer } from '../testing/index.ts';
|
|
27
|
+
import { AGENT_PROCESS_KEY } from './agent-process.ts';
|
|
24
28
|
import * as AgentService from './AgentService.ts';
|
|
25
29
|
|
|
26
30
|
const { text, toolCall, scriptedAiService } = ScriptedLanguageModel;
|
|
@@ -139,6 +143,41 @@ const readFeed = (feed: Feed.Feed) =>
|
|
|
139
143
|
return { items, messages, ...pending };
|
|
140
144
|
});
|
|
141
145
|
|
|
146
|
+
describe('AgentProcess residency (scripted)', () => {
|
|
147
|
+
it.effect(
|
|
148
|
+
'a resident process stays idle after its turn and takes the next prompt itself',
|
|
149
|
+
Effect.fnUntraced(
|
|
150
|
+
function* (_) {
|
|
151
|
+
const processManager = yield* ProcessManager.ProcessManagerService;
|
|
152
|
+
const session = yield* AgentService.createSession();
|
|
153
|
+
const target = Obj.getURI(session.chat);
|
|
154
|
+
|
|
155
|
+
yield* session.submitPrompt('first');
|
|
156
|
+
yield* session.waitForCompletion();
|
|
157
|
+
const [handle] = yield* processManager.list({ target, key: AGENT_PROCESS_KEY });
|
|
158
|
+
expect(handle.status.state).toBe(Process.State.IDLE);
|
|
159
|
+
|
|
160
|
+
// The same session, the same process: no respawn (and no history replay) for a follow-up.
|
|
161
|
+
const followUp = yield* ComputeAgentService.getSession(session.chat);
|
|
162
|
+
yield* followUp.submitPrompt('second');
|
|
163
|
+
yield* followUp.waitForCompletion();
|
|
164
|
+
const processes = yield* processManager.list({ target, key: AGENT_PROCESS_KEY });
|
|
165
|
+
expect(processes.map((process) => String(process.pid))).toEqual([String(handle.pid)]);
|
|
166
|
+
expect(new Set(promptTexts((yield* readFeed(session.feed)).messages))).toEqual(new Set(['first', 'second']));
|
|
167
|
+
},
|
|
168
|
+
Effect.provide(
|
|
169
|
+
AssistantTestLayer({
|
|
170
|
+
types: [Alarm.Alarm],
|
|
171
|
+
agent: { resident: true },
|
|
172
|
+
aiService: scriptedAiService(Array.from({ length: 4 }, () => ({ parts: [text('Acknowledged.')] }))),
|
|
173
|
+
}),
|
|
174
|
+
),
|
|
175
|
+
TestHelpers.provideTestContext,
|
|
176
|
+
),
|
|
177
|
+
{ timeout: 30_000 },
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
|
|
142
181
|
describe('AgentProcess input queue (scripted)', () => {
|
|
143
182
|
it.effect(
|
|
144
183
|
'a submitted prompt is queued on the feed, then acked by the turn that consumes it',
|
|
@@ -7,7 +7,13 @@ import { describe, it } from 'vitest';
|
|
|
7
7
|
import * as Process from '@dxos/compute/Process';
|
|
8
8
|
import { type ContentBlock } from '@dxos/types';
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
type ToolResultEvent,
|
|
12
|
+
dropReportedToolResults,
|
|
13
|
+
isToolResultHandled,
|
|
14
|
+
toolResultPrompt,
|
|
15
|
+
wakeUpPrompt,
|
|
16
|
+
} from './agent-process.ts';
|
|
11
17
|
|
|
12
18
|
// Recovering a tool result across a reload was, until now, exercised only when a race inside
|
|
13
19
|
// `AgentService.test.ts`'s `recovers queued tool results after reload` happened to land on it, and no
|
|
@@ -55,6 +61,22 @@ describe('dropReportedToolResults', () => {
|
|
|
55
61
|
});
|
|
56
62
|
});
|
|
57
63
|
|
|
64
|
+
// A tool child that can no longer be reattached is answered with an error unless its result is already
|
|
65
|
+
// accounted for; otherwise the call would stay pending and the agent could never complete.
|
|
66
|
+
describe('isToolResultHandled', () => {
|
|
67
|
+
it('is handled when the result is queued for the next turn', ({ expect }) => {
|
|
68
|
+
expect(isToolResultHandled([toolResult('1')], Process.ID.make('1'), () => false)).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('is handled when the result already reached the agent', ({ expect }) => {
|
|
72
|
+
expect(isToolResultHandled([], Process.ID.make('1'), (pid) => pid === Process.ID.make('1'))).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('is not handled when the result was neither queued nor reported', ({ expect }) => {
|
|
76
|
+
expect(isToolResultHandled([toolResult('2')], Process.ID.make('1'), () => false)).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
58
80
|
describe('toolResultPrompt', () => {
|
|
59
81
|
it('redelivers a recovered tool result as a synthetic <result> block', ({ expect }) => {
|
|
60
82
|
const [block] = toolResultPrompt(toolResult('9', 'listed on the NASDAQ'));
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { beforeEach, describe, expect, it } from '@effect/vitest';
|
|
6
|
+
import * as Effect from 'effect/Effect';
|
|
7
|
+
import * as Schema from 'effect/Schema';
|
|
8
|
+
|
|
9
|
+
import { ScriptedLanguageModel } from '@dxos/ai/testing';
|
|
10
|
+
import { ProcessManager } from '@dxos/compute-runtime';
|
|
11
|
+
import * as Operation from '@dxos/compute/Operation';
|
|
12
|
+
import * as OperationHandlerSet from '@dxos/compute/OperationHandlerSet';
|
|
13
|
+
import * as Process from '@dxos/compute/Process';
|
|
14
|
+
import * as Skill from '@dxos/compute/Skill';
|
|
15
|
+
import { Feed, Obj, Ref } from '@dxos/echo';
|
|
16
|
+
import { TestHelpers } from '@dxos/effect/testing';
|
|
17
|
+
import { DXN, EntityId } from '@dxos/keys';
|
|
18
|
+
import { Message } from '@dxos/types';
|
|
19
|
+
|
|
20
|
+
import { AssistantTestLayer } from '../testing/index.ts';
|
|
21
|
+
import { AGENT_PROCESS_KEY } from './agent-process.ts';
|
|
22
|
+
import * as AgentService from './AgentService.ts';
|
|
23
|
+
|
|
24
|
+
EntityId.dangerouslyDisableRandomness();
|
|
25
|
+
|
|
26
|
+
const InlineHook = Operation.make({
|
|
27
|
+
meta: { key: DXN.make('com.example.operation.inlineHook'), name: 'Inline hook' },
|
|
28
|
+
input: Schema.Struct({}),
|
|
29
|
+
output: Schema.Void,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const BackgroundHook = Operation.make({
|
|
33
|
+
meta: { key: DXN.make('com.example.operation.backgroundHook'), name: 'Background hook' },
|
|
34
|
+
input: Schema.Struct({}),
|
|
35
|
+
output: Schema.Void,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const QuickHook = Operation.make({
|
|
39
|
+
meta: { key: DXN.make('com.example.operation.quickHook'), name: 'Quick hook' },
|
|
40
|
+
input: Schema.Struct({}),
|
|
41
|
+
output: Schema.Void,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const FailingHook = Operation.make({
|
|
45
|
+
meta: { key: DXN.make('com.example.operation.failingHook'), name: 'Failing hook' },
|
|
46
|
+
input: Schema.Struct({}),
|
|
47
|
+
output: Schema.Void,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/** Hook invocations in the order they started and finished, and the gate the background hook holds on. */
|
|
51
|
+
type HookLog = { events: string[]; release: () => void; released: Promise<void> };
|
|
52
|
+
|
|
53
|
+
const makeHookLog = (): HookLog => {
|
|
54
|
+
let release = () => {};
|
|
55
|
+
const released = new Promise<void>((resolve) => {
|
|
56
|
+
release = resolve;
|
|
57
|
+
});
|
|
58
|
+
return { events: [], release, released };
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
let hookLog = makeHookLog();
|
|
62
|
+
|
|
63
|
+
const handlers = OperationHandlerSet.make(
|
|
64
|
+
InlineHook.pipe(Operation.withHandler(() => Effect.sync(() => void hookLog.events.push('inline')))),
|
|
65
|
+
BackgroundHook.pipe(
|
|
66
|
+
Operation.withHandler(() =>
|
|
67
|
+
Effect.gen(function* () {
|
|
68
|
+
hookLog.events.push('background:start');
|
|
69
|
+
yield* Effect.promise(() => hookLog.released);
|
|
70
|
+
hookLog.events.push('background:end');
|
|
71
|
+
}),
|
|
72
|
+
),
|
|
73
|
+
),
|
|
74
|
+
FailingHook.pipe(Operation.withHandler(() => Effect.die(new Error('hook failed')))),
|
|
75
|
+
QuickHook.pipe(Operation.withHandler(() => Effect.sync(() => void hookLog.events.push('quick')))),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const hook = (operation: Operation.Definition.Any, async: boolean): Skill.Hook => ({
|
|
79
|
+
spec: { _tag: 'end-request' },
|
|
80
|
+
async,
|
|
81
|
+
function: Ref.make(Operation.serialize(operation)),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const HookSkill = Skill.make({
|
|
85
|
+
key: 'com.example.skill.hooks',
|
|
86
|
+
name: 'Hooks',
|
|
87
|
+
hooks: [hook(InlineHook, false), hook(BackgroundHook, true)],
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const FailingHookSkill = Skill.make({
|
|
91
|
+
key: 'com.example.skill.failingHooks',
|
|
92
|
+
name: 'Failing hooks',
|
|
93
|
+
hooks: [hook(FailingHook, true)],
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Finishes at once, so its exit can race the agent registering it.
|
|
97
|
+
const QuickHookSkill = Skill.make({
|
|
98
|
+
key: 'com.example.skill.quickHooks',
|
|
99
|
+
name: 'Quick hooks',
|
|
100
|
+
hooks: [hook(QuickHook, true)],
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const TestLayer = AssistantTestLayer({
|
|
104
|
+
operationHandlers: handlers,
|
|
105
|
+
types: [Skill.Skill, Feed.Feed, Message.Message],
|
|
106
|
+
skills: [HookSkill, FailingHookSkill, QuickHookSkill],
|
|
107
|
+
aiService: ScriptedLanguageModel.scriptedAiService(() => ({ parts: [ScriptedLanguageModel.text('Done.')] })),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
/** The agent process serving the session's chat. */
|
|
111
|
+
const agentProcess = Effect.fnUntraced(function* (session: { chat: Obj.Unknown }) {
|
|
112
|
+
const processManager = yield* ProcessManager.ProcessManagerService;
|
|
113
|
+
const [handle] = yield* processManager.list({ target: Obj.getURI(session.chat), key: AGENT_PROCESS_KEY });
|
|
114
|
+
return handle;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('end-request skill hooks', () => {
|
|
118
|
+
beforeEach(() => {
|
|
119
|
+
hookLog = makeHookLog();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it.effect(
|
|
123
|
+
'a background hook does not hold up the request, and the process awaits it before finishing',
|
|
124
|
+
Effect.fnUntraced(
|
|
125
|
+
function* (_) {
|
|
126
|
+
const session = yield* AgentService.createSession({ skills: [HookSkill] });
|
|
127
|
+
yield* session.submitPrompt('Hello.');
|
|
128
|
+
yield* session.waitForCompletion();
|
|
129
|
+
|
|
130
|
+
// The request settled while the background hook is still blocked; the inline hook ran first.
|
|
131
|
+
expect(hookLog.events).toEqual(['inline', 'background:start']);
|
|
132
|
+
const handle = yield* agentProcess(session);
|
|
133
|
+
expect(handle.status.state).not.toBe(Process.State.SUCCEEDED);
|
|
134
|
+
|
|
135
|
+
hookLog.release();
|
|
136
|
+
yield* Effect.promise(async () => {
|
|
137
|
+
await expect.poll(() => handle.status.state, { timeout: 5_000 }).toBe(Process.State.SUCCEEDED);
|
|
138
|
+
});
|
|
139
|
+
expect(hookLog.events).toEqual(['inline', 'background:start', 'background:end']);
|
|
140
|
+
},
|
|
141
|
+
Effect.provide(TestLayer),
|
|
142
|
+
TestHelpers.provideTestContext,
|
|
143
|
+
),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
it.effect(
|
|
147
|
+
'a background hook that finishes at once still lets the process finish',
|
|
148
|
+
Effect.fnUntraced(
|
|
149
|
+
function* (_) {
|
|
150
|
+
const session = yield* AgentService.createSession({ skills: [QuickHookSkill] });
|
|
151
|
+
yield* session.submitPrompt('Hello.');
|
|
152
|
+
yield* session.waitForCompletion();
|
|
153
|
+
|
|
154
|
+
const handle = yield* agentProcess(session);
|
|
155
|
+
yield* Effect.promise(async () => {
|
|
156
|
+
await expect.poll(() => handle.status.state, { timeout: 5_000 }).toBe(Process.State.SUCCEEDED);
|
|
157
|
+
});
|
|
158
|
+
expect(hookLog.events).toEqual(['quick']);
|
|
159
|
+
},
|
|
160
|
+
Effect.provide(TestLayer),
|
|
161
|
+
TestHelpers.provideTestContext,
|
|
162
|
+
),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
it.effect(
|
|
166
|
+
'a failing background hook still lets the process finish',
|
|
167
|
+
Effect.fnUntraced(
|
|
168
|
+
function* (_) {
|
|
169
|
+
const session = yield* AgentService.createSession({ skills: [FailingHookSkill] });
|
|
170
|
+
yield* session.submitPrompt('Hello.');
|
|
171
|
+
yield* session.waitForCompletion();
|
|
172
|
+
|
|
173
|
+
const handle = yield* agentProcess(session);
|
|
174
|
+
yield* Effect.promise(async () => {
|
|
175
|
+
await expect.poll(() => handle.status.state, { timeout: 5_000 }).toBe(Process.State.SUCCEEDED);
|
|
176
|
+
});
|
|
177
|
+
},
|
|
178
|
+
Effect.provide(TestLayer),
|
|
179
|
+
TestHelpers.provideTestContext,
|
|
180
|
+
),
|
|
181
|
+
);
|
|
182
|
+
});
|
|
@@ -18,6 +18,7 @@ import * as Chat from '@dxos/assistant/Chat';
|
|
|
18
18
|
import {
|
|
19
19
|
FeedTraceSink,
|
|
20
20
|
ProcessManager,
|
|
21
|
+
ProcessOperationInvoker,
|
|
21
22
|
RemoteProcessManager,
|
|
22
23
|
RemoteTraceMonitor,
|
|
23
24
|
TriggerDispatcher,
|
|
@@ -101,7 +102,7 @@ export type AssistantTestServices =
|
|
|
101
102
|
| Operation.Service
|
|
102
103
|
| ProcessManager.Service
|
|
103
104
|
| RemoteProcessManager.Service
|
|
104
|
-
|
|
|
105
|
+
| ProcessOperationInvoker.Service
|
|
105
106
|
| Process.ManagerService
|
|
106
107
|
| AtomRegistry.AtomRegistry
|
|
107
108
|
| OperationHandlerSet.OperationHandlerProvider
|
|
@@ -139,7 +140,7 @@ export const AssistantTestLayer = (
|
|
|
139
140
|
return Layer.empty.pipe(
|
|
140
141
|
// Captures must sit above the layers they read (a provideMerge chain feeds upward).
|
|
141
142
|
Layer.provideMerge(captureAgentService(agentServiceHolder)),
|
|
142
|
-
Layer.provideMerge(
|
|
143
|
+
Layer.provideMerge(ProcessOperationInvoker.layer),
|
|
143
144
|
Layer.provideMerge(AgentServiceRuntime.layer(agentOptions)),
|
|
144
145
|
Layer.provideMerge(UnifiedProcessManager.layer),
|
|
145
146
|
// A local test stack has no EDGE, so both the manager's remote half and `location: 'edge'` see
|