@dxos/agent-runtime 0.0.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/LICENSE +105 -0
- package/dist/lib/neutral/chunk-EZKG2ZGG.mjs +754 -0
- package/dist/lib/neutral/chunk-EZKG2ZGG.mjs.map +7 -0
- package/dist/lib/neutral/index.mjs +9 -0
- package/dist/lib/neutral/index.mjs.map +7 -0
- package/dist/lib/neutral/meta.json +1 -0
- package/dist/lib/neutral/testing/index.mjs +285 -0
- package/dist/lib/neutral/testing/index.mjs.map +7 -0
- package/dist/types/src/agent-service/AgentService.d.ts +45 -0
- package/dist/types/src/agent-service/AgentService.d.ts.map +1 -0
- package/dist/types/src/agent-service/AgentService.test.d.ts +2 -0
- package/dist/types/src/agent-service/AgentService.test.d.ts.map +1 -0
- package/dist/types/src/agent-service/agent-process.d.ts +457 -0
- package/dist/types/src/agent-service/agent-process.d.ts.map +1 -0
- package/dist/types/src/agent-service/agent-process.test.d.ts +2 -0
- package/dist/types/src/agent-service/agent-process.test.d.ts.map +1 -0
- package/dist/types/src/agent-service/delegation-strategy.d.ts +47 -0
- package/dist/types/src/agent-service/delegation-strategy.d.ts.map +1 -0
- package/dist/types/src/agent-service/index.d.ts +4 -0
- package/dist/types/src/agent-service/index.d.ts.map +1 -0
- package/dist/types/src/assistant-session-tests/format.test.d.ts +2 -0
- package/dist/types/src/assistant-session-tests/format.test.d.ts.map +1 -0
- package/dist/types/src/assistant-session-tests/request.test.d.ts +2 -0
- package/dist/types/src/assistant-session-tests/request.test.d.ts.map +1 -0
- package/dist/types/src/assistant-session-tests/session-ollama.test.d.ts +2 -0
- package/dist/types/src/assistant-session-tests/session-ollama.test.d.ts.map +1 -0
- package/dist/types/src/assistant-session-tests/session.test.d.ts +2 -0
- package/dist/types/src/assistant-session-tests/session.test.d.ts.map +1 -0
- package/dist/types/src/assistant-session-tests/xml-response.test.d.ts +2 -0
- package/dist/types/src/assistant-session-tests/xml-response.test.d.ts.map +1 -0
- package/dist/types/src/functions.test.d.ts +2 -0
- package/dist/types/src/functions.test.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +2 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/testing/ResearchService.d.ts +28 -0
- package/dist/types/src/testing/ResearchService.d.ts.map +1 -0
- package/dist/types/src/testing/assistant-test-layer.d.ts +70 -0
- package/dist/types/src/testing/assistant-test-layer.d.ts.map +1 -0
- package/dist/types/src/testing/index.d.ts +4 -0
- package/dist/types/src/testing/index.d.ts.map +1 -0
- package/dist/types/src/testing/trace-pretty-print.d.ts +16 -0
- package/dist/types/src/testing/trace-pretty-print.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +70 -0
- package/src/agent-service/AgentService.conversations.json +1 -0
- package/src/agent-service/AgentService.test.ts +523 -0
- package/src/agent-service/AgentService.ts +239 -0
- package/src/agent-service/README.md +91 -0
- package/src/agent-service/agent-process.test.ts +206 -0
- package/src/agent-service/agent-process.ts +793 -0
- package/src/agent-service/delegation-strategy.ts +63 -0
- package/src/agent-service/index.ts +7 -0
- package/src/assistant-session-tests/format.test.ts +131 -0
- package/src/assistant-session-tests/request.conversations.json +1 -0
- package/src/assistant-session-tests/request.test.ts +190 -0
- package/src/assistant-session-tests/session-ollama.test.ts +80 -0
- package/src/assistant-session-tests/session.test.ts +56 -0
- package/src/assistant-session-tests/xml-response.conversations.json +1 -0
- package/src/assistant-session-tests/xml-response.test.ts +65 -0
- package/src/functions.conversations.json +1 -0
- package/src/functions.test.ts +87 -0
- package/src/index.ts +7 -0
- package/src/testing/ResearchService.ts +144 -0
- package/src/testing/assistant-test-layer.ts +299 -0
- package/src/testing/index.ts +7 -0
- package/src/testing/trace-pretty-print.ts +55 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type * as Effect from 'effect/Effect';
|
|
6
|
+
import type * as Exit from 'effect/Exit';
|
|
7
|
+
|
|
8
|
+
import { type Process } from '@dxos/compute';
|
|
9
|
+
import { type ProcessManager } from '@dxos/compute-runtime';
|
|
10
|
+
import { type Database, type Feed } from '@dxos/echo';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A unit of work the supervisor delegates to a linked child process. `spawn` is an existential over
|
|
14
|
+
* the child operation's input/output types: the strategy constructs it (e.g. via `invokeFiber`)
|
|
15
|
+
* so {@link AgentProcess} can remain agnostic to the operation type and just track the returned pid.
|
|
16
|
+
*/
|
|
17
|
+
export interface Delegation {
|
|
18
|
+
/**
|
|
19
|
+
* Stable id correlating this delegation with its completion (e.g. a plan task id).
|
|
20
|
+
*/
|
|
21
|
+
readonly id: string;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Spawns the child for this delegation and returns its process id. Must be linked (non-blocking)
|
|
25
|
+
* so the child's exit wakes the supervisor's `onChildEvent` — i.e. `ProcessOperationInvoker.invokeFiber`.
|
|
26
|
+
*/
|
|
27
|
+
readonly spawn: Effect.Effect<Process.ID, never, ProcessManager.ProcessOperationInvoker.Service>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Services the strategy callbacks may use. The hosting {@link AgentProcess} provides these.
|
|
32
|
+
*/
|
|
33
|
+
export type StrategyServices = Database.Service;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Turns a conversational agent process into a supervisor: after each turn it reconciles outstanding
|
|
37
|
+
* work into linked child processes, and folds their results back into the conversation on completion.
|
|
38
|
+
*
|
|
39
|
+
* Implemented outside `@dxos/functions-runtime` (which cannot depend on the agent/plan types)
|
|
40
|
+
* and passed to {@link AgentService.layer} so {@link AgentProcess} stays generic.
|
|
41
|
+
* Absent by default — a plain conversational agent.
|
|
42
|
+
*/
|
|
43
|
+
export interface DelegationStrategy {
|
|
44
|
+
/**
|
|
45
|
+
* Called after each agent turn. Returns delegations for outstanding work not already in flight.
|
|
46
|
+
* @param feed - The conversation feed (identifies which agent/plan to reconcile).
|
|
47
|
+
* @param activeIds - Ids of delegations already running, so reconcile does not double-spawn.
|
|
48
|
+
*/
|
|
49
|
+
readonly reconcile: (
|
|
50
|
+
feed: Feed.Feed,
|
|
51
|
+
activeIds: ReadonlySet<string>,
|
|
52
|
+
) => Effect.Effect<readonly Delegation[], never, StrategyServices>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Called when a delegated child completes. Use to record the result and notify the user (e.g.
|
|
56
|
+
* update the plan task status and append a message to the conversation feed).
|
|
57
|
+
*/
|
|
58
|
+
readonly onComplete: (
|
|
59
|
+
feed: Feed.Feed,
|
|
60
|
+
id: string,
|
|
61
|
+
exit: Exit.Exit<unknown>,
|
|
62
|
+
) => Effect.Effect<void, never, StrategyServices>;
|
|
63
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from '@effect/vitest';
|
|
6
|
+
import * as Effect from 'effect/Effect';
|
|
7
|
+
import * as Schema from 'effect/Schema';
|
|
8
|
+
|
|
9
|
+
import { createSystemPrompt, formatSystemPrompt } from '@dxos/assistant';
|
|
10
|
+
import { Operation, OperationHandlerSet, Skill, Template } from '@dxos/compute';
|
|
11
|
+
import { Database, Obj, Query } from '@dxos/echo';
|
|
12
|
+
import { TestHelpers } from '@dxos/effect/testing';
|
|
13
|
+
import { DXN } from '@dxos/keys';
|
|
14
|
+
import { Text } from '@dxos/schema';
|
|
15
|
+
import { Organization } from '@dxos/types';
|
|
16
|
+
import { trim } from '@dxos/util';
|
|
17
|
+
|
|
18
|
+
import { AssistantTestLayer } from '../testing';
|
|
19
|
+
|
|
20
|
+
const OrganizationList = Operation.make({
|
|
21
|
+
meta: {
|
|
22
|
+
key: DXN.make('org.dxos.function.organizationList'),
|
|
23
|
+
name: 'Organization List',
|
|
24
|
+
description: 'List organizations',
|
|
25
|
+
},
|
|
26
|
+
input: Schema.Struct({}),
|
|
27
|
+
output: Schema.Array(Schema.String),
|
|
28
|
+
services: [Database.Service],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const Handlers = OperationHandlerSet.make(
|
|
32
|
+
Operation.withHandler(
|
|
33
|
+
OrganizationList,
|
|
34
|
+
Effect.fnUntraced(function* () {
|
|
35
|
+
const organizations = yield* Database.query(Query.type(Organization.Organization)).run;
|
|
36
|
+
return organizations.map((organization) => organization.name ?? '<no org>');
|
|
37
|
+
}),
|
|
38
|
+
),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const TestLayer = AssistantTestLayer({
|
|
42
|
+
types: [Text.Text, Organization.Organization, Skill.Skill],
|
|
43
|
+
operationHandlers: Handlers,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('format', () => {
|
|
47
|
+
it.effect(
|
|
48
|
+
'should format',
|
|
49
|
+
Effect.fnUntraced(
|
|
50
|
+
function* (_) {
|
|
51
|
+
const { db } = yield* Database.Service;
|
|
52
|
+
const object = db.add(
|
|
53
|
+
Obj.make(Organization.Organization, {
|
|
54
|
+
name: 'Test',
|
|
55
|
+
website: 'https://www.test.com',
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const skill = db.add(
|
|
60
|
+
Skill.make({
|
|
61
|
+
key: 'com.example.skill.test',
|
|
62
|
+
name: 'Test',
|
|
63
|
+
instructions: Template.make({
|
|
64
|
+
source: trim`
|
|
65
|
+
Test
|
|
66
|
+
This is the test skill.
|
|
67
|
+
`,
|
|
68
|
+
}),
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const output = yield* formatSystemPrompt({
|
|
73
|
+
system: createSystemPrompt({}),
|
|
74
|
+
skills: [skill],
|
|
75
|
+
objects: [object],
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
console.log(output);
|
|
79
|
+
},
|
|
80
|
+
Effect.provide(TestLayer),
|
|
81
|
+
TestHelpers.provideTestContext,
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
it.effect(
|
|
86
|
+
'should format with function input',
|
|
87
|
+
Effect.fnUntraced(
|
|
88
|
+
function* (_) {
|
|
89
|
+
const { db } = yield* Database.Service;
|
|
90
|
+
const object = db.add(
|
|
91
|
+
Obj.make(Organization.Organization, {
|
|
92
|
+
name: 'Test Org',
|
|
93
|
+
website: 'https://www.test.com',
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const skill = db.add(
|
|
98
|
+
Skill.make({
|
|
99
|
+
key: 'com.example.skill.test',
|
|
100
|
+
name: 'Test',
|
|
101
|
+
instructions: Template.make({
|
|
102
|
+
source: trim`
|
|
103
|
+
Organization List:
|
|
104
|
+
{{#each organizations}}
|
|
105
|
+
- {{this}}
|
|
106
|
+
{{/each}}
|
|
107
|
+
`,
|
|
108
|
+
inputs: [
|
|
109
|
+
{
|
|
110
|
+
name: 'organizations',
|
|
111
|
+
kind: 'operation',
|
|
112
|
+
operation: OrganizationList.meta.key,
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
}),
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const output = yield* formatSystemPrompt({
|
|
120
|
+
system: createSystemPrompt({}),
|
|
121
|
+
skills: [skill],
|
|
122
|
+
objects: [object],
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(output).to.include('Test Org');
|
|
126
|
+
},
|
|
127
|
+
Effect.provide(TestLayer),
|
|
128
|
+
TestHelpers.provideTestContext,
|
|
129
|
+
),
|
|
130
|
+
);
|
|
131
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"conversations":[{"parameters":{"model":"dxn:com.anthropic.model.claude-opus-4-8.default","stream":true,"tools":[]},"prompt":{"content":[{"role":"user","content":"Hello world!","options":{"anthropic":{"cacheControl":{"ttl":"5m","type":"ephemeral"}}}}]},"response":[{"type":"response-metadata","id":"msg_01FpufqYyxESFAyhqbgxxDzh","modelId":"claude-opus-4-8","timestamp":"1970-01-01T00:00:00.000Z","metadata":{}},{"type":"text-start","id":"0","metadata":{}},{"type":"text-delta","id":"0","delta":"Hello!","metadata":{}},{"type":"text-delta","id":"0","delta":" 👋 Nice to meet you!\n\nHow can I help you today? Whether you have a question, need help with a task, want to brainstorm ide","metadata":{}},{"type":"text-delta","id":"0","delta":"as, or just feel like chatting, I'm happy to assist.","metadata":{}},{"type":"text-end","id":"0","metadata":{}},{"type":"finish","reason":"stop","usage":{"inputTokens":11,"outputTokens":69,"totalTokens":80},"metadata":{"anthropic":{"usage":{"cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":11,"output_tokens":4,"server_tool_use":null,"service_tier":"standard"}}}}]},{"parameters":{"model":"dxn:com.anthropic.model.claude-opus-4-8.default","stream":true,"tools":[{"name":"Calculator","description":"Basic calculator tool","inputSchema":{"type":"object","required":["input"],"properties":{"input":{"type":"string","description":"The calculation to perform."}},"additionalProperties":false}}]},"prompt":{"content":[{"role":"user","content":"What is 10 + 30?","options":{"anthropic":{"cacheControl":{"ttl":"5m","type":"ephemeral"}}}}]},"response":[{"type":"response-metadata","id":"msg_01ArHuDbCcJbCuEbKQWbVgNP","modelId":"claude-opus-4-8","timestamp":"1970-01-01T00:00:00.000Z","metadata":{}},{"type":"reasoning-start","id":"0","metadata":{}},{"type":"reasoning-delta","id":"0","delta":"","metadata":{"anthropic":{"type":"thinking","signature":"EswBCmMIDxgCKkCYWMo4h+gTuoOJyej3UAvI/z9N7dYZpIw1rpyM0pAj/LGQIcM+c4h7Vk4GHtKp709n07ZxKqSK/CNlabX+vVcgMg9jbGF1ZGUtb3B1cy00LTg4AEIIdGhpbmtpbmcSDOr1/rlK3KARyw42YRoMbyke63kbVOmiO98bIjCpiMfmStMK3sXtw2tSO6NVvn9livrg8FdZfGGPbiuI93daDYpxAQ5tPsK/AIJ19V0qF9HjBzmlU4khZ3Xs+4azncZok1GLHI9IGAE="}}},{"type":"reasoning-end","id":"0","metadata":{}},{"type":"tool-params-start","id":"toolu_01Cwmh4VVGrpoEgGSvnC9mkB","name":"Calculator","providerExecuted":false,"metadata":{}},{"type":"tool-params-delta","id":"toolu_01Cwmh4VVGrpoEgGSvnC9mkB","delta":"","metadata":{}},{"type":"tool-params-delta","id":"toolu_01Cwmh4VVGrpoEgGSvnC9mkB","delta":"{\"inp","metadata":{}},{"type":"tool-params-delta","id":"toolu_01Cwmh4VVGrpoEgGSvnC9mkB","delta":"ut\": \"10 + 3","metadata":{}},{"type":"tool-params-delta","id":"toolu_01Cwmh4VVGrpoEgGSvnC9mkB","delta":"0\"}","metadata":{}},{"type":"tool-params-end","id":"toolu_01Cwmh4VVGrpoEgGSvnC9mkB","metadata":{}},{"type":"finish","reason":"tool-calls","usage":{"inputTokens":398,"outputTokens":59,"totalTokens":457},"metadata":{"anthropic":{"usage":{"cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":398,"output_tokens":5,"server_tool_use":null,"service_tier":"standard"}}}}]},{"parameters":{"model":"dxn:com.anthropic.model.claude-opus-4-8.default","stream":true,"tools":[{"name":"Calculator","description":"Basic calculator tool","inputSchema":{"type":"object","required":["input"],"properties":{"input":{"type":"string","description":"The calculation to perform."}},"additionalProperties":false}}]},"prompt":{"content":[{"role":"user","content":"What is 10 + 30?","options":{}},{"role":"assistant","content":[{"type":"reasoning","text":"","options":{}},{"type":"tool-call","id":"toolu_01Cwmh4VVGrpoEgGSvnC9mkB","name":"Calculator","params":{"input":"10 + 30"},"providerExecuted":false,"options":{}}],"options":{}},{"role":"tool","content":[{"type":"tool-result","id":"toolu_01Cwmh4VVGrpoEgGSvnC9mkB","name":"Calculator","isFailure":false,"result":{"result":40},"providerExecuted":false,"options":{}}],"options":{"anthropic":{"cacheControl":{"ttl":"5m","type":"ephemeral"}}}}]},"response":[{"type":"response-metadata","id":"msg_014jK4oKvYXUZfkQH6rdg3Sk","modelId":"claude-opus-4-8","timestamp":"1970-01-01T00:00:00.000Z","metadata":{}},{"type":"text-start","id":"0","metadata":{}},{"type":"text-delta","id":"0","delta":"10","metadata":{}},{"type":"text-delta","id":"0","delta":" + 30 = **40**","metadata":{}},{"type":"text-end","id":"0","metadata":{}},{"type":"finish","reason":"stop","usage":{"inputTokens":461,"outputTokens":12,"totalTokens":473},"metadata":{"anthropic":{"usage":{"cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":461,"output_tokens":1,"server_tool_use":null,"service_tier":"standard"}}}}]},{"parameters":{"model":"dxn:com.anthropic.model.claude-opus-4-8.default","stream":true,"tools":[{"name":"Calculator","description":"Basic calculator tool","inputSchema":{"type":"object","required":["input"],"properties":{"input":{"type":"string","description":"The calculation to perform."}},"additionalProperties":false}}]},"prompt":{"content":[{"role":"user","content":"I am testing error handling in tool paramter parsing. I want you to call the calculator tool but omit the input parameter to intentionally differ from the tool schema.","options":{"anthropic":{"cacheControl":{"ttl":"5m","type":"ephemeral"}}}}]},"response":[{"type":"response-metadata","id":"msg_013GLZgtSMQ6Xhy7dyj24QoQ","modelId":"claude-opus-4-8","timestamp":"1970-01-01T00:00:00.000Z","metadata":{}},{"type":"reasoning-start","id":"0","metadata":{}},{"type":"reasoning-delta","id":"0","delta":"","metadata":{"anthropic":{"type":"thinking","signature":"EsECCmMIDxgCKkBByEwRYF9GiBoM479XPHHs7dxOSQ3NPMauc8wLYse91ljriLub5fGeBmkPlt4rko3tqY+sTzS948f0a9sYSGwuMg9jbGF1ZGUtb3B1cy00LTg4AEIIdGhpbmtpbmcSDDb6sRNiZTblNxhf5RoMgNqpUCJZhZupbHGEIjA+7zxzzkHLPZzJd8jdl2u2RKWjbFh42dh92TCW8ZOQEpNYzHYhjVdG6UDAmyDTyeEqiwFnzQN2DeuuUOsZNrsAs8fpmOx/lqHGf/yalvrZBAtK7Q9ZTf5vUX1G7WuEWopM6aoRUK8VizJr9GP2RK2XFG3BG3RNr3cllZYsPImcKbGASX87CpPchuw5mnEFSyLXjhGclj7uKYBaXjQuYTqwmgzgfToQ41N4FnoiEUa2vaDXqVeNagVxM46U3lh8GAE="}}},{"type":"reasoning-end","id":"0","metadata":{}},{"type":"text-start","id":"1","metadata":{}},{"type":"text-delta","id":"1","delta":"I'll help you test the error handling.","metadata":{}},{"type":"text-delta","id":"1","delta":" Let me call the Calculator tool while intentionally omitting the required `input` parameter:","metadata":{}},{"type":"text-end","id":"1","metadata":{}},{"type":"tool-params-start","id":"toolu_01FKjZtYwnbyrMzyWH6fgK4k","name":"Calculator","providerExecuted":false,"metadata":{}},{"type":"tool-params-delta","id":"toolu_01FKjZtYwnbyrMzyWH6fgK4k","delta":"","metadata":{}},{"type":"tool-params-end","id":"toolu_01FKjZtYwnbyrMzyWH6fgK4k","metadata":{}}]},{"parameters":{"model":"dxn:com.anthropic.model.claude-opus-4-8.default","stream":true,"tools":[{"name":"Calculator","description":"Basic calculator tool","inputSchema":{"type":"object","required":["input"],"properties":{"input":{"type":"string","description":"The calculation to perform."}},"additionalProperties":false}}]},"prompt":{"content":[{"role":"user","content":"I am testing error handling in tool paramter parsing. I want you to call the calculator tool but omit the input parameter to intentionally differ from the tool schema.","options":{}},{"role":"assistant","content":[{"type":"reasoning","text":"","options":{}},{"type":"text","text":"I'll help you test the error handling. Let me call the Calculator tool while intentionally omitting the required `input` parameter:","options":{}},{"type":"tool-call","id":"toolu_01FKjZtYwnbyrMzyWH6fgK4k","name":"Calculator","params":{},"providerExecuted":false,"options":{}}],"options":{}},{"role":"tool","content":[{"type":"tool-result","id":"toolu_01FKjZtYwnbyrMzyWH6fgK4k","name":"Calculator","isFailure":true,"result":"MalformedOutput: { \"module\": \"Toolkit\", \"method\": \"Calculator.handle\", \"description\": \"Failed to decode tool call parameters for tool 'Calculator' from:\\n'{}'\", \"cause\": { readonly input: string }\n└─ [\"input\"]\n └─ is missing }\ncaused by:\nParseError: { readonly input: string }\n└─ [\"input\"]\n └─ is missing","providerExecuted":false,"options":{}}],"options":{"anthropic":{"cacheControl":{"ttl":"5m","type":"ephemeral"}}}}]},"response":[{"type":"response-metadata","id":"msg_01HjxuVYH2y4aPHMh5KcRc2E","modelId":"claude-opus-4-8","timestamp":"1970-01-01T00:00:00.000Z","metadata":{}},{"type":"text-start","id":"0","metadata":{}},{"type":"text-delta","id":"0","delta":"The","metadata":{}},{"type":"text-delta","id":"0","delta":" error handling worked as expected. Here's what happened when I called the Calculator tool without the required `input` parameter:\n\n**Error Type:** `MalformedOut","metadata":{}},{"type":"text-delta","id":"0","delta":"put` / `ParseError`\n\n**Key details from the error:**\n- **Module:** `Toolkit`\n- **Method:** `Calculator.hand","metadata":{}},{"type":"text-delta","id":"0","delta":"le`\n- **Description:** Failed to decode tool call parameters for tool 'Calculator' from the empty object `{}`\n- **Root cause:** The schema exp","metadata":{}},{"type":"text-delta","id":"0","delta":"ects `{ readonly input: string }`, but the `[\"input\"]` field **is missing**\n\nThe parser correctly:\n1. Det","metadata":{}},{"type":"text-delta","id":"0","delta":"ected that the `input` parameter was absent from the tool call\n2. Identified exactly which field was missing (`[\"input\"]`)\n3.","metadata":{}},{"type":"text-delta","id":"0","delta":" Reported the expected schema type (`{ readonly input: string }`)\n4. Returned a descriptive error rather than cr","metadata":{}},{"type":"text-delta","id":"0","delta":"ashing or passing through invalid data\n\nThis confirms your parameter validation is functioning properly—it r","metadata":{}},{"type":"text-delta","id":"0","delta":"ejects malformed tool calls that don't conform to the schema and provides a clear, traceable error message pointing to the specific missing field. Would you like to test any other error","metadata":{}},{"type":"text-delta","id":"0","delta":" cases (e.g., wrong type for `input`, or extra unexpected parameters)?","metadata":{}},{"type":"text-end","id":"0","metadata":{}},{"type":"finish","reason":"stop","usage":{"inputTokens":676,"outputTokens":381,"totalTokens":1057},"metadata":{"anthropic":{"usage":{"cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":676,"output_tokens":1,"server_tool_use":null,"service_tier":"standard"}}}}]},{"parameters":{"model":"dxn:com.anthropic.model.claude-opus-4-8.default","stream":false,"tools":[]},"prompt":{"content":[{"role":"system","content":"<instructions>\n You are an agent that summarizes conversations.\n You will be given a conversation and you will need to summarize it to compact context.\n Your summary will be used by AI model to continue the conversation without seeing the original messages.\n Remove unnecessary details and focus on the most important information.\n Keep user requests, method of action, and progress.\n Keep the important objects and references in original format.\n It is very important you maintain unchanged ECHO DXNs and IDs of the objects.\n Output only the summary.\n</instructions>\n\n<example_output>\n Processing a flight booking confirmation email. The user requested a round-trip flight from Warsaw to Barcelona for March 15–22, 2026.\n\n Action taken:\n - Found existing booking object `echo:/01KHZWKQ9HPJYG147HV7CP5ARX` (type `echo:/01KHXAMH4F3V94RCYC7P7M2H0C` / `com.example.type.flight-booking`) matching confirmation code ABC123.\n - Updated the booking status from \"pending\" to \"confirmed\".\n - Added flight details: Outbound WAW→BCN Mar 15 08:30, Return BCN→WAW Mar 22 14:15, carrier LOT Polish Airlines.\n - Stored passenger info and seat assignments in notes.\n\n Booking details preserved: Confirmation ABC123, total 1,240 PLN, booked 2026-02-18, airline LOT Polish Airlines, flight numbers LO391/LO392.\n\n Status enum mapping for the booking schema: pending=Awaiting payment, confirmed=Booked, cancelled=Cancelled, completed=Flown.\n</example_output>\n\nTASK: Reply with the summary of the prior conversation.","options":{}},{"role":"user","content":"How many apples are in the basket?","options":{}},{"role":"assistant","content":"There are 10 apples in the basket.","options":{}},{"role":"user","content":"<instructions>\n You are an agent that summarizes conversations.\n You will be given a conversation and you will need to summarize it to compact context.\n Your summary will be used by AI model to continue the conversation without seeing the original messages.\n Remove unnecessary details and focus on the most important information.\n Keep user requests, method of action, and progress.\n Keep the important objects and references in original format.\n It is very important you maintain unchanged ECHO DXNs and IDs of the objects.\n Output only the summary.\n</instructions>\n\n<example_output>\n Processing a flight booking confirmation email. The user requested a round-trip flight from Warsaw to Barcelona for March 15–22, 2026.\n\n Action taken:\n - Found existing booking object `echo:/01KHZWKQ9HPJYG147HV7CP5ARX` (type `echo:/01KHXAMH4F3V94RCYC7P7M2H0C` / `com.example.type.flight-booking`) matching confirmation code ABC123.\n - Updated the booking status from \"pending\" to \"confirmed\".\n - Added flight details: Outbound WAW→BCN Mar 15 08:30, Return BCN→WAW Mar 22 14:15, carrier LOT Polish Airlines.\n - Stored passenger info and seat assignments in notes.\n\n Booking details preserved: Confirmation ABC123, total 1,240 PLN, booked 2026-02-18, airline LOT Polish Airlines, flight numbers LO391/LO392.\n\n Status enum mapping for the booking schema: pending=Awaiting payment, confirmed=Booked, cancelled=Cancelled, completed=Flown.\n</example_output>\n\nTASK: Reply with the summary of the prior conversation.","options":{}}]},"response":[{"type":"response-metadata","id":"msg_01HCQLhHL7h9m5bCqHbxMt3M","modelId":"claude-opus-4-8","timestamp":"1970-01-01T00:00:00.000Z","metadata":{}},{"type":"text","text":"The user asked how many apples are in the basket. I answered that there are 10 apples in the basket.","metadata":{}},{"type":"finish","reason":"stop","usage":{"inputTokens":1203,"outputTokens":32,"totalTokens":1235,"cachedInputTokens":0},"metadata":{"anthropic":{"usage":{"cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":1203,"output_tokens":32,"server_tool_use":null,"service_tier":"standard"}}}}]},{"parameters":{"model":"dxn:com.anthropic.model.claude-opus-4-8.default","stream":true,"tools":[]},"prompt":{"content":[{"role":"assistant","content":"This is the continuation of a conversation that was compacted to preserve context-window space. Summary of what happened in this conversation previously: <summary>The user asked how many apples are in the basket. I answered that there are 10 apples in the basket.</summary>","options":{}},{"role":"user","content":"What did we talk about?","options":{"anthropic":{"cacheControl":{"ttl":"5m","type":"ephemeral"}}}}]},"response":[{"type":"response-metadata","id":"msg_01Xf6mR2pWbx3WLv6kwbLQiS","modelId":"claude-opus-4-8","timestamp":"1970-01-01T00:00:00.000Z","metadata":{}},{"type":"reasoning-start","id":"0","metadata":{}},{"type":"reasoning-delta","id":"0","delta":"","metadata":{"anthropic":{"type":"thinking","signature":"Ev0CCmMIDxgCKkBWs7g6G04DqgFvDZZ+e48xE0cc3EYv160jAgqxm5x2mLho7S+B8QaCfaBDfhLvDsYx3Bnxeig672zRkNSx4/zqMg9jbGF1ZGUtb3B1cy00LTg4AEIIdGhpbmtpbmcSDBlkR5QuxfQfXtLzfxoMSdaRl5SRKqYT3fm9IjC6COGxxqy0hBEoOowzEm/CyFwmPHgVTFuJxy866+hi5qfXTtmoF3nolcIGXsW9iJEqxwG9ks2sto1fbfLCcP+n+4RTylUusnjtMYYZ84cBeOxvtgPaiw86zfrrK2XR/+jJ4QcP6ugRIe+fxiaL1HNIT2fA0dvN2+FyKMw8ADm+rMZDE4q+6dUmMa2FQOVRYlnmW+XA7VPm9ogmsaRRNm8EemVKITV5N9P4Fbm/k+OqHGnTgc6kjWcRBk8O3jBQBVwEIRX4qtlxyIhUlfh/rP47hFGfu+OgOWtiOob8Bb8AU71ZFVvw+qW2+RGRq+dAMxbaWDAxW52/WoUzGAE="}}},{"type":"reasoning-end","id":"0","metadata":{}},{"type":"text-start","id":"1","metadata":{}},{"type":"text-delta","id":"1","delta":"Based on our earlier conversation, we talked about how many apples are in the basket","metadata":{}},{"type":"text-delta","id":"1","delta":". You asked me that question, and I told you there are 10 apples in the basket.\n\nIs there anything else you'd like to know, or would you like to continue from","metadata":{}},{"type":"text-delta","id":"1","delta":" there?","metadata":{}},{"type":"text-end","id":"1","metadata":{}},{"type":"finish","reason":"stop","usage":{"inputTokens":114,"outputTokens":126,"totalTokens":240},"metadata":{"anthropic":{"usage":{"cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":114,"output_tokens":2,"server_tool_use":null,"service_tier":"standard"}}}}]}]}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Tool from '@effect/ai/Tool';
|
|
6
|
+
import * as Toolkit from '@effect/ai/Toolkit';
|
|
7
|
+
import { describe, expect, it } from '@effect/vitest';
|
|
8
|
+
import * as Effect from 'effect/Effect';
|
|
9
|
+
import * as Layer from 'effect/Layer';
|
|
10
|
+
import * as Schema from 'effect/Schema';
|
|
11
|
+
|
|
12
|
+
import { OpaqueToolkit } from '@dxos/ai';
|
|
13
|
+
import { AiRequest, ToolExecutionServices } from '@dxos/assistant';
|
|
14
|
+
import { DXN, Obj, Type } from '@dxos/echo';
|
|
15
|
+
import { TestHelpers } from '@dxos/effect/testing';
|
|
16
|
+
import { log } from '@dxos/log';
|
|
17
|
+
import { Message } from '@dxos/types';
|
|
18
|
+
|
|
19
|
+
import { AssistantTestLayer } from '../testing';
|
|
20
|
+
|
|
21
|
+
// Define a calendar event artifact schema.
|
|
22
|
+
class CalendarEvent extends Type.makeObject<CalendarEvent>(DXN.make('com.example.type.calendarEvent', '0.1.0'))(
|
|
23
|
+
Schema.Struct({
|
|
24
|
+
title: Schema.String,
|
|
25
|
+
startTime: Schema.String,
|
|
26
|
+
endTime: Schema.String,
|
|
27
|
+
description: Schema.String,
|
|
28
|
+
}),
|
|
29
|
+
) {}
|
|
30
|
+
|
|
31
|
+
const TestToolkit = Toolkit.make(
|
|
32
|
+
Tool.make('Calculator', {
|
|
33
|
+
description: 'Basic calculator tool',
|
|
34
|
+
parameters: {
|
|
35
|
+
input: Schema.String.annotations({
|
|
36
|
+
description: 'The calculation to perform.',
|
|
37
|
+
}),
|
|
38
|
+
},
|
|
39
|
+
success: Schema.Struct({
|
|
40
|
+
result: Schema.Number,
|
|
41
|
+
}),
|
|
42
|
+
failure: Schema.Never,
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Tool handlers.
|
|
47
|
+
const toolkitLayer = TestToolkit.toLayer({
|
|
48
|
+
Calculator: ({ input }) =>
|
|
49
|
+
Effect.gen(function* () {
|
|
50
|
+
const result = (() => {
|
|
51
|
+
// Restrict to basic arithmetic operations for safety.
|
|
52
|
+
const sanitizedInput = input.replace(/[^0-9+\-*/().\s]/g, '');
|
|
53
|
+
log.info('calculate', { sanitizedInput });
|
|
54
|
+
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
56
|
+
return Function(`"use strict"; return (${sanitizedInput})`)();
|
|
57
|
+
})();
|
|
58
|
+
|
|
59
|
+
return { result };
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const TestLayer = Layer.empty.pipe(
|
|
64
|
+
Layer.provideMerge(ToolExecutionServices),
|
|
65
|
+
Layer.provideMerge(
|
|
66
|
+
AssistantTestLayer({
|
|
67
|
+
types: [CalendarEvent],
|
|
68
|
+
tracing: 'pretty',
|
|
69
|
+
}),
|
|
70
|
+
),
|
|
71
|
+
Layer.provideMerge(toolkitLayer),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
describe('AiRequest.Request', () => {
|
|
75
|
+
it.effect(
|
|
76
|
+
'no tools',
|
|
77
|
+
Effect.fnUntraced(
|
|
78
|
+
function* (_) {
|
|
79
|
+
const request = new AiRequest.Request();
|
|
80
|
+
const response = yield* request.run({
|
|
81
|
+
prompt: 'Hello world!',
|
|
82
|
+
history: [],
|
|
83
|
+
});
|
|
84
|
+
log.info('response', { response });
|
|
85
|
+
},
|
|
86
|
+
Effect.provide(TestLayer),
|
|
87
|
+
TestHelpers.provideTestContext,
|
|
88
|
+
),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
it.effect(
|
|
92
|
+
'calculator',
|
|
93
|
+
Effect.fnUntraced(
|
|
94
|
+
function* (_) {
|
|
95
|
+
const request = new AiRequest.Request();
|
|
96
|
+
const toolkit = yield* OpaqueToolkit.fromContext(TestToolkit);
|
|
97
|
+
const response = yield* request.run({
|
|
98
|
+
toolkit,
|
|
99
|
+
prompt: 'What is 10 + 30?',
|
|
100
|
+
history: [],
|
|
101
|
+
});
|
|
102
|
+
log.info('response', { response });
|
|
103
|
+
},
|
|
104
|
+
Effect.provide(TestLayer),
|
|
105
|
+
TestHelpers.provideTestContext,
|
|
106
|
+
),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
it.effect(
|
|
110
|
+
'tool schema error',
|
|
111
|
+
Effect.fnUntraced(
|
|
112
|
+
function* (_) {
|
|
113
|
+
const request = new AiRequest.Request();
|
|
114
|
+
const toolkit = yield* OpaqueToolkit.fromContext(TestToolkit);
|
|
115
|
+
const response = yield* request.run({
|
|
116
|
+
toolkit,
|
|
117
|
+
prompt:
|
|
118
|
+
'I am testing error handling in tool paramter parsing. I want you to call the calculator tool but omit the input parameter to intentionally differ from the tool schema.',
|
|
119
|
+
history: [],
|
|
120
|
+
});
|
|
121
|
+
log.info('response', { response });
|
|
122
|
+
},
|
|
123
|
+
Effect.provide(TestLayer),
|
|
124
|
+
TestHelpers.provideTestContext,
|
|
125
|
+
),
|
|
126
|
+
{ timeout: 60_000 },
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
it.effect(
|
|
130
|
+
'summarization',
|
|
131
|
+
Effect.fnUntraced(
|
|
132
|
+
function* (_) {
|
|
133
|
+
const request = new AiRequest.Request({ summarizationThreshold: 0 }); // Force summarization.
|
|
134
|
+
const response = yield* request.run({
|
|
135
|
+
prompt: 'What did we talk about?',
|
|
136
|
+
history: [
|
|
137
|
+
Obj.make(Message.Message, {
|
|
138
|
+
created: '2024-01-01T10:00:00Z',
|
|
139
|
+
sender: { role: 'user' },
|
|
140
|
+
blocks: [{ _tag: 'text', text: 'How many apples are in the basket?' }],
|
|
141
|
+
}),
|
|
142
|
+
Obj.make(Message.Message, {
|
|
143
|
+
created: '2024-01-01T11:00:00Z',
|
|
144
|
+
sender: { role: 'assistant' },
|
|
145
|
+
blocks: [{ _tag: 'text', text: 'There are 10 apples in the basket.' }],
|
|
146
|
+
}),
|
|
147
|
+
],
|
|
148
|
+
});
|
|
149
|
+
expect(response).toBeDefined();
|
|
150
|
+
},
|
|
151
|
+
Effect.provide(TestLayer),
|
|
152
|
+
TestHelpers.provideTestContext,
|
|
153
|
+
),
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Travel to rome, florence, livorno, siena, madrid for conferences
|
|
158
|
+
|
|
159
|
+
const _CALENDAR_EVENTS: CalendarEvent[] = [
|
|
160
|
+
Obj.make(CalendarEvent, {
|
|
161
|
+
title: 'Exploring Ancient Ruins in Rome',
|
|
162
|
+
startTime: '2024-01-01T10:00:00Z',
|
|
163
|
+
endTime: '2024-01-01T11:00:00Z',
|
|
164
|
+
description: 'Tech conference at the historic Colosseum with networking opportunities',
|
|
165
|
+
}),
|
|
166
|
+
Obj.make(CalendarEvent, {
|
|
167
|
+
title: 'Renaissance Tech Summit in Florence',
|
|
168
|
+
startTime: '2024-01-01T11:00:00Z',
|
|
169
|
+
endTime: '2024-01-01T12:00:00Z',
|
|
170
|
+
description: 'Discussing AI innovations surrounded by Renaissance art',
|
|
171
|
+
}),
|
|
172
|
+
Obj.make(CalendarEvent, {
|
|
173
|
+
title: 'Travel to Livorno',
|
|
174
|
+
startTime: '2024-01-01T12:00:00Z',
|
|
175
|
+
endTime: '2024-01-01T13:00:00Z',
|
|
176
|
+
description: 'Travel to Livorno',
|
|
177
|
+
}),
|
|
178
|
+
Obj.make(CalendarEvent, {
|
|
179
|
+
title: 'Travel to Siena',
|
|
180
|
+
startTime: '2024-01-01T13:00:00Z',
|
|
181
|
+
endTime: '2024-01-01T14:00:00Z',
|
|
182
|
+
description: 'Travel to Siena',
|
|
183
|
+
}),
|
|
184
|
+
Obj.make(CalendarEvent, {
|
|
185
|
+
title: 'Travel to Madrid',
|
|
186
|
+
startTime: '2024-01-01T14:00:00Z',
|
|
187
|
+
endTime: '2024-01-01T15:00:00Z',
|
|
188
|
+
description: 'Travel to Madrid',
|
|
189
|
+
}),
|
|
190
|
+
];
|
|
@@ -0,0 +1,80 @@
|
|
|
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 Layer from 'effect/Layer';
|
|
8
|
+
|
|
9
|
+
import { OpaqueToolkit } from '@dxos/ai';
|
|
10
|
+
import { CalculatorLayer, CalculatorToolkit } from '@dxos/ai/testing';
|
|
11
|
+
import { AiRequest, ToolExecutionServices } from '@dxos/assistant';
|
|
12
|
+
import { TestHelpers } from '@dxos/effect/testing';
|
|
13
|
+
import { DXN } from '@dxos/keys';
|
|
14
|
+
import { dbg, log } from '@dxos/log';
|
|
15
|
+
import { ContentBlock } from '@dxos/types';
|
|
16
|
+
|
|
17
|
+
import { AssistantTestLayer } from '../testing';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Exercises the full AiRequest tool-call loop against a local Ollama instance.
|
|
21
|
+
*
|
|
22
|
+
* Prerequisites:
|
|
23
|
+
* - `ollama serve` running on `localhost:11434` with the `gpt-oss:20b` model pulled.
|
|
24
|
+
*
|
|
25
|
+
* Run with:
|
|
26
|
+
* ```bash
|
|
27
|
+
* DX_TEST_TAGS=llm pnpm --filter @dxos/functions-runtime exec vitest run session-ollama
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const TestLayer = Layer.empty.pipe(
|
|
32
|
+
Layer.provideMerge(ToolExecutionServices),
|
|
33
|
+
Layer.provideMerge(
|
|
34
|
+
AssistantTestLayer({
|
|
35
|
+
tracing: 'pretty',
|
|
36
|
+
aiServicePreset: 'ollama',
|
|
37
|
+
model: DXN.make('com.openai.model.gpt-oss-20b.default'),
|
|
38
|
+
disableLlmMemoization: true,
|
|
39
|
+
}),
|
|
40
|
+
),
|
|
41
|
+
Layer.provideMerge(CalculatorLayer),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
describe('AiRequest.Request (ollama gpt-oss:20b)', () => {
|
|
45
|
+
it.effect(
|
|
46
|
+
'calculator tool-call loop',
|
|
47
|
+
Effect.fn(
|
|
48
|
+
function* ({ expect }) {
|
|
49
|
+
const request = new AiRequest.Request();
|
|
50
|
+
const toolkit = yield* OpaqueToolkit.fromContext(CalculatorToolkit);
|
|
51
|
+
|
|
52
|
+
const messages = yield* request.run({
|
|
53
|
+
toolkit,
|
|
54
|
+
prompt: 'What is six times seven? Use the Calculator tool and reply with only the number.',
|
|
55
|
+
history: [],
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
dbg(messages);
|
|
59
|
+
|
|
60
|
+
const toolCalls = messages.flatMap((m) => m.blocks).filter(ContentBlock.is('toolCall'));
|
|
61
|
+
const toolResults = messages.flatMap((m) => m.blocks).filter(ContentBlock.is('toolResult'));
|
|
62
|
+
|
|
63
|
+
log.info('request result', {
|
|
64
|
+
messageCount: messages.length,
|
|
65
|
+
toolCalls: toolCalls.length,
|
|
66
|
+
toolResults: toolResults.length,
|
|
67
|
+
requestToolCalls: request.toolCalls,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(toolCalls.length).toBeGreaterThan(0);
|
|
71
|
+
expect(toolCalls.some((call) => call.name === 'Calculator')).toBe(true);
|
|
72
|
+
expect(toolResults.length).toBeGreaterThan(0);
|
|
73
|
+
expect(toolResults.some((result) => String(result.result).includes('42'))).toBe(true);
|
|
74
|
+
},
|
|
75
|
+
Effect.provide(TestLayer),
|
|
76
|
+
TestHelpers.provideTestContext,
|
|
77
|
+
),
|
|
78
|
+
{ timeout: 120_000, tags: ['manual'] },
|
|
79
|
+
);
|
|
80
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { it } from '@effect/vitest';
|
|
6
|
+
import * as Effect from 'effect/Effect';
|
|
7
|
+
import { describe, expect } from 'vitest';
|
|
8
|
+
|
|
9
|
+
import { AiContext, AiSession } from '@dxos/assistant';
|
|
10
|
+
import { Skill } from '@dxos/compute';
|
|
11
|
+
import { Database, Feed, Obj, Ref } from '@dxos/echo';
|
|
12
|
+
import { TestDatabaseLayer } from '@dxos/echo-client/testing';
|
|
13
|
+
|
|
14
|
+
describe('AiSession', () => {
|
|
15
|
+
const TestLayer = TestDatabaseLayer({
|
|
16
|
+
types: [Skill.Skill, AiContext.Binding],
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it.effect('loads skills on open', () =>
|
|
20
|
+
Effect.gen(function* () {
|
|
21
|
+
const { db } = yield* Database.Service;
|
|
22
|
+
|
|
23
|
+
// Create feed.
|
|
24
|
+
const feed = db.add(Feed.make());
|
|
25
|
+
|
|
26
|
+
// Create skill.
|
|
27
|
+
const skill = db.add(
|
|
28
|
+
Skill.make({
|
|
29
|
+
key: 'com.example.skill.test',
|
|
30
|
+
name: 'Test Skill',
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Add skill to feed via binding.
|
|
35
|
+
yield* Feed.append(feed, [
|
|
36
|
+
Obj.make(AiContext.Binding, {
|
|
37
|
+
skills: {
|
|
38
|
+
added: [Ref.make(skill)],
|
|
39
|
+
removed: [],
|
|
40
|
+
},
|
|
41
|
+
objects: {
|
|
42
|
+
added: [],
|
|
43
|
+
removed: [],
|
|
44
|
+
},
|
|
45
|
+
}),
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
const runtime = yield* Effect.runtime<Database.Service>();
|
|
49
|
+
const session = new AiSession.Session({ feed, runtime });
|
|
50
|
+
yield* Effect.promise(() => session.open());
|
|
51
|
+
|
|
52
|
+
expect(session.context.getSkills()).toHaveLength(1);
|
|
53
|
+
expect(session.context.getObjects()).toHaveLength(0);
|
|
54
|
+
}).pipe(Effect.provide(TestLayer)),
|
|
55
|
+
);
|
|
56
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"conversations":[{"parameters":{"model":"dxn:com.anthropic.model.claude-opus-4-8.default","stream":true,"tools":[]},"prompt":{"content":[{"role":"user","content":"Respond with your name inside an xml tag (use any tag name you like).","options":{"anthropic":{"cacheControl":{"ttl":"5m","type":"ephemeral"}}}}]},"response":[{"type":"response-metadata","id":"msg_01HHSwQbKCrCJ6BYESA4BpLb","modelId":"claude-opus-4-8","timestamp":"1970-01-01T00:00:00.000Z","metadata":{}},{"type":"text-start","id":"0","metadata":{}},{"type":"text-delta","id":"0","delta":"<name","metadata":{}},{"type":"text-delta","id":"0","delta":">Claude</name>","metadata":{}},{"type":"text-end","id":"0","metadata":{}},{"type":"finish","reason":"stop","usage":{"inputTokens":28,"outputTokens":11,"totalTokens":39},"metadata":{"anthropic":{"usage":{"cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":28,"output_tokens":2,"server_tool_use":null,"service_tier":"standard"}}}}]}]}
|
|
@@ -0,0 +1,65 @@
|
|
|
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 Layer from 'effect/Layer';
|
|
8
|
+
|
|
9
|
+
import { AiRequest, ToolExecutionServices } from '@dxos/assistant';
|
|
10
|
+
import { TestHelpers } from '@dxos/effect/testing';
|
|
11
|
+
import { log } from '@dxos/log';
|
|
12
|
+
|
|
13
|
+
import { AssistantTestLayer } from '../testing';
|
|
14
|
+
|
|
15
|
+
const TestLayer = Layer.empty.pipe(
|
|
16
|
+
Layer.provideMerge(ToolExecutionServices),
|
|
17
|
+
Layer.provideMerge(AssistantTestLayer({ tracing: 'noop' })),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
describe('AiRequest.Request xml response', () => {
|
|
21
|
+
// End-to-end regression: drive a real (memoized) LLM call via `AiRequest` and inspect the
|
|
22
|
+
// streamed blocks. Original bug: when the user prompted "respond with your name inside an
|
|
23
|
+
// xml tag", the model emitted `<name>Claude</name>` as text, but `AiParser.makeContentBlock`
|
|
24
|
+
// returned `undefined` for any unrecognized tag, silently dropping the entire response.
|
|
25
|
+
// The chat UI then showed only the reasoning preamble. This test guards against future
|
|
26
|
+
// regressions further down the parser/preprocessor path that might re-introduce the drop.
|
|
27
|
+
it.effect(
|
|
28
|
+
'response with xml tag emits a text block',
|
|
29
|
+
Effect.fn(
|
|
30
|
+
function* ({ expect }) {
|
|
31
|
+
const request = new AiRequest.Request();
|
|
32
|
+
const messages = yield* request.run({
|
|
33
|
+
prompt: 'Respond with your name inside an xml tag (use any tag name you like).',
|
|
34
|
+
history: [],
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const summary = messages.map((message) => ({
|
|
38
|
+
role: message.sender.role,
|
|
39
|
+
blocks: message.blocks.map((block) => ({
|
|
40
|
+
tag: block._tag,
|
|
41
|
+
preview: JSON.stringify(block).slice(0, 240),
|
|
42
|
+
})),
|
|
43
|
+
}));
|
|
44
|
+
log.info('llm messages', { summary });
|
|
45
|
+
|
|
46
|
+
const assistantBlocks = messages
|
|
47
|
+
.filter((message) => message.sender.role === 'assistant')
|
|
48
|
+
.flatMap((message) => message.blocks);
|
|
49
|
+
|
|
50
|
+
const reasoningBlocks = assistantBlocks.filter((block) => block._tag === 'reasoning');
|
|
51
|
+
const textBlocks = assistantBlocks.filter((block) => block._tag === 'text');
|
|
52
|
+
log.info('block summary', { reasoning: reasoningBlocks.length, text: textBlocks.length });
|
|
53
|
+
|
|
54
|
+
expect(textBlocks.length).toBeGreaterThan(0);
|
|
55
|
+
|
|
56
|
+
const combined = textBlocks.map((block) => (block as any).text ?? '').join('\n');
|
|
57
|
+
log.info('combined text', { combined });
|
|
58
|
+
expect(combined).toMatch(/<\w+[^>]*>/);
|
|
59
|
+
},
|
|
60
|
+
Effect.provide(TestLayer),
|
|
61
|
+
TestHelpers.provideTestContext,
|
|
62
|
+
),
|
|
63
|
+
{ timeout: 60_000 },
|
|
64
|
+
);
|
|
65
|
+
});
|