@dxos/assistant-toolkit 0.8.4-main.ae835ea

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 (200) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +3 -0
  3. package/dist/lib/browser/index.mjs +2481 -0
  4. package/dist/lib/browser/index.mjs.map +7 -0
  5. package/dist/lib/browser/meta.json +1 -0
  6. package/dist/lib/node-esm/index.mjs +2483 -0
  7. package/dist/lib/node-esm/index.mjs.map +7 -0
  8. package/dist/lib/node-esm/meta.json +1 -0
  9. package/dist/types/src/blueprints/design/design-blueprint.d.ts +4 -0
  10. package/dist/types/src/blueprints/design/design-blueprint.d.ts.map +1 -0
  11. package/dist/types/src/blueprints/design/design-blueprint.test.d.ts +2 -0
  12. package/dist/types/src/blueprints/design/design-blueprint.test.d.ts.map +1 -0
  13. package/dist/types/src/blueprints/design/index.d.ts +3 -0
  14. package/dist/types/src/blueprints/design/index.d.ts.map +1 -0
  15. package/dist/types/src/blueprints/discord/discord-blueprint.d.ts +18 -0
  16. package/dist/types/src/blueprints/discord/discord-blueprint.d.ts.map +1 -0
  17. package/dist/types/src/blueprints/discord/index.d.ts +3 -0
  18. package/dist/types/src/blueprints/discord/index.d.ts.map +1 -0
  19. package/dist/types/src/blueprints/index.d.ts +7 -0
  20. package/dist/types/src/blueprints/index.d.ts.map +1 -0
  21. package/dist/types/src/blueprints/linear/index.d.ts +3 -0
  22. package/dist/types/src/blueprints/linear/index.d.ts.map +1 -0
  23. package/dist/types/src/blueprints/linear/linear-blueprint.d.ts +18 -0
  24. package/dist/types/src/blueprints/linear/linear-blueprint.d.ts.map +1 -0
  25. package/dist/types/src/blueprints/planning/index.d.ts +3 -0
  26. package/dist/types/src/blueprints/planning/index.d.ts.map +1 -0
  27. package/dist/types/src/blueprints/planning/planning-blueprint.d.ts +4 -0
  28. package/dist/types/src/blueprints/planning/planning-blueprint.d.ts.map +1 -0
  29. package/dist/types/src/blueprints/planning/planning-blueprint.test.d.ts +2 -0
  30. package/dist/types/src/blueprints/planning/planning-blueprint.test.d.ts.map +1 -0
  31. package/dist/types/src/blueprints/research/index.d.ts +3 -0
  32. package/dist/types/src/blueprints/research/index.d.ts.map +1 -0
  33. package/dist/types/src/blueprints/research/research-blueprint.d.ts +4 -0
  34. package/dist/types/src/blueprints/research/research-blueprint.d.ts.map +1 -0
  35. package/dist/types/src/blueprints/research/research-blueprint.test.d.ts +2 -0
  36. package/dist/types/src/blueprints/research/research-blueprint.test.d.ts.map +1 -0
  37. package/dist/types/src/blueprints/testing.d.ts +12 -0
  38. package/dist/types/src/blueprints/testing.d.ts.map +1 -0
  39. package/dist/types/src/blueprints/websearch/index.d.ts +4 -0
  40. package/dist/types/src/blueprints/websearch/index.d.ts.map +1 -0
  41. package/dist/types/src/blueprints/websearch/websearch-blueprint.d.ts +4 -0
  42. package/dist/types/src/blueprints/websearch/websearch-blueprint.d.ts.map +1 -0
  43. package/dist/types/src/blueprints/websearch/websearch-toolkit.d.ts +26 -0
  44. package/dist/types/src/blueprints/websearch/websearch-toolkit.d.ts.map +1 -0
  45. package/dist/types/src/experimental/feed.test.d.ts +2 -0
  46. package/dist/types/src/experimental/feed.test.d.ts.map +1 -0
  47. package/dist/types/src/functions/agent/index.d.ts +5 -0
  48. package/dist/types/src/functions/agent/index.d.ts.map +1 -0
  49. package/dist/types/src/functions/agent/prompt.d.ts +11 -0
  50. package/dist/types/src/functions/agent/prompt.d.ts.map +1 -0
  51. package/dist/types/src/functions/discord/fetch-messages.d.ts +11 -0
  52. package/dist/types/src/functions/discord/fetch-messages.d.ts.map +1 -0
  53. package/dist/types/src/functions/discord/fetch-messages.test.d.ts +2 -0
  54. package/dist/types/src/functions/discord/fetch-messages.test.d.ts.map +1 -0
  55. package/dist/types/src/functions/discord/index.d.ts +12 -0
  56. package/dist/types/src/functions/discord/index.d.ts.map +1 -0
  57. package/dist/types/src/functions/document/index.d.ts +12 -0
  58. package/dist/types/src/functions/document/index.d.ts.map +1 -0
  59. package/dist/types/src/functions/document/read.d.ts +7 -0
  60. package/dist/types/src/functions/document/read.d.ts.map +1 -0
  61. package/dist/types/src/functions/document/update.d.ts +6 -0
  62. package/dist/types/src/functions/document/update.d.ts.map +1 -0
  63. package/dist/types/src/functions/entity-extraction/entity-extraction.d.ts +173 -0
  64. package/dist/types/src/functions/entity-extraction/entity-extraction.d.ts.map +1 -0
  65. package/dist/types/src/functions/entity-extraction/entity-extraction.test.d.ts +2 -0
  66. package/dist/types/src/functions/entity-extraction/entity-extraction.test.d.ts.map +1 -0
  67. package/dist/types/src/functions/entity-extraction/index.d.ts +174 -0
  68. package/dist/types/src/functions/entity-extraction/index.d.ts.map +1 -0
  69. package/dist/types/src/functions/exa/exa.d.ts +5 -0
  70. package/dist/types/src/functions/exa/exa.d.ts.map +1 -0
  71. package/dist/types/src/functions/exa/index.d.ts +3 -0
  72. package/dist/types/src/functions/exa/index.d.ts.map +1 -0
  73. package/dist/types/src/functions/exa/mock.d.ts +5 -0
  74. package/dist/types/src/functions/exa/mock.d.ts.map +1 -0
  75. package/dist/types/src/functions/github/fetch-prs.d.ts +6 -0
  76. package/dist/types/src/functions/github/fetch-prs.d.ts.map +1 -0
  77. package/dist/types/src/functions/index.d.ts +8 -0
  78. package/dist/types/src/functions/index.d.ts.map +1 -0
  79. package/dist/types/src/functions/linear/index.d.ts +9 -0
  80. package/dist/types/src/functions/linear/index.d.ts.map +1 -0
  81. package/dist/types/src/functions/linear/linear.test.d.ts +2 -0
  82. package/dist/types/src/functions/linear/linear.test.d.ts.map +1 -0
  83. package/dist/types/src/functions/linear/sync-issues.d.ts +12 -0
  84. package/dist/types/src/functions/linear/sync-issues.d.ts.map +1 -0
  85. package/dist/types/src/functions/research/create-document.d.ts +7 -0
  86. package/dist/types/src/functions/research/create-document.d.ts.map +1 -0
  87. package/dist/types/src/functions/research/graph.d.ts +64 -0
  88. package/dist/types/src/functions/research/graph.d.ts.map +1 -0
  89. package/dist/types/src/functions/research/graph.test.d.ts +2 -0
  90. package/dist/types/src/functions/research/graph.test.d.ts.map +1 -0
  91. package/dist/types/src/functions/research/index.d.ts +19 -0
  92. package/dist/types/src/functions/research/index.d.ts.map +1 -0
  93. package/dist/types/src/functions/research/research-graph.d.ts +18 -0
  94. package/dist/types/src/functions/research/research-graph.d.ts.map +1 -0
  95. package/dist/types/src/functions/research/research.d.ts +13 -0
  96. package/dist/types/src/functions/research/research.d.ts.map +1 -0
  97. package/dist/types/src/functions/research/research.test.d.ts +2 -0
  98. package/dist/types/src/functions/research/research.test.d.ts.map +1 -0
  99. package/dist/types/src/functions/research/types.d.ts +384 -0
  100. package/dist/types/src/functions/research/types.d.ts.map +1 -0
  101. package/dist/types/src/functions/tasks/index.d.ts +15 -0
  102. package/dist/types/src/functions/tasks/index.d.ts.map +1 -0
  103. package/dist/types/src/functions/tasks/read.d.ts +7 -0
  104. package/dist/types/src/functions/tasks/read.d.ts.map +1 -0
  105. package/dist/types/src/functions/tasks/task-list.d.ts +74 -0
  106. package/dist/types/src/functions/tasks/task-list.d.ts.map +1 -0
  107. package/dist/types/src/functions/tasks/task-list.test.d.ts +2 -0
  108. package/dist/types/src/functions/tasks/task-list.test.d.ts.map +1 -0
  109. package/dist/types/src/functions/tasks/update.d.ts +9 -0
  110. package/dist/types/src/functions/tasks/update.d.ts.map +1 -0
  111. package/dist/types/src/index.d.ts +5 -0
  112. package/dist/types/src/index.d.ts.map +1 -0
  113. package/dist/types/src/plugins.d.ts +19 -0
  114. package/dist/types/src/plugins.d.ts.map +1 -0
  115. package/dist/types/src/sync/index.d.ts +2 -0
  116. package/dist/types/src/sync/index.d.ts.map +1 -0
  117. package/dist/types/src/sync/sync.d.ts +15 -0
  118. package/dist/types/src/sync/sync.d.ts.map +1 -0
  119. package/dist/types/src/testing/data/exa-search-1748337321991.d.ts +38 -0
  120. package/dist/types/src/testing/data/exa-search-1748337321991.d.ts.map +1 -0
  121. package/dist/types/src/testing/data/exa-search-1748337331526.d.ts +37 -0
  122. package/dist/types/src/testing/data/exa-search-1748337331526.d.ts.map +1 -0
  123. package/dist/types/src/testing/data/exa-search-1748337344119.d.ts +58 -0
  124. package/dist/types/src/testing/data/exa-search-1748337344119.d.ts.map +1 -0
  125. package/dist/types/src/testing/data/index.d.ts +3 -0
  126. package/dist/types/src/testing/data/index.d.ts.map +1 -0
  127. package/dist/types/src/testing/index.d.ts +2 -0
  128. package/dist/types/src/testing/index.d.ts.map +1 -0
  129. package/dist/types/src/util/graphql.d.ts +22 -0
  130. package/dist/types/src/util/graphql.d.ts.map +1 -0
  131. package/dist/types/src/util/index.d.ts +2 -0
  132. package/dist/types/src/util/index.d.ts.map +1 -0
  133. package/dist/types/tsconfig.tsbuildinfo +1 -0
  134. package/package.json +67 -0
  135. package/src/blueprints/design/design-blueprint.test.ts +108 -0
  136. package/src/blueprints/design/design-blueprint.ts +33 -0
  137. package/src/blueprints/design/index.ts +7 -0
  138. package/src/blueprints/discord/discord-blueprint.ts +34 -0
  139. package/src/blueprints/discord/index.ts +7 -0
  140. package/src/blueprints/index.ts +10 -0
  141. package/src/blueprints/linear/index.ts +7 -0
  142. package/src/blueprints/linear/linear-blueprint.ts +35 -0
  143. package/src/blueprints/planning/index.ts +7 -0
  144. package/src/blueprints/planning/planning-blueprint.test.ts +129 -0
  145. package/src/blueprints/planning/planning-blueprint.ts +98 -0
  146. package/src/blueprints/research/index.ts +7 -0
  147. package/src/blueprints/research/research-blueprint.test.ts +7 -0
  148. package/src/blueprints/research/research-blueprint.ts +45 -0
  149. package/src/blueprints/testing.ts +34 -0
  150. package/src/blueprints/websearch/index.ts +8 -0
  151. package/src/blueprints/websearch/websearch-blueprint.ts +20 -0
  152. package/src/blueprints/websearch/websearch-toolkit.ts +8 -0
  153. package/src/experimental/feed.test.ts +108 -0
  154. package/src/functions/agent/index.ts +11 -0
  155. package/src/functions/agent/prompt.ts +101 -0
  156. package/src/functions/discord/fetch-messages.test.ts +59 -0
  157. package/src/functions/discord/fetch-messages.ts +251 -0
  158. package/src/functions/discord/index.ts +9 -0
  159. package/src/functions/document/index.ts +11 -0
  160. package/src/functions/document/read.ts +29 -0
  161. package/src/functions/document/update.ts +30 -0
  162. package/src/functions/entity-extraction/entity-extraction.conversations.json +1 -0
  163. package/src/functions/entity-extraction/entity-extraction.test.ts +100 -0
  164. package/src/functions/entity-extraction/entity-extraction.ts +163 -0
  165. package/src/functions/entity-extraction/index.ts +9 -0
  166. package/src/functions/exa/exa.ts +37 -0
  167. package/src/functions/exa/index.ts +6 -0
  168. package/src/functions/exa/mock.ts +71 -0
  169. package/src/functions/github/fetch-prs.ts +30 -0
  170. package/src/functions/index.ts +11 -0
  171. package/src/functions/linear/index.ts +9 -0
  172. package/src/functions/linear/linear.test.ts +86 -0
  173. package/src/functions/linear/sync-issues.ts +189 -0
  174. package/src/functions/research/create-document.ts +69 -0
  175. package/src/functions/research/graph.test.ts +69 -0
  176. package/src/functions/research/graph.ts +388 -0
  177. package/src/functions/research/index.ts +15 -0
  178. package/src/functions/research/instructions-research.tpl +98 -0
  179. package/src/functions/research/research-graph.ts +47 -0
  180. package/src/functions/research/research.conversations.json +10714 -0
  181. package/src/functions/research/research.test.ts +240 -0
  182. package/src/functions/research/research.ts +155 -0
  183. package/src/functions/research/types.ts +24 -0
  184. package/src/functions/tasks/index.ts +11 -0
  185. package/src/functions/tasks/read.ts +34 -0
  186. package/src/functions/tasks/task-list.test.ts +99 -0
  187. package/src/functions/tasks/task-list.ts +165 -0
  188. package/src/functions/tasks/update.ts +52 -0
  189. package/src/index.ts +8 -0
  190. package/src/plugins.tsx +68 -0
  191. package/src/sync/index.ts +5 -0
  192. package/src/sync/sync.ts +87 -0
  193. package/src/testing/data/exa-search-1748337321991.ts +131 -0
  194. package/src/testing/data/exa-search-1748337331526.ts +144 -0
  195. package/src/testing/data/exa-search-1748337344119.ts +133 -0
  196. package/src/testing/data/index.ts +11 -0
  197. package/src/testing/index.ts +5 -0
  198. package/src/typedefs.d.ts +8 -0
  199. package/src/util/graphql.ts +31 -0
  200. package/src/util/index.ts +5 -0
@@ -0,0 +1,20 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { ToolId } from '@dxos/ai';
6
+ import { Blueprint } from '@dxos/blueprints';
7
+ import { Obj, Ref } from '@dxos/echo';
8
+ import { DataType } from '@dxos/schema';
9
+
10
+ const blueprint: Blueprint.Blueprint = Obj.make(Blueprint.Blueprint, {
11
+ key: 'dxos.org/blueprint/web-search',
12
+ name: 'Web Search',
13
+ description: 'Search the web.',
14
+ instructions: {
15
+ source: Ref.make(DataType.makeText('')), // No instructions required.
16
+ },
17
+ tools: [ToolId.make('AnthropicWebSearch')],
18
+ });
19
+
20
+ export default blueprint;
@@ -0,0 +1,8 @@
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
+
8
+ export const WebSearchToolkit = Toolkit.make(AnthropicTool.WebSearch_20250305({}));
@@ -0,0 +1,108 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Toolkit from '@effect/ai/Toolkit';
6
+ import * as FetchHttpClient from '@effect/platform/FetchHttpClient';
7
+ import { describe, it } from '@effect/vitest';
8
+ import * as Config from 'effect/Config';
9
+ import * as Effect from 'effect/Effect';
10
+ import * as Layer from 'effect/Layer';
11
+ import * as Redacted from 'effect/Redacted';
12
+
13
+ import { AiService } from '@dxos/ai';
14
+ import { AiServiceTestingPreset, EXA_API_KEY } from '@dxos/ai/testing';
15
+ import { makeToolExecutionServiceFromFunctions, makeToolResolverFromFunctions } from '@dxos/assistant';
16
+ import { TestHelpers } from '@dxos/effect';
17
+ import { ComputeEventLogger, CredentialsService, FunctionInvocationService, TracingService } from '@dxos/functions';
18
+ import { TestDatabaseLayer, testStoragePath } from '@dxos/functions/testing';
19
+
20
+ import { Discord, Linear } from '../functions';
21
+
22
+ const TestLayer = Layer.mergeAll(
23
+ AiService.model('@anthropic/claude-opus-4-0'),
24
+ makeToolResolverFromFunctions([], Toolkit.make()),
25
+ makeToolExecutionServiceFromFunctions(Toolkit.make() as any, Layer.empty as any),
26
+ ComputeEventLogger.layerFromTracing,
27
+ ).pipe(
28
+ Layer.provideMerge(
29
+ Layer.mergeAll(
30
+ AiServiceTestingPreset('direct'),
31
+ TestDatabaseLayer({
32
+ indexing: { vector: true },
33
+ types: [],
34
+ storagePath: testStoragePath({ name: 'feed-test' }),
35
+ }),
36
+ CredentialsService.layerConfig([
37
+ { service: 'exa.ai', apiKey: Config.succeed(Redacted.make(EXA_API_KEY)) },
38
+ { service: 'discord.com', apiKey: Config.redacted('DISCORD_TOKEN') },
39
+ { service: 'linear.app', apiKey: Config.redacted('LINEAR_API_KEY') },
40
+ ]),
41
+ FunctionInvocationService.layerTestMocked({ functions: [Linear.sync, Discord.fetch] }).pipe(
42
+ Layer.provideMerge(ComputeEventLogger.layerFromTracing),
43
+ Layer.provideMerge(TracingService.layerLogInfo()),
44
+ ),
45
+ FetchHttpClient.layer,
46
+ ),
47
+ ),
48
+ );
49
+
50
+ describe('Feed', { timeout: 600_000 }, () => {
51
+ it.effect(
52
+ 'fetch discord messages',
53
+ Effect.fnUntraced(
54
+ function* (_) {
55
+ // const messages = yield* LocalFunctionExecutionService.invokeFunction(fetchDiscordMessages, {
56
+ // serverId: '837138313172353095',
57
+ // // channelId: '1404487604761526423',
58
+ // after: Date.now() / 1000 - 128 * 3600,
59
+ // });
60
+ // for (const message of messages) {
61
+ // console.log(message.sender.name, message.blocks.find((block) => block._tag === 'text')?.text);
62
+ // }
63
+ // console.log(`Fetched ${messages.length} messages`);
64
+
65
+ // const result = yield* AiSession.run({
66
+ // history: [
67
+ // Obj.make(DataType.Message, {
68
+ // created: new Date().toISOString(),
69
+ // sender: { role: 'user' },
70
+ // blocks: messages
71
+ // .map(
72
+ // (message) =>
73
+ // ({
74
+ // _tag: 'text',
75
+ // text: message.sender.name + ': ' + message.blocks.find((block) => block._tag === 'text')?.text,
76
+ // }) as const,
77
+ // )
78
+ // .filter((block) => block._tag === 'text' && block.text.trim().length > 0),
79
+ // }),
80
+ // ],
81
+ // prompt: 'Summarize the messages.',
82
+ // system: 'Summarize the messages.',
83
+ // }).pipe(Effect.provide(AiService.model('@anthropic/claude-3-5-haiku-latest')));
84
+ // console.log(result);
85
+
86
+ const linearIssues = yield* FunctionInvocationService.invokeFunction(Linear.sync, {
87
+ team: '1127c63a-6f77-4725-9229-50f6cd47321c',
88
+ });
89
+ console.log(linearIssues);
90
+
91
+ // const result = yield* AiSession.run({
92
+ // history: [
93
+ // Obj.make(DataType.Message, {
94
+ // created: new Date().toISOString(),
95
+ // sender: { role: 'user' },
96
+ // blocks: [{ _tag: 'text', text: JSON.stringify(linearIssues) }],
97
+ // }),
98
+ // ],
99
+ // prompt: 'Summarize the whats new.',
100
+ // system: 'Summarize the whats new. Reference specific people by name.',
101
+ // }).pipe(Effect.provide(AiService.model('@anthropic/claude-sonnet-4-0')));
102
+ // console.log(result.at(-1)?.blocks.find((block) => block._tag === 'text')?.text);
103
+ },
104
+ Effect.provide(TestLayer),
105
+ TestHelpers.taggedTest('llm'),
106
+ ),
107
+ );
108
+ });
@@ -0,0 +1,11 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type FunctionDefinition } from '@dxos/functions';
6
+
7
+ import { default as prompt$ } from './prompt';
8
+
9
+ export namespace Agent {
10
+ export const prompt: FunctionDefinition.Any = prompt$; // TODO(burdon): Temp fix for TS error.
11
+ }
@@ -0,0 +1,101 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Array from 'effect/Array';
6
+ import * as Effect from 'effect/Effect';
7
+ import * as Function from 'effect/Function';
8
+ import * as Option from 'effect/Option';
9
+ import * as Schema from 'effect/Schema';
10
+
11
+ import { AiService, ConsolePrinter, ModelName } from '@dxos/ai';
12
+ import { AiSession, GenerationObserver, createToolkit } from '@dxos/assistant';
13
+ import { Prompt, Template } from '@dxos/blueprints';
14
+ import { Obj, Ref, Type } from '@dxos/echo';
15
+ import { DatabaseService, TracingService, defineFunction } from '@dxos/functions';
16
+ import { log } from '@dxos/log';
17
+
18
+ const DEFAULT_MODEL: ModelName = '@anthropic/claude-opus-4-0';
19
+
20
+ export default defineFunction({
21
+ key: 'dxos.org/function/prompt',
22
+ name: 'Agent',
23
+ description: 'Agentic worker that executes a provided prompt using blueprints and tools.',
24
+ inputSchema: Schema.Struct({
25
+ prompt: Type.Ref(Prompt.Prompt),
26
+ systemPrompt: Type.Ref(Prompt.Prompt).pipe(Schema.optional),
27
+ /**
28
+ * @default @anthropic/claude-opus-4-0
29
+ */
30
+ model: Schema.optional(ModelName),
31
+ /**
32
+ * Input object or data.
33
+ * References get auto-resolved.
34
+ */
35
+ input: Schema.Record({ key: Schema.String, value: Schema.Any }),
36
+ }),
37
+ outputSchema: Schema.Any,
38
+ handler: Effect.fnUntraced(function* ({ data }) {
39
+ log.info('processing input', { input: data.input });
40
+
41
+ const input = { ...data.input };
42
+ for (const key of Object.keys(data.input)) {
43
+ const value = data.input[key];
44
+ if (Ref.isRef(value)) {
45
+ const object = yield* DatabaseService.load(value);
46
+ input[key] = Obj.toJSON(object);
47
+ } else {
48
+ input[key] = JSON.stringify(value);
49
+ }
50
+ }
51
+
52
+ yield* DatabaseService.flush({ indexes: true });
53
+ const prompt = yield* DatabaseService.load(data.prompt);
54
+ const systemPrompt = data.systemPrompt ? yield* DatabaseService.load(data.systemPrompt) : undefined;
55
+ yield* TracingService.emitStatus({ message: `Running ${prompt.id}` });
56
+
57
+ log.info('starting agent', { prompt: prompt.id, input: data.input });
58
+
59
+ const blueprints = yield* Function.pipe(
60
+ prompt.blueprints,
61
+ Array.appendAll(systemPrompt?.blueprints ?? []),
62
+ Effect.forEach(DatabaseService.loadOption),
63
+ Effect.map(Array.filter(Option.isSome)),
64
+ Effect.map(Array.map((option) => option.value)),
65
+ );
66
+ const objects = yield* Function.pipe(
67
+ prompt.context,
68
+ Array.appendAll(systemPrompt?.context ?? []),
69
+ Effect.forEach(DatabaseService.loadOption),
70
+ Effect.map(Array.filter(Option.isSome)),
71
+ Effect.map(Array.map((option) => option.value)),
72
+ );
73
+ const toolkit = yield* createToolkit({ blueprints });
74
+
75
+ const promptInstructions = yield* DatabaseService.load(prompt.instructions.source);
76
+ const promptText = Template.process(promptInstructions.content, input);
77
+
78
+ const systemInstructions = systemPrompt ? yield* DatabaseService.load(systemPrompt.instructions.source) : undefined;
79
+ const systemText = systemInstructions ? Template.process(systemInstructions.content, {}) : undefined;
80
+
81
+ const session = new AiSession();
82
+ const result = yield* session
83
+ .run({
84
+ prompt: promptText,
85
+ system: systemText,
86
+ blueprints,
87
+ objects: objects as Obj.Any[],
88
+ toolkit,
89
+ observer: GenerationObserver.fromPrinter(new ConsolePrinter({ tag: 'agent' })),
90
+ })
91
+ .pipe(Effect.provide(AiService.model(data.model ?? DEFAULT_MODEL)));
92
+ const lastBlock = result
93
+ .at(-1)
94
+ ?.blocks.filter((block) => block._tag === 'text')
95
+ .at(-1);
96
+
97
+ return {
98
+ note: lastBlock?.text,
99
+ };
100
+ }),
101
+ });
@@ -0,0 +1,59 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Toolkit from '@effect/ai/Toolkit';
6
+ import * as FetchHttpClient from '@effect/platform/FetchHttpClient';
7
+ import { describe, it } from '@effect/vitest';
8
+ import * as Config from 'effect/Config';
9
+ import * as Effect from 'effect/Effect';
10
+ import * as Layer from 'effect/Layer';
11
+
12
+ import { AiService } from '@dxos/ai';
13
+ import { AiServiceTestingPreset } from '@dxos/ai/testing';
14
+ import { makeToolExecutionServiceFromFunctions, makeToolResolverFromFunctions } from '@dxos/assistant';
15
+ import { TestHelpers } from '@dxos/effect';
16
+ import { ComputeEventLogger, CredentialsService, FunctionInvocationService, TracingService } from '@dxos/functions';
17
+ import { TestDatabaseLayer } from '@dxos/functions/testing';
18
+
19
+ import { default as fetchMessages } from './fetch-messages';
20
+
21
+ const TestLayer = Layer.mergeAll(
22
+ AiService.model('@anthropic/claude-opus-4-0'),
23
+ makeToolResolverFromFunctions([], Toolkit.make()),
24
+ makeToolExecutionServiceFromFunctions(Toolkit.make() as any, Layer.empty as any),
25
+ ComputeEventLogger.layerFromTracing,
26
+ ).pipe(
27
+ Layer.provideMerge(
28
+ Layer.mergeAll(
29
+ AiServiceTestingPreset('direct'),
30
+ TestDatabaseLayer({}),
31
+ CredentialsService.layerConfig([{ service: 'discord.com', apiKey: Config.redacted('DISCORD_TOKEN') }]),
32
+ FetchHttpClient.layer,
33
+ FunctionInvocationService.layerTestMocked({ functions: [fetchMessages] }).pipe(
34
+ Layer.provideMerge(ComputeEventLogger.layerFromTracing),
35
+ Layer.provideMerge(TracingService.layerLogInfo()),
36
+ ),
37
+ ),
38
+ ),
39
+ );
40
+
41
+ const DXOS_SERVER_ID = '837138313172353095';
42
+
43
+ describe('Feed', { timeout: 600_000 }, () => {
44
+ it.effect(
45
+ 'fetch discord messages',
46
+ Effect.fnUntraced(
47
+ function* (_) {
48
+ const messages = yield* FunctionInvocationService.invokeFunction(fetchMessages, {
49
+ serverId: DXOS_SERVER_ID,
50
+ // channelId: '1404487604761526423',
51
+ last: '7d',
52
+ });
53
+ console.log(messages);
54
+ },
55
+ Effect.provide(TestLayer),
56
+ TestHelpers.taggedTest('sync'),
57
+ ),
58
+ );
59
+ });
@@ -0,0 +1,251 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as FetchHttpClient from '@effect/platform/FetchHttpClient';
6
+ import { DiscordConfig, DiscordREST, DiscordRESTMemoryLive } from 'dfx';
7
+ import type {
8
+ GuildChannelResponse,
9
+ MessageResponse,
10
+ PrivateChannelResponse,
11
+ PrivateGroupChannelResponse,
12
+ ThreadResponse,
13
+ } from 'dfx/types';
14
+ import * as Array from 'effect/Array';
15
+ import * as Effect from 'effect/Effect';
16
+ import * as Function from 'effect/Function';
17
+ import * as Layer from 'effect/Layer';
18
+ import * as Option from 'effect/Option';
19
+ import * as Schema from 'effect/Schema';
20
+
21
+ import { Obj } from '@dxos/echo';
22
+ import { CredentialsService, TracingService, defineFunction } from '@dxos/functions';
23
+ import { log } from '@dxos/log';
24
+ import { DataType } from '@dxos/schema';
25
+
26
+ // TODO(dmaretskyi): Extract.
27
+ const TimeRange = class extends Schema.String.pipe(Schema.pattern(/\d+(s|m|h|d)/)).annotations({
28
+ description: 'Time range. 1d - 1 day, 2h - 2 hours, 30m - 30 minutes, 15s - 15 seconds.',
29
+ examples: ['1d', '2h', '30m', '15s'],
30
+ }) {
31
+ static toSeconds(timeRange: Schema.Schema.Type<typeof TimeRange>) {
32
+ const match = timeRange.match(/(\d+)(s|m|h|d)/);
33
+ if (!match) {
34
+ throw new Error(`Invalid time range: ${timeRange}`);
35
+ }
36
+ const [_, amount, unit] = match;
37
+ switch (unit) {
38
+ case 's':
39
+ return Number(amount);
40
+ case 'm':
41
+ return Number(amount) * 60;
42
+ case 'h':
43
+ return Number(amount) * 60 * 60;
44
+ case 'd':
45
+ return Number(amount) * 24 * 60 * 60;
46
+ default:
47
+ throw new Error(`Invalid time range unit: ${unit}`);
48
+ }
49
+ }
50
+ };
51
+ type TimeRange = Schema.Schema.Type<typeof TimeRange>;
52
+
53
+ const DiscordConfigFromCredential = Layer.unwrapEffect(
54
+ Effect.gen(function* () {
55
+ return DiscordConfig.layer({
56
+ token: yield* CredentialsService.getApiKey({ service: 'discord.com' }),
57
+ rest: {
58
+ baseUrl: 'https://api-proxy.dxos.workers.dev/discord.com/api/v10',
59
+ },
60
+ });
61
+ }),
62
+ );
63
+
64
+ type DiscordChannel = GuildChannelResponse | PrivateChannelResponse | PrivateGroupChannelResponse | ThreadResponse;
65
+
66
+ const DEFAULT_AFTER = 1704067200; // 2024-01-01
67
+ const DEFAULT_LIMIT = 500;
68
+ const DEFAULT_IGNORE_USERNAMES = ['GitHub', 'Needle'];
69
+
70
+ // TODO(dmaretskyi): Align with standard thread type.
71
+ type Thread = {
72
+ discordChannelId: string;
73
+ name?: string;
74
+ messages: DataType.Message[];
75
+ };
76
+
77
+ export default defineFunction({
78
+ key: 'dxos.org/function/fetch-discord-messages',
79
+ name: 'Sync Discord messages',
80
+ inputSchema: Schema.Struct({
81
+ serverId: Schema.String.annotations({
82
+ description: 'The ID of the server to fetch messages from.',
83
+ }),
84
+ channelId: Schema.optional(Schema.String).annotations({
85
+ description:
86
+ 'The ID of the channel to fetch messages from. Will crawl all channels from the server if not specified.',
87
+ }),
88
+ after: Schema.optional(Schema.Number).annotations({
89
+ description:
90
+ 'Fetch messages that were sent after a given date. Unix timestamp in seconds. Exclusive with `last`.',
91
+ }),
92
+ last: TimeRange.annotations({
93
+ description:
94
+ 'Time range to fetch most recent messages. Specifies the range in the past, from now. "1d" would fetch messages from the last 24 hours.',
95
+ }),
96
+ limit: Schema.optional(Schema.Number).annotations({
97
+ description: 'The maximum number of messages to fetch.',
98
+ }),
99
+ pageSize: Schema.optional(Schema.Number).annotations({
100
+ description: 'The number of messages to fetch per page.',
101
+ }),
102
+ ignoreUsernames: Schema.optional(Schema.Array(Schema.String)).annotations({
103
+ description: 'Exclude messages from these usernames.',
104
+ }),
105
+ }),
106
+ handler: Effect.fnUntraced(
107
+ function* ({
108
+ data: {
109
+ serverId,
110
+ channelId,
111
+ after,
112
+ last,
113
+ pageSize = 100,
114
+ limit = DEFAULT_LIMIT,
115
+ ignoreUsernames = DEFAULT_IGNORE_USERNAMES,
116
+ },
117
+ }) {
118
+ if (!after && !last) {
119
+ throw new Error('cannot specify both `after` and `last`');
120
+ }
121
+ const afterTs = last ? Date.now() / 1000 - TimeRange.toSeconds(last) : (after ?? DEFAULT_AFTER);
122
+
123
+ const rest = yield* DiscordREST;
124
+
125
+ let channels: DiscordChannel[] = [];
126
+ channels.push(...(yield* rest.listGuildChannels(serverId)));
127
+ const { threads: guildThreads } = yield* rest.getActiveGuildThreads(serverId);
128
+ channels.push(...guildThreads);
129
+ if (channelId) {
130
+ channels = channels.filter((channel) => channel.id === channelId);
131
+ }
132
+ if (channels.length === 0) {
133
+ throw new Error('no channels found');
134
+ }
135
+ for (const channel of channels) {
136
+ console.log(channel.id, 'name' in channel ? channel.name : undefined);
137
+ }
138
+
139
+ yield* TracingService.emitStatus({ message: `Will fetch from channels: ${channels.length}` });
140
+
141
+ const threads = yield* Effect.forEach(
142
+ channels,
143
+ Effect.fnUntraced(function* (channel) {
144
+ const allMessages: DataType.Message[] = [];
145
+
146
+ let lastMessage: Option.Option<DataType.Message> = Option.none();
147
+ while (true) {
148
+ const { id: lastId = undefined } = Function.pipe(
149
+ lastMessage,
150
+ Option.map(Obj.getKeys('discord.com')),
151
+ Option.flatMap(Option.fromIterable),
152
+ Option.getOrElse(() => ({ id: undefined })),
153
+ );
154
+
155
+ const options = {
156
+ after: !lastId ? `${generateSnowflake(afterTs)}` : lastId,
157
+ limit: pageSize,
158
+ };
159
+ log.info('fetching messages', {
160
+ lastId,
161
+ afterTs,
162
+ afterSnowflake: options.after,
163
+ after: parseSnowflake(options.after),
164
+ limit: options.limit,
165
+ });
166
+ const messages = yield* rest.listMessages(channel.id, options).pipe(
167
+ Effect.map(Array.map(makeMessage)),
168
+ Effect.map(Array.reverse),
169
+ Effect.catchTag('ErrorResponse', (err) =>
170
+ err.cause.code === 50001 ? Effect.succeed([]) : Effect.fail(err),
171
+ ),
172
+ );
173
+ if (messages.length > 0) {
174
+ lastMessage = Option.fromNullable(messages.at(-1));
175
+ allMessages.push(...messages);
176
+ } else {
177
+ break;
178
+ }
179
+ yield* TracingService.emitStatus({ message: `Fetched messages: ${allMessages.length}` });
180
+ if (allMessages.length >= limit) {
181
+ break;
182
+ }
183
+ }
184
+
185
+ return {
186
+ discordChannelId: channel.id,
187
+ name: 'name' in channel ? (channel.name ?? undefined) : undefined,
188
+ messages: allMessages
189
+ .filter((message) => !message.sender.name || !ignoreUsernames.includes(message.sender.name))
190
+ .filter((message) =>
191
+ message.blocks.some((block) => block._tag === 'text' && block.text.trim().length > 0),
192
+ ),
193
+ } satisfies Thread;
194
+ }),
195
+ { concurrency: 10 },
196
+ );
197
+
198
+ return threads
199
+ .filter((thread) => thread.messages.length > 0)
200
+ .map(serializeThread)
201
+ .join('\n');
202
+ },
203
+ Effect.provide(
204
+ DiscordRESTMemoryLive.pipe(Layer.provideMerge(DiscordConfigFromCredential)).pipe(
205
+ Layer.provide(FetchHttpClient.layer),
206
+ ),
207
+ ),
208
+ Effect.orDie,
209
+ ),
210
+ });
211
+
212
+ /**
213
+ * @param unixTimestamp in seconds
214
+ */
215
+ const generateSnowflake = (unixTimestamp: number): bigint => {
216
+ const discordEpoch = 1420070400000n; // Discord Epoch (ms)
217
+ return (BigInt(unixTimestamp * 1000) - discordEpoch) << 22n;
218
+ };
219
+
220
+ const parseSnowflake = (snowflake: string): Date => {
221
+ const discordEpoch = 1420070400000n; // Discord Epoch (ms)
222
+ return new Date(Number((BigInt(snowflake) >> 22n) + discordEpoch));
223
+ };
224
+
225
+ const makeMessage = (message: MessageResponse): DataType.Message =>
226
+ Obj.make(DataType.Message, {
227
+ [Obj.Meta]: {
228
+ keys: [
229
+ { id: message.id, source: 'discord.com' },
230
+ { id: message.channel_id, source: 'discord.com/thread' },
231
+ ],
232
+ },
233
+ sender: { name: message.author.username },
234
+ created: message.timestamp,
235
+ blocks: [{ _tag: 'text', text: message.content }],
236
+ });
237
+
238
+ /**
239
+ * Standard JSON serialization is to verbose for large amounts of data.
240
+ */
241
+ const serializeThread = (thread: Thread): string => {
242
+ return `<thread id=${thread.discordChannelId} name=${thread.name ?? ''}>\n${thread.messages
243
+ .map(
244
+ (message) =>
245
+ ` ${message.sender.name}: ${message.blocks
246
+ .filter((block) => block._tag === 'text')
247
+ .map((block) => block.text)
248
+ .join(' ')}`,
249
+ )
250
+ .join('\n')}\n</thread>`;
251
+ };
@@ -0,0 +1,9 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { default as fetch$ } from './fetch-messages';
6
+
7
+ export namespace Discord {
8
+ export const fetch = fetch$;
9
+ }
@@ -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 Document {
9
+ export const read = read$;
10
+ export const update = update$;
11
+ }
@@ -0,0 +1,29 @@
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 { DatabaseService, defineFunction } from '@dxos/functions';
10
+ import { Markdown } from '@dxos/plugin-markdown/types';
11
+
12
+ export default defineFunction({
13
+ key: 'dxos.org/function/markdown/read',
14
+ name: 'Read markdown document',
15
+ description: 'Read markdown document.',
16
+ inputSchema: Schema.Struct({
17
+ id: ArtifactId.annotations({
18
+ description: 'The ID of the document to read.',
19
+ }),
20
+ }),
21
+ outputSchema: Schema.Struct({
22
+ content: Schema.String,
23
+ }),
24
+ handler: Effect.fn(function* ({ data: { id } }) {
25
+ const doc = yield* DatabaseService.resolve(ArtifactId.toDXN(id), Markdown.Document);
26
+ const { content } = yield* DatabaseService.load(doc.content);
27
+ return { content };
28
+ }),
29
+ });
@@ -0,0 +1,30 @@
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 { DatabaseService, defineFunction } from '@dxos/functions';
10
+ import { Markdown } from '@dxos/plugin-markdown/types';
11
+
12
+ export default defineFunction({
13
+ key: 'dxos.org/function/markdown/update',
14
+ name: 'Update markdown',
15
+ description: 'Updates the entire contents of the markdown document.',
16
+ inputSchema: Schema.Struct({
17
+ id: ArtifactId.annotations({
18
+ description: 'The ID of the document to write.',
19
+ }),
20
+ content: Schema.String.annotations({
21
+ description: 'New content to write to the document.',
22
+ }),
23
+ }),
24
+ outputSchema: Schema.Void,
25
+ handler: Effect.fn(function* ({ data: { id, content } }) {
26
+ const doc = yield* DatabaseService.resolve(ArtifactId.toDXN(id), Markdown.Document);
27
+ const text = yield* DatabaseService.load(doc.content);
28
+ text.content = content;
29
+ }),
30
+ });