@dxos/assistant-toolkit 0.8.4-main.21d9917

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.
Files changed (249) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +3 -0
  3. package/dist/lib/browser/chunk-J5LGTIGS.mjs +10 -0
  4. package/dist/lib/browser/chunk-J5LGTIGS.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +2880 -0
  6. package/dist/lib/browser/index.mjs.map +7 -0
  7. package/dist/lib/browser/meta.json +1 -0
  8. package/dist/lib/browser/testing/index.mjs +43 -0
  9. package/dist/lib/browser/testing/index.mjs.map +7 -0
  10. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +11 -0
  11. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +7 -0
  12. package/dist/lib/node-esm/index.mjs +2881 -0
  13. package/dist/lib/node-esm/index.mjs.map +7 -0
  14. package/dist/lib/node-esm/meta.json +1 -0
  15. package/dist/lib/node-esm/testing/index.mjs +44 -0
  16. package/dist/lib/node-esm/testing/index.mjs.map +7 -0
  17. package/dist/types/src/blueprints/design/design-blueprint.d.ts +18 -0
  18. package/dist/types/src/blueprints/design/design-blueprint.d.ts.map +1 -0
  19. package/dist/types/src/blueprints/design/design-blueprint.test.d.ts +2 -0
  20. package/dist/types/src/blueprints/design/design-blueprint.test.d.ts.map +1 -0
  21. package/dist/types/src/blueprints/design/index.d.ts +3 -0
  22. package/dist/types/src/blueprints/design/index.d.ts.map +1 -0
  23. package/dist/types/src/blueprints/discord/discord-blueprint.d.ts +18 -0
  24. package/dist/types/src/blueprints/discord/discord-blueprint.d.ts.map +1 -0
  25. package/dist/types/src/blueprints/discord/index.d.ts +3 -0
  26. package/dist/types/src/blueprints/discord/index.d.ts.map +1 -0
  27. package/dist/types/src/blueprints/index.d.ts +7 -0
  28. package/dist/types/src/blueprints/index.d.ts.map +1 -0
  29. package/dist/types/src/blueprints/linear/index.d.ts +3 -0
  30. package/dist/types/src/blueprints/linear/index.d.ts.map +1 -0
  31. package/dist/types/src/blueprints/linear/linear-blueprint.d.ts +18 -0
  32. package/dist/types/src/blueprints/linear/linear-blueprint.d.ts.map +1 -0
  33. package/dist/types/src/blueprints/planning/index.d.ts +3 -0
  34. package/dist/types/src/blueprints/planning/index.d.ts.map +1 -0
  35. package/dist/types/src/blueprints/planning/planning-blueprint.d.ts +18 -0
  36. package/dist/types/src/blueprints/planning/planning-blueprint.d.ts.map +1 -0
  37. package/dist/types/src/blueprints/planning/planning-blueprint.test.d.ts +2 -0
  38. package/dist/types/src/blueprints/planning/planning-blueprint.test.d.ts.map +1 -0
  39. package/dist/types/src/blueprints/research/index.d.ts +3 -0
  40. package/dist/types/src/blueprints/research/index.d.ts.map +1 -0
  41. package/dist/types/src/blueprints/research/research-blueprint.d.ts +18 -0
  42. package/dist/types/src/blueprints/research/research-blueprint.d.ts.map +1 -0
  43. package/dist/types/src/blueprints/research/research-blueprint.test.d.ts +2 -0
  44. package/dist/types/src/blueprints/research/research-blueprint.test.d.ts.map +1 -0
  45. package/dist/types/src/blueprints/testing.d.ts +12 -0
  46. package/dist/types/src/blueprints/testing.d.ts.map +1 -0
  47. package/dist/types/src/blueprints/websearch/index.d.ts +4 -0
  48. package/dist/types/src/blueprints/websearch/index.d.ts.map +1 -0
  49. package/dist/types/src/blueprints/websearch/websearch-blueprint.d.ts +18 -0
  50. package/dist/types/src/blueprints/websearch/websearch-blueprint.d.ts.map +1 -0
  51. package/dist/types/src/blueprints/websearch/websearch-toolkit.d.ts +27 -0
  52. package/dist/types/src/blueprints/websearch/websearch-toolkit.d.ts.map +1 -0
  53. package/dist/types/src/crud/graph.d.ts +67 -0
  54. package/dist/types/src/crud/graph.d.ts.map +1 -0
  55. package/dist/types/src/crud/graph.test.d.ts +2 -0
  56. package/dist/types/src/crud/graph.test.d.ts.map +1 -0
  57. package/dist/types/src/crud/index.d.ts +2 -0
  58. package/dist/types/src/crud/index.d.ts.map +1 -0
  59. package/dist/types/src/experimental/feed.test.d.ts +2 -0
  60. package/dist/types/src/experimental/feed.test.d.ts.map +1 -0
  61. package/dist/types/src/functions/agent/index.d.ts +5 -0
  62. package/dist/types/src/functions/agent/index.d.ts.map +1 -0
  63. package/dist/types/src/functions/agent/prompt.d.ts +76 -0
  64. package/dist/types/src/functions/agent/prompt.d.ts.map +1 -0
  65. package/dist/types/src/functions/discord/fetch-messages.d.ts +11 -0
  66. package/dist/types/src/functions/discord/fetch-messages.d.ts.map +1 -0
  67. package/dist/types/src/functions/discord/fetch-messages.test.d.ts +2 -0
  68. package/dist/types/src/functions/discord/fetch-messages.test.d.ts.map +1 -0
  69. package/dist/types/src/functions/discord/index.d.ts +12 -0
  70. package/dist/types/src/functions/discord/index.d.ts.map +1 -0
  71. package/dist/types/src/functions/document/index.d.ts +13 -0
  72. package/dist/types/src/functions/document/index.d.ts.map +1 -0
  73. package/dist/types/src/functions/document/read.d.ts +7 -0
  74. package/dist/types/src/functions/document/read.d.ts.map +1 -0
  75. package/dist/types/src/functions/document/update.d.ts +6 -0
  76. package/dist/types/src/functions/document/update.d.ts.map +1 -0
  77. package/dist/types/src/functions/entity-extraction/entity-extraction.d.ts +174 -0
  78. package/dist/types/src/functions/entity-extraction/entity-extraction.d.ts.map +1 -0
  79. package/dist/types/src/functions/entity-extraction/entity-extraction.test.d.ts +2 -0
  80. package/dist/types/src/functions/entity-extraction/entity-extraction.test.d.ts.map +1 -0
  81. package/dist/types/src/functions/entity-extraction/index.d.ts +175 -0
  82. package/dist/types/src/functions/entity-extraction/index.d.ts.map +1 -0
  83. package/dist/types/src/functions/exa/data/exa-search-1748337321991.d.ts +38 -0
  84. package/dist/types/src/functions/exa/data/exa-search-1748337321991.d.ts.map +1 -0
  85. package/dist/types/src/functions/exa/data/exa-search-1748337331526.d.ts +37 -0
  86. package/dist/types/src/functions/exa/data/exa-search-1748337331526.d.ts.map +1 -0
  87. package/dist/types/src/functions/exa/data/exa-search-1748337344119.d.ts +58 -0
  88. package/dist/types/src/functions/exa/data/exa-search-1748337344119.d.ts.map +1 -0
  89. package/dist/types/src/functions/exa/data/index.d.ts +3 -0
  90. package/dist/types/src/functions/exa/data/index.d.ts.map +1 -0
  91. package/dist/types/src/functions/exa/exa.d.ts +5 -0
  92. package/dist/types/src/functions/exa/exa.d.ts.map +1 -0
  93. package/dist/types/src/functions/exa/index.d.ts +3 -0
  94. package/dist/types/src/functions/exa/index.d.ts.map +1 -0
  95. package/dist/types/src/functions/exa/mock.d.ts +5 -0
  96. package/dist/types/src/functions/exa/mock.d.ts.map +1 -0
  97. package/dist/types/src/functions/github/fetch-prs.d.ts +6 -0
  98. package/dist/types/src/functions/github/fetch-prs.d.ts.map +1 -0
  99. package/dist/types/src/functions/index.d.ts +8 -0
  100. package/dist/types/src/functions/index.d.ts.map +1 -0
  101. package/dist/types/src/functions/linear/index.d.ts +9 -0
  102. package/dist/types/src/functions/linear/index.d.ts.map +1 -0
  103. package/dist/types/src/functions/linear/linear.test.d.ts +2 -0
  104. package/dist/types/src/functions/linear/linear.test.d.ts.map +1 -0
  105. package/dist/types/src/functions/linear/sync-issues.d.ts +12 -0
  106. package/dist/types/src/functions/linear/sync-issues.d.ts.map +1 -0
  107. package/dist/types/src/functions/research/document-create.d.ts +9 -0
  108. package/dist/types/src/functions/research/document-create.d.ts.map +1 -0
  109. package/dist/types/src/functions/research/index.d.ts +21 -0
  110. package/dist/types/src/functions/research/index.d.ts.map +1 -0
  111. package/dist/types/src/functions/research/research-graph.d.ts +19 -0
  112. package/dist/types/src/functions/research/research-graph.d.ts.map +1 -0
  113. package/dist/types/src/functions/research/research.d.ts +14 -0
  114. package/dist/types/src/functions/research/research.d.ts.map +1 -0
  115. package/dist/types/src/functions/research/research.test.d.ts +2 -0
  116. package/dist/types/src/functions/research/research.test.d.ts.map +1 -0
  117. package/dist/types/src/functions/research/types.d.ts +6 -0
  118. package/dist/types/src/functions/research/types.d.ts.map +1 -0
  119. package/dist/types/src/functions/tasks/index.d.ts +15 -0
  120. package/dist/types/src/functions/tasks/index.d.ts.map +1 -0
  121. package/dist/types/src/functions/tasks/read.d.ts +7 -0
  122. package/dist/types/src/functions/tasks/read.d.ts.map +1 -0
  123. package/dist/types/src/functions/tasks/task-list.d.ts +74 -0
  124. package/dist/types/src/functions/tasks/task-list.d.ts.map +1 -0
  125. package/dist/types/src/functions/tasks/task-list.test.d.ts +2 -0
  126. package/dist/types/src/functions/tasks/task-list.test.d.ts.map +1 -0
  127. package/dist/types/src/functions/tasks/update.d.ts +9 -0
  128. package/dist/types/src/functions/tasks/update.d.ts.map +1 -0
  129. package/dist/types/src/index.d.ts +6 -0
  130. package/dist/types/src/index.d.ts.map +1 -0
  131. package/dist/types/src/initiative/Initiative.d.ts +84 -0
  132. package/dist/types/src/initiative/Initiative.d.ts.map +1 -0
  133. package/dist/types/src/initiative/InitiativeSchema.d.ts +19 -0
  134. package/dist/types/src/initiative/InitiativeSchema.d.ts.map +1 -0
  135. package/dist/types/src/initiative/functions/agent.d.ts +37 -0
  136. package/dist/types/src/initiative/functions/agent.d.ts.map +1 -0
  137. package/dist/types/src/initiative/functions/getContext.d.ts +17 -0
  138. package/dist/types/src/initiative/functions/getContext.d.ts.map +1 -0
  139. package/dist/types/src/initiative/functions/index.d.ts +4 -0
  140. package/dist/types/src/initiative/functions/index.d.ts.map +1 -0
  141. package/dist/types/src/initiative/functions/update.d.ts +7 -0
  142. package/dist/types/src/initiative/functions/update.d.ts.map +1 -0
  143. package/dist/types/src/initiative/index.d.ts +1 -0
  144. package/dist/types/src/initiative/index.d.ts.map +1 -0
  145. package/dist/types/src/initiative/initiative.test.d.ts +2 -0
  146. package/dist/types/src/initiative/initiative.test.d.ts.map +1 -0
  147. package/dist/types/src/sync/index.d.ts +2 -0
  148. package/dist/types/src/sync/index.d.ts.map +1 -0
  149. package/dist/types/src/sync/sync.d.ts +15 -0
  150. package/dist/types/src/sync/sync.d.ts.map +1 -0
  151. package/dist/types/src/testing/index.d.ts +2 -0
  152. package/dist/types/src/testing/index.d.ts.map +1 -0
  153. package/dist/types/src/testing/plugins.d.ts +19 -0
  154. package/dist/types/src/testing/plugins.d.ts.map +1 -0
  155. package/dist/types/src/toolkits/AssistantToolkit.d.ts +43 -0
  156. package/dist/types/src/toolkits/AssistantToolkit.d.ts.map +1 -0
  157. package/dist/types/src/toolkits/AssistantToolkit.test.d.ts +2 -0
  158. package/dist/types/src/toolkits/AssistantToolkit.test.d.ts.map +1 -0
  159. package/dist/types/src/toolkits/SystemToolkit.d.ts +99 -0
  160. package/dist/types/src/toolkits/SystemToolkit.d.ts.map +1 -0
  161. package/dist/types/src/toolkits/index.d.ts +3 -0
  162. package/dist/types/src/toolkits/index.d.ts.map +1 -0
  163. package/dist/types/src/util/graphql.d.ts +22 -0
  164. package/dist/types/src/util/graphql.d.ts.map +1 -0
  165. package/dist/types/src/util/index.d.ts +2 -0
  166. package/dist/types/src/util/index.d.ts.map +1 -0
  167. package/dist/types/tsconfig.tsbuildinfo +1 -0
  168. package/package.json +83 -0
  169. package/src/blueprints/design/design-blueprint.test.ts +105 -0
  170. package/src/blueprints/design/design-blueprint.ts +31 -0
  171. package/src/blueprints/design/index.ts +7 -0
  172. package/src/blueprints/discord/discord-blueprint.ts +32 -0
  173. package/src/blueprints/discord/index.ts +7 -0
  174. package/src/blueprints/index.ts +10 -0
  175. package/src/blueprints/linear/index.ts +7 -0
  176. package/src/blueprints/linear/linear-blueprint.ts +33 -0
  177. package/src/blueprints/planning/index.ts +7 -0
  178. package/src/blueprints/planning/planning-blueprint.test.ts +126 -0
  179. package/src/blueprints/planning/planning-blueprint.ts +96 -0
  180. package/src/blueprints/research/index.ts +7 -0
  181. package/src/blueprints/research/research-blueprint.test.ts +7 -0
  182. package/src/blueprints/research/research-blueprint.ts +51 -0
  183. package/src/blueprints/testing.ts +34 -0
  184. package/src/blueprints/websearch/index.ts +9 -0
  185. package/src/blueprints/websearch/websearch-blueprint.ts +18 -0
  186. package/src/blueprints/websearch/websearch-toolkit.ts +8 -0
  187. package/src/crud/graph.test.ts +46 -0
  188. package/src/crud/graph.ts +380 -0
  189. package/src/crud/index.ts +5 -0
  190. package/src/experimental/feed.test.ts +111 -0
  191. package/src/functions/agent/index.ts +11 -0
  192. package/src/functions/agent/prompt.ts +118 -0
  193. package/src/functions/discord/fetch-messages.test.ts +58 -0
  194. package/src/functions/discord/fetch-messages.ts +252 -0
  195. package/src/functions/discord/index.ts +9 -0
  196. package/src/functions/document/index.ts +12 -0
  197. package/src/functions/document/read.ts +30 -0
  198. package/src/functions/document/update.ts +33 -0
  199. package/src/functions/entity-extraction/entity-extraction.conversations.json +6597 -0
  200. package/src/functions/entity-extraction/entity-extraction.test.ts +93 -0
  201. package/src/functions/entity-extraction/entity-extraction.ts +183 -0
  202. package/src/functions/entity-extraction/index.ts +9 -0
  203. package/src/functions/exa/data/exa-search-1748337321991.ts +131 -0
  204. package/src/functions/exa/data/exa-search-1748337331526.ts +144 -0
  205. package/src/functions/exa/data/exa-search-1748337344119.ts +133 -0
  206. package/src/functions/exa/data/index.ts +11 -0
  207. package/src/functions/exa/exa.ts +37 -0
  208. package/src/functions/exa/index.ts +6 -0
  209. package/src/functions/exa/mock.ts +71 -0
  210. package/src/functions/github/fetch-prs.ts +31 -0
  211. package/src/functions/index.ts +11 -0
  212. package/src/functions/linear/index.ts +9 -0
  213. package/src/functions/linear/linear.test.ts +84 -0
  214. package/src/functions/linear/sync-issues.ts +191 -0
  215. package/src/functions/research/document-create.ts +76 -0
  216. package/src/functions/research/index.ts +14 -0
  217. package/src/functions/research/research-graph.ts +49 -0
  218. package/src/functions/research/research-instructions.tpl +106 -0
  219. package/src/functions/research/research.conversations.json +1 -0
  220. package/src/functions/research/research.test.ts +171 -0
  221. package/src/functions/research/research.ts +189 -0
  222. package/src/functions/research/types.ts +26 -0
  223. package/src/functions/tasks/index.ts +11 -0
  224. package/src/functions/tasks/read.ts +35 -0
  225. package/src/functions/tasks/task-list.test.ts +99 -0
  226. package/src/functions/tasks/task-list.ts +165 -0
  227. package/src/functions/tasks/update.ts +53 -0
  228. package/src/index.ts +9 -0
  229. package/src/initiative/Initiative.ts +99 -0
  230. package/src/initiative/InitiativeSchema.ts +37 -0
  231. package/src/initiative/functions/agent.ts +57 -0
  232. package/src/initiative/functions/getContext.ts +74 -0
  233. package/src/initiative/functions/index.ts +7 -0
  234. package/src/initiative/functions/update.ts +63 -0
  235. package/src/initiative/index.ts +3 -0
  236. package/src/initiative/initiative.conversations.json +1 -0
  237. package/src/initiative/initiative.test.ts +485 -0
  238. package/src/sync/index.ts +5 -0
  239. package/src/sync/sync.ts +95 -0
  240. package/src/testing/index.ts +5 -0
  241. package/src/testing/plugins.tsx +68 -0
  242. package/src/toolkits/AssistantToolkit.conversations.json +1 -0
  243. package/src/toolkits/AssistantToolkit.test.ts +94 -0
  244. package/src/toolkits/AssistantToolkit.ts +70 -0
  245. package/src/toolkits/SystemToolkit.ts +299 -0
  246. package/src/toolkits/index.ts +6 -0
  247. package/src/typedefs.d.ts +8 -0
  248. package/src/util/graphql.ts +31 -0
  249. package/src/util/index.ts +5 -0
@@ -0,0 +1,171 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { inspect } from 'node:util';
6
+
7
+ import { describe, it } from '@effect/vitest';
8
+ import * as Effect from 'effect/Effect';
9
+ import * as Layer from 'effect/Layer';
10
+
11
+ import { AiService, ConsolePrinter } from '@dxos/ai';
12
+ import { MemoizedAiService, TestAiService } from '@dxos/ai/testing';
13
+ import {
14
+ AiConversation,
15
+ type ContextBinding,
16
+ GenerationObserver,
17
+ makeToolExecutionServiceFromFunctions,
18
+ makeToolResolverFromFunctions,
19
+ } from '@dxos/assistant';
20
+ import { Blueprint } from '@dxos/blueprints';
21
+ import { Database, Filter, Obj, Query, Ref } from '@dxos/echo';
22
+ import { acquireReleaseResource } from '@dxos/effect';
23
+ import { TestHelpers } from '@dxos/effect/testing';
24
+ import { CredentialsService, FunctionInvocationService, QueueService, TracingService } from '@dxos/functions';
25
+ import { FunctionInvocationServiceLayerTest, TestDatabaseLayer } from '@dxos/functions-runtime/testing';
26
+ import { invariant } from '@dxos/invariant';
27
+ import { ObjectId } from '@dxos/keys';
28
+ import { MarkdownBlueprint } from '@dxos/plugin-markdown/blueprints';
29
+ import { Markdown } from '@dxos/plugin-markdown/types';
30
+ import { HasSubject, type Message, Organization } from '@dxos/types';
31
+
32
+ import { ResearchBlueprint } from '../../blueprints';
33
+ import { testToolkit } from '../../blueprints/testing';
34
+
35
+ import { default as createDocument } from './document-create';
36
+ import { default as research } from './research';
37
+ import { ResearchGraph, queryResearchGraph } from './research-graph';
38
+ import { ResearchDataTypes } from './types';
39
+
40
+ ObjectId.dangerouslyDisableRandomness();
41
+
42
+ const TestLayer = Layer.mergeAll(
43
+ AiService.model('@anthropic/claude-opus-4-0'),
44
+ makeToolResolverFromFunctions([research, createDocument, ...MarkdownBlueprint.functions], testToolkit),
45
+ makeToolExecutionServiceFromFunctions(testToolkit, testToolkit.toLayer({}) as any),
46
+ ).pipe(
47
+ Layer.provideMerge(
48
+ FunctionInvocationServiceLayerTest({
49
+ functions: [research, createDocument, ...MarkdownBlueprint.functions],
50
+ }),
51
+ ),
52
+ Layer.provideMerge(
53
+ Layer.mergeAll(
54
+ TestAiService(),
55
+ TestDatabaseLayer({
56
+ spaceKey: 'fixed',
57
+ indexing: { vector: true },
58
+ types: [...ResearchDataTypes, ResearchGraph, Blueprint.Blueprint, Markdown.Document, HasSubject.HasSubject],
59
+ }),
60
+ CredentialsService.configuredLayer([]),
61
+ TracingService.layerNoop,
62
+ ),
63
+ ),
64
+ );
65
+
66
+ describe('Research', () => {
67
+ it.effect(
68
+ 'call a function to generate a research report',
69
+ Effect.fnUntraced(
70
+ function* (_) {
71
+ yield* Database.Service.add(
72
+ Obj.make(Organization.Organization, {
73
+ name: 'BlueYard',
74
+ website: 'https://blueyard.com',
75
+ }),
76
+ );
77
+ yield* Database.Service.flush({ indexes: true });
78
+ const result = yield* FunctionInvocationService.invokeFunction(research, {
79
+ query: 'Founders and portfolio of BlueYard.',
80
+ });
81
+
82
+ console.log(inspect(result, { depth: null, colors: true }));
83
+ console.log(JSON.stringify(result, null, 2));
84
+
85
+ yield* Database.Service.flush({ indexes: true });
86
+ const researchGraph = yield* queryResearchGraph();
87
+ if (researchGraph) {
88
+ const data = yield* Database.Service.load(researchGraph.queue).pipe(
89
+ Effect.flatMap((queue) => Effect.promise(() => queue.queryObjects())),
90
+ );
91
+ console.log(inspect(data, { depth: null, colors: true }));
92
+ }
93
+ },
94
+ Effect.provide(TestLayer),
95
+ TestHelpers.provideTestContext,
96
+ ),
97
+ MemoizedAiService.isGenerationEnabled() ? 240_000 : 30_000,
98
+ );
99
+
100
+ it.scoped(
101
+ 'create and update research report',
102
+ Effect.fnUntraced(
103
+ function* (_) {
104
+ const organization = yield* Database.Service.add(
105
+ Obj.make(Organization.Organization, {
106
+ name: 'BlueYard',
107
+ website: 'https://blueyard.com',
108
+ }),
109
+ );
110
+
111
+ const queue = yield* QueueService.createQueue<Message.Message | ContextBinding>();
112
+ const conversation = yield* acquireReleaseResource(() => new AiConversation({ queue }));
113
+
114
+ yield* Database.Service.flush({ indexes: true });
115
+ const researchBlueprint = yield* Database.Service.add(Obj.clone(ResearchBlueprint));
116
+ const markdownBlueprint = yield* Database.Service.add(Obj.clone(MarkdownBlueprint.make()));
117
+ yield* Effect.promise(() =>
118
+ conversation.context.bind({
119
+ blueprints: [Ref.make(researchBlueprint), Ref.make(markdownBlueprint)],
120
+ objects: [Ref.make(organization)],
121
+ }),
122
+ );
123
+
124
+ const observer = GenerationObserver.fromPrinter(new ConsolePrinter());
125
+
126
+ yield* conversation.createRequest({
127
+ observer,
128
+ prompt: `Create a research summary about ${organization.name}.`,
129
+ });
130
+ {
131
+ const docs = yield* Database.Service.runQuery(
132
+ Query.select(Filter.id(organization.id)).targetOf(HasSubject.HasSubject).source(),
133
+ );
134
+ if (docs.length !== 1) {
135
+ throw new Error(`Expected 1 research document; got ${docs.length}: ${docs.map((_) => _.name)}`);
136
+ }
137
+
138
+ const doc = docs[0];
139
+ invariant(Obj.instanceOf(Markdown.Document, doc));
140
+ console.log({
141
+ name: doc.name,
142
+ content: yield* Database.Service.load(doc.content).pipe(Effect.map((_) => _.content)),
143
+ });
144
+ }
145
+
146
+ yield* conversation.createRequest({
147
+ observer,
148
+ prompt: 'Add a section about their portfolio.',
149
+ });
150
+ {
151
+ const docs = yield* Database.Service.runQuery(
152
+ Query.select(Filter.id(organization.id)).targetOf(HasSubject.HasSubject).source(),
153
+ );
154
+ if (docs.length !== 1) {
155
+ throw new Error(`Expected 1 research document; got ${docs.length}: ${docs.map((_) => _.name)}`);
156
+ }
157
+
158
+ const doc = docs[0];
159
+ invariant(Obj.instanceOf(Markdown.Document, doc));
160
+ console.log({
161
+ name: doc.name,
162
+ content: yield* Database.Service.load(doc.content).pipe(Effect.map((_) => _.content)),
163
+ });
164
+ }
165
+ },
166
+ Effect.provide(TestLayer),
167
+ TestHelpers.provideTestContext,
168
+ ),
169
+ MemoizedAiService.isGenerationEnabled() ? 240_000 : 30_000,
170
+ );
171
+ });
@@ -0,0 +1,189 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Toolkit from '@effect/ai/Toolkit';
6
+ import * as AnthropicTool from '@effect/ai-anthropic/AnthropicTool';
7
+ import * as Array from 'effect/Array';
8
+ import * as Effect from 'effect/Effect';
9
+ import * as Function from 'effect/Function';
10
+ import * as Layer from 'effect/Layer';
11
+ import * as Option from 'effect/Option';
12
+ import * as Schema from 'effect/Schema';
13
+ import * as String from 'effect/String';
14
+
15
+ import { AiService, ConsolePrinter } from '@dxos/ai';
16
+ import {
17
+ AiSession,
18
+ GenerationObserver,
19
+ createToolkit,
20
+ makeToolExecutionServiceFromFunctions,
21
+ makeToolResolverFromFunctions,
22
+ } from '@dxos/assistant';
23
+ import { Template } from '@dxos/blueprints';
24
+ import { type DXN, Entity, Obj } from '@dxos/echo';
25
+ import { Database } from '@dxos/echo';
26
+ import { TracingService, defineFunction } from '@dxos/functions';
27
+ import { FunctionInvocationServiceLayerTestMocked } from '@dxos/functions-runtime/testing';
28
+ import { type Message, Person } from '@dxos/types';
29
+ import { trim } from '@dxos/util';
30
+
31
+ import { LocalSearchHandler, LocalSearchToolkit, makeGraphWriterHandler, makeGraphWriterToolkit } from '../../crud';
32
+ import { exaFunction, exaMockFunction } from '../exa';
33
+
34
+ import { contextQueueLayerFromResearchGraph } from './research-graph';
35
+ import PROMPT from './research-instructions.tpl?raw';
36
+ import { ResearchDataTypes } from './types';
37
+
38
+ /**
39
+ * Exec external service and return the results as a Subgraph.
40
+ */
41
+ export default defineFunction({
42
+ key: 'dxos.org/function/research',
43
+ name: 'Research',
44
+ description: trim`
45
+ Search the web to research information about the given subject.
46
+ Inserts structured data into the research graph.
47
+ Creates a research summary and returns the objects created.
48
+ `,
49
+ inputSchema: Schema.Struct({
50
+ query: Schema.String.annotations({
51
+ description: trim`
52
+ The search query.
53
+ If doing research on an object then load it first and pass it as JSON.
54
+ `,
55
+ }),
56
+
57
+ instructions: Schema.optional(Schema.String).annotations({
58
+ description: trim`
59
+ The instructions for the research agent.
60
+ `,
61
+ }),
62
+
63
+ // TOOD(burdon): Move to context.
64
+ mockSearch: Schema.optional(Schema.Boolean).annotations({
65
+ description: 'Whether to use the mock search tool.',
66
+ default: false,
67
+ }),
68
+
69
+ entityExtraction: Schema.optional(Schema.Boolean).annotations({
70
+ description: trim`
71
+ Whether to extract structured entities from the research.
72
+ Experimental feature only enable if user explicitly requests it.
73
+ `,
74
+ default: false,
75
+ }),
76
+ }),
77
+ outputSchema: Schema.Struct({
78
+ document: Schema.optional(Schema.String).annotations({
79
+ description: 'The generated research document.',
80
+ }),
81
+ objects: Schema.Array(Schema.Unknown).annotations({
82
+ description: 'Structured objects created during the research process.',
83
+ }),
84
+ }),
85
+ handler: Effect.fnUntraced(
86
+ function* ({ data: { query, instructions, mockSearch = false, entityExtraction = false } }) {
87
+ if (mockSearch) {
88
+ const mockPerson = yield* Database.Service.add(
89
+ Obj.make(Person.Person, {
90
+ preferredName: 'John Doe',
91
+ emails: [{ value: 'john.doe@example.com' }],
92
+ phoneNumbers: [{ value: '123-456-7890' }],
93
+ }),
94
+ );
95
+
96
+ return {
97
+ document: trim`
98
+ The research ran in test-mode and was mocked. Proceed as usual.
99
+ We reference John Doe to test reference: ${Obj.getDXN(mockPerson)}
100
+ `,
101
+ objects: [Obj.toJSON(mockPerson)],
102
+ };
103
+ }
104
+
105
+ yield* Database.Service.flush({ indexes: true });
106
+ yield* TracingService.emitStatus({ message: 'Starting research...' });
107
+
108
+ const NativeWebSearch = Toolkit.make(AnthropicTool.WebSearch_20250305({}));
109
+
110
+ let toolkit: Toolkit.Any = NativeWebSearch;
111
+ let handlers: Layer.Layer<any, any> = Layer.empty as any;
112
+
113
+ const objectDXNs: DXN[] = [];
114
+ if (entityExtraction) {
115
+ const GraphWriterToolkit = makeGraphWriterToolkit({ schema: ResearchDataTypes });
116
+ const GraphWriterHandler = makeGraphWriterHandler(GraphWriterToolkit, {
117
+ onAppend: (dxns) => objectDXNs.push(...dxns),
118
+ });
119
+
120
+ toolkit = Toolkit.merge(toolkit, LocalSearchToolkit, GraphWriterToolkit);
121
+ handlers = Layer.mergeAll(handlers, LocalSearchHandler, GraphWriterHandler).pipe(
122
+ Layer.provide(contextQueueLayerFromResearchGraph),
123
+ ) as any;
124
+ }
125
+
126
+ const finishedToolkit = yield* createToolkit({ toolkit }).pipe(Effect.provide(handlers));
127
+
128
+ const session = new AiSession();
129
+ const result = yield* session.run({
130
+ prompt: query,
131
+ system: join(
132
+ Template.process(PROMPT, { entityExtraction }),
133
+ instructions && `<instructions>${instructions}</instructions>`,
134
+ ),
135
+ toolkit: finishedToolkit,
136
+ observer: GenerationObserver.fromPrinter(new ConsolePrinter({ tag: 'research' })),
137
+ });
138
+
139
+ const objects = yield* Effect.forEach(objectDXNs, (dxn) => Database.Service.resolve(dxn)).pipe(
140
+ Effect.map(Array.map((obj) => Entity.toJSON(obj))),
141
+ );
142
+
143
+ return {
144
+ document: extractLastTextBlock(result),
145
+ objects,
146
+ };
147
+ },
148
+ Effect.provide(
149
+ Layer.mergeAll(
150
+ AiService.model('@anthropic/claude-sonnet-4-0'),
151
+ // TODO(dmaretskyi): Extract.
152
+ makeToolResolverFromFunctions([exaFunction, exaMockFunction], Toolkit.make()),
153
+ makeToolExecutionServiceFromFunctions(Toolkit.make() as any, Layer.empty as any),
154
+ ).pipe(
155
+ Layer.provide(
156
+ // TODO(dmaretskyi): This should be provided by environment.
157
+ Layer.mergeAll(FunctionInvocationServiceLayerTestMocked({ functions: [exaFunction, exaMockFunction] })),
158
+ ),
159
+ ),
160
+ ),
161
+ ),
162
+ });
163
+
164
+ // TODO(burdon): Factor out.
165
+ const join = (...strings: (string | undefined)[]) => strings.filter(Boolean).join('\n\n');
166
+
167
+ /**
168
+ * Extracts the last text block from the result.
169
+ * Skips citations.
170
+ */
171
+ // TODO(burdon): Factor out.
172
+ const extractLastTextBlock = (result: Message.Message[]) => {
173
+ return Function.pipe(
174
+ result,
175
+ Array.last,
176
+ Option.map(
177
+ Function.flow(
178
+ (_: Message.Message) => _.blocks,
179
+ Array.reverse,
180
+ Array.dropWhile((_: any) => _._tag === 'summary'),
181
+ Array.takeWhile((_: any) => _._tag === 'text'),
182
+ Array.reverse,
183
+ Array.map((_: any) => _.text),
184
+ Array.reduce('', String.concat),
185
+ ),
186
+ ),
187
+ Option.getOrElse(() => ''),
188
+ );
189
+ };
@@ -0,0 +1,26 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Type } from '@dxos/echo';
6
+ import { Text } from '@dxos/schema';
7
+ import { Event, HasConnection, HasRelationship, LegacyOrganization, LegacyPerson, Project, Task } from '@dxos/types';
8
+
9
+ /**
10
+ * Data types for research.
11
+ */
12
+ export const ResearchDataTypes: Type.Entity.Any[] = [
13
+ // Objects
14
+ Event.Event,
15
+ LegacyOrganization,
16
+ Task.Task,
17
+ Text.Text,
18
+
19
+ // Relations
20
+ // TODO(wittjosiah): Until views (e.g., Table) support relations this needs to be expressed via organization ref.
21
+ // Employer.Employer,
22
+ LegacyPerson,
23
+ Project.Project,
24
+ HasRelationship.HasRelationship,
25
+ HasConnection.HasConnection,
26
+ ];
@@ -0,0 +1,11 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { default as read$ } from './read';
6
+ import { default as update$ } from './update';
7
+
8
+ export namespace Tasks {
9
+ export const read = read$;
10
+ export const update = update$;
11
+ }
@@ -0,0 +1,35 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+ import * as Schema from 'effect/Schema';
7
+
8
+ import { ArtifactId } from '@dxos/assistant';
9
+ import { Database } from '@dxos/echo';
10
+ import { defineFunction } from '@dxos/functions';
11
+ import { Markdown } from '@dxos/plugin-markdown/types';
12
+
13
+ export default defineFunction({
14
+ key: 'dxos.org/function/markdown/read-tasks',
15
+ name: 'Read',
16
+ description: 'Read markdown tasks.',
17
+ inputSchema: Schema.Struct({
18
+ id: ArtifactId.annotations({
19
+ description: 'The ID of the document to read.',
20
+ }),
21
+ }),
22
+ outputSchema: Schema.Struct({
23
+ content: Schema.String,
24
+ }),
25
+ handler: Effect.fn(function* ({ data: { id } }) {
26
+ const doc = yield* Database.Service.resolve(ArtifactId.toDXN(id), Markdown.Document);
27
+
28
+ // Return content with line numbers prefixed.
29
+ const { content } = yield* Database.Service.load(doc.content);
30
+ const lines = content.split('\n');
31
+ const len = String(lines.length).length;
32
+ const numbered = lines.map((line, i) => `${String(i + 1).padStart(len, ' ')}. ${line}`).join('\n');
33
+ return { content: numbered };
34
+ }),
35
+ });
@@ -0,0 +1,99 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { describe, expect, it } from 'vitest';
6
+
7
+ import { MarkdownTasks, type TaskOperation } from './task-list';
8
+
9
+ describe('MarkdownTasks', () => {
10
+ it('should initialize with content', () => {
11
+ const content = '- [ ] Task 1\n- [x] Task 2';
12
+ const manager = new MarkdownTasks(content);
13
+ expect(manager.getRawContent()).toBe(content);
14
+ });
15
+
16
+ it('should provide numbered content', () => {
17
+ const manager = new MarkdownTasks('- [ ] Task 1\n - [ ] Subtask');
18
+ const numbered = manager.getNumberedContent();
19
+ expect(numbered).toBe(' 1→- [ ] Task 1\n 2→ - [ ] Subtask');
20
+ });
21
+
22
+ it('should insert tasks with indentation', () => {
23
+ const manager = new MarkdownTasks('- [ ] Main task');
24
+ manager.insertTask(2, 'Subtask', false, 2);
25
+ expect(manager.getRawContent()).toBe('- [ ] Main task\n - [ ] Subtask');
26
+ });
27
+
28
+ it('should insert completed tasks', () => {
29
+ const manager = new MarkdownTasks('- [ ] Task 1');
30
+ manager.insertTask(2, 'Completed task', true, 0);
31
+ expect(manager.getRawContent()).toBe('- [ ] Task 1\n- [x] Completed task');
32
+ });
33
+
34
+ it('should delete tasks', () => {
35
+ const manager = new MarkdownTasks('- [ ] Task 1\n- [ ] Task 2');
36
+ const success = manager.deleteTask(2);
37
+ expect(success).toBe(true);
38
+ expect(manager.getRawContent()).toBe('- [ ] Task 1');
39
+ });
40
+
41
+ it('should update task text', () => {
42
+ const manager = new MarkdownTasks('- [ ] Old task');
43
+ const success = manager.updateTaskText(1, 'New task description');
44
+ expect(success).toBe(true);
45
+ expect(manager.getRawContent()).toBe('- [ ] New task description');
46
+ });
47
+
48
+ it('should toggle task completion', () => {
49
+ const manager = new MarkdownTasks('- [ ] Task 1\n- [x] Task 2');
50
+
51
+ // Complete first task
52
+ manager.toggleTaskCompletion(1, true);
53
+ expect(manager.getRawContent()).toBe('- [x] Task 1\n- [x] Task 2');
54
+
55
+ // Uncomplete second task
56
+ manager.toggleTaskCompletion(2, false);
57
+ expect(manager.getRawContent()).toBe('- [x] Task 1\n- [ ] Task 2');
58
+ });
59
+
60
+ it('should change task indentation', () => {
61
+ const manager = new MarkdownTasks('- [ ] Task 1');
62
+ const success = manager.setTaskIndent(1, 4);
63
+ expect(success).toBe(true);
64
+ expect(manager.getRawContent()).toBe(' - [ ] Task 1');
65
+ });
66
+
67
+ it('should apply multiple operations', () => {
68
+ const manager = new MarkdownTasks('- [ ] Task 1');
69
+
70
+ const operations: TaskOperation[] = [
71
+ { type: 'insertTask', lineNumber: 2, text: 'New task', indent: 2 },
72
+ { type: 'toggleTaskCompletion', lineNumber: 1, completed: true },
73
+ { type: 'insertTask', lineNumber: 3, text: 'Another task', indent: 0 },
74
+ ];
75
+
76
+ manager.applyOperations(operations);
77
+ const result = manager.getRawContent();
78
+ expect(result).toBe('- [x] Task 1\n - [ ] New task\n- [ ] Another task');
79
+ });
80
+
81
+ it('should handle hierarchical task structure', () => {
82
+ const manager = new MarkdownTasks();
83
+
84
+ // Build a hierarchical structure
85
+ manager.insertTask(1, 'Main task', false, 0);
86
+ manager.insertTask(2, 'Subtask 1', false, 2);
87
+ manager.insertTask(3, 'Sub-subtask', false, 4);
88
+ manager.insertTask(4, 'Subtask 2', false, 2);
89
+
90
+ const expected = '- [ ] Main task\n - [ ] Subtask 1\n - [ ] Sub-subtask\n - [ ] Subtask 2';
91
+ expect(manager.getRawContent()).toBe(expected);
92
+ });
93
+
94
+ it('should handle line insertions at the end', () => {
95
+ const manager = new MarkdownTasks('- [ ] Task 1');
96
+ manager.insertTask(999, 'End task', false, 0);
97
+ expect(manager.getRawContent()).toBe('- [ ] Task 1\n- [ ] End task');
98
+ });
99
+ });
@@ -0,0 +1,165 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ /**
6
+ * Hierarchical Task List Manager.
7
+ *
8
+ * A framework for managing hierarchical task lists where each line is a task.
9
+ * Designed to work with language model agents that receive documents with line numbers.
10
+ */
11
+ export class MarkdownTasks {
12
+ private _lineEndings: string;
13
+ private _content: string[];
14
+
15
+ constructor(initialContent: string = '') {
16
+ // Detect line endings.
17
+ this._lineEndings = initialContent.includes('\r\n') ? '\r\n' : '\n';
18
+ this._content = initialContent ? initialContent.split(this._lineEndings) : [];
19
+ }
20
+
21
+ /**
22
+ * Get the current document content with line numbers prefixed.
23
+ */
24
+ getNumberedContent(): string {
25
+ return this._content
26
+ .map((line, index) => `${(index + 1).toString().padStart(3, ' ')}→${line}`)
27
+ .join(this._lineEndings);
28
+ }
29
+
30
+ /**
31
+ * Get the raw document content without line numbers.
32
+ */
33
+ getRawContent(): string {
34
+ return this._content.join(this._lineEndings);
35
+ }
36
+
37
+ /**
38
+ * Insert a new task at the specified line number (1-based).
39
+ * Indentation level determines hierarchy (0, 2, 4, 6 spaces etc.)
40
+ */
41
+ insertTask(lineNumber: number, taskText: string, completed: boolean = false, indent: number = 0): void {
42
+ const indentStr = ' '.repeat(indent);
43
+ const taskLine = completed ? `${indentStr}- [x] ${taskText}` : `${indentStr}- [ ] ${taskText}`;
44
+ const insertIndex = Math.max(0, Math.min(lineNumber - 1, this._content.length));
45
+ this._content.splice(insertIndex, 0, taskLine);
46
+ }
47
+
48
+ /**
49
+ * Delete a task at the specified line number (1-based).
50
+ */
51
+ deleteTask(lineNumber: number): boolean {
52
+ if (lineNumber < 1 || lineNumber > this._content.length) {
53
+ return false;
54
+ }
55
+ this._content.splice(lineNumber - 1, 1);
56
+ return true;
57
+ }
58
+
59
+ /**
60
+ * Update the text of a task at the specified line number (1-based).
61
+ */
62
+ updateTaskText(lineNumber: number, newText: string): boolean {
63
+ if (lineNumber < 1 || lineNumber > this._content.length) {
64
+ return false;
65
+ }
66
+
67
+ const currentLine = this._content[lineNumber - 1];
68
+ const taskMatch = currentLine.match(/^(\s*- \[[x ]\] )(.*)$/);
69
+
70
+ if (taskMatch) {
71
+ this._content[lineNumber - 1] = `${taskMatch[1]}${newText}`;
72
+ return true;
73
+ }
74
+ return false;
75
+ }
76
+
77
+ /**
78
+ * Mark a task as complete or incomplete at the specified line number (1-based).
79
+ */
80
+ toggleTaskCompletion(lineNumber: number, completed?: boolean): boolean {
81
+ if (lineNumber < 1 || lineNumber > this._content.length) {
82
+ return false;
83
+ }
84
+
85
+ const currentLine = this._content[lineNumber - 1];
86
+ const taskMatch = currentLine.match(/^(\s*- \[)([x ])(.*)$/);
87
+ if (taskMatch) {
88
+ const isCurrentlyComplete = taskMatch[2] === 'x';
89
+ const newStatus = completed !== undefined ? completed : !isCurrentlyComplete;
90
+ const statusChar = newStatus ? 'x' : ' ';
91
+ this._content[lineNumber - 1] = `${taskMatch[1]}${statusChar}${taskMatch[3]}`;
92
+ return true;
93
+ }
94
+
95
+ return false;
96
+ }
97
+
98
+ /**
99
+ * Change the indentation level of a task (for hierarchy).
100
+ */
101
+ setTaskIndent(lineNumber: number, indent: number): boolean {
102
+ if (lineNumber < 1 || lineNumber > this._content.length) {
103
+ return false;
104
+ }
105
+
106
+ const currentLine = this._content[lineNumber - 1];
107
+ const taskMatch = currentLine.match(/^\s*- (\[[x ]\] .*)$/);
108
+ if (taskMatch) {
109
+ const indentStr = ' '.repeat(indent);
110
+ this._content[lineNumber - 1] = `${indentStr}- ${taskMatch[1]}`;
111
+ return true;
112
+ }
113
+
114
+ return false;
115
+ }
116
+
117
+ /**
118
+ * Get the total number of lines in the document.
119
+ */
120
+ getLineCount(): number {
121
+ return this._content.length;
122
+ }
123
+
124
+ /**
125
+ * Apply multiple operations atomically.
126
+ */
127
+ applyOperations(operations: TaskOperation[]): void {
128
+ // Sort operations by line number in descending order to avoid index shifts.
129
+ const sortedOps = [...operations].sort((a, b) => {
130
+ const aLine = 'lineNumber' in a ? a.lineNumber : 0;
131
+ const bLine = 'lineNumber' in b ? b.lineNumber : 0;
132
+ return bLine - aLine;
133
+ });
134
+
135
+ for (const op of sortedOps) {
136
+ switch (op.type) {
137
+ case 'insertTask':
138
+ this.insertTask(op.lineNumber, op.text, op.completed, op.indent);
139
+ break;
140
+ case 'deleteTask':
141
+ this.deleteTask(op.lineNumber);
142
+ break;
143
+ case 'updateTaskText':
144
+ this.updateTaskText(op.lineNumber, op.text);
145
+ break;
146
+ case 'toggleTaskCompletion':
147
+ this.toggleTaskCompletion(op.lineNumber, op.completed);
148
+ break;
149
+ case 'setTaskIndent':
150
+ this.setTaskIndent(op.lineNumber, op.indent);
151
+ break;
152
+ }
153
+ }
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Operation types for batch task updates.
159
+ */
160
+ export type TaskOperation =
161
+ | { type: 'insertTask'; lineNumber: number; text: string; completed?: boolean; indent?: number }
162
+ | { type: 'deleteTask'; lineNumber: number }
163
+ | { type: 'updateTaskText'; lineNumber: number; text: string }
164
+ | { type: 'toggleTaskCompletion'; lineNumber: number; completed?: boolean }
165
+ | { type: 'setTaskIndent'; lineNumber: number; indent: number };