@dxos/assistant 0.9.0 → 0.9.1-staging.ee54ba693a

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 (84) hide show
  1. package/dist/lib/neutral/extraction/index.mjs +108 -119
  2. package/dist/lib/neutral/extraction/index.mjs.map +4 -4
  3. package/dist/lib/neutral/index.mjs +395 -234
  4. package/dist/lib/neutral/index.mjs.map +4 -4
  5. package/dist/lib/neutral/meta.json +1 -1
  6. package/dist/types/src/extraction/extraction-llm-function/index.d.ts +2 -0
  7. package/dist/types/src/extraction/extraction-llm-function/index.d.ts.map +1 -0
  8. package/dist/types/src/extraction/extraction-llm-function/proper-noun-extraction.d.ts +26 -0
  9. package/dist/types/src/extraction/extraction-llm-function/proper-noun-extraction.d.ts.map +1 -0
  10. package/dist/types/src/extraction/index.d.ts +1 -1
  11. package/dist/types/src/extraction/index.d.ts.map +1 -1
  12. package/dist/types/src/extraction/quotes.d.ts +8 -1
  13. package/dist/types/src/extraction/quotes.d.ts.map +1 -1
  14. package/dist/types/src/index.d.ts +1 -1
  15. package/dist/types/src/index.d.ts.map +1 -1
  16. package/dist/types/src/request/AiRequest.d.ts +19 -208
  17. package/dist/types/src/request/AiRequest.d.ts.map +1 -1
  18. package/dist/types/src/request/format.d.ts +1 -1
  19. package/dist/types/src/request/format.d.ts.map +1 -1
  20. package/dist/types/src/session/AiContext.d.ts +27 -105
  21. package/dist/types/src/session/AiContext.d.ts.map +1 -1
  22. package/dist/types/src/session/AiSession.d.ts +11 -29
  23. package/dist/types/src/session/AiSession.d.ts.map +1 -1
  24. package/dist/types/src/session/Harness.d.ts +122 -0
  25. package/dist/types/src/session/Harness.d.ts.map +1 -0
  26. package/dist/types/src/session/SessionLink.d.ts +8 -11
  27. package/dist/types/src/session/SessionLink.d.ts.map +1 -1
  28. package/dist/types/src/session/SessionLoader.d.ts +2 -2
  29. package/dist/types/src/session/SessionLoader.d.ts.map +1 -1
  30. package/dist/types/src/session/SkillHooks.d.ts +28 -0
  31. package/dist/types/src/session/SkillHooks.d.ts.map +1 -0
  32. package/dist/types/src/session/harness-control.d.ts +177 -0
  33. package/dist/types/src/session/harness-control.d.ts.map +1 -0
  34. package/dist/types/src/session/index.d.ts +3 -0
  35. package/dist/types/src/session/index.d.ts.map +1 -1
  36. package/dist/types/src/session/toolkit.d.ts +4 -4
  37. package/dist/types/src/session/toolkit.d.ts.map +1 -1
  38. package/dist/types/src/templates/system.d.ts +4 -6
  39. package/dist/types/src/templates/system.d.ts.map +1 -1
  40. package/dist/types/src/tool-runtime/index.d.ts.map +1 -0
  41. package/dist/types/src/tool-runtime/services.d.ts.map +1 -0
  42. package/dist/types/src/util/artifact.d.ts.map +1 -1
  43. package/dist/types/src/util/diff.d.ts +9 -3
  44. package/dist/types/src/util/diff.d.ts.map +1 -1
  45. package/dist/types/src/util/tracing.d.ts +40 -0
  46. package/dist/types/src/util/tracing.d.ts.map +1 -1
  47. package/dist/types/tsconfig.tsbuildinfo +1 -1
  48. package/package.json +28 -25
  49. package/src/extraction/extraction-llm-function/index.ts +5 -0
  50. package/src/extraction/extraction-llm-function/proper-noun-extraction.ts +78 -0
  51. package/src/extraction/index.ts +1 -1
  52. package/src/extraction/quotes.test.ts +67 -2
  53. package/src/extraction/quotes.ts +47 -13
  54. package/src/index.ts +1 -1
  55. package/src/request/AiRequest.ts +45 -14
  56. package/src/request/format.ts +9 -11
  57. package/src/session/AiContext.test.ts +52 -32
  58. package/src/session/AiContext.ts +73 -95
  59. package/src/session/AiSession.test.ts +4 -4
  60. package/src/session/AiSession.ts +51 -83
  61. package/src/session/Harness.ts +241 -0
  62. package/src/session/SessionLink.ts +8 -8
  63. package/src/session/SessionLoader.ts +4 -4
  64. package/src/session/SkillHooks.ts +71 -0
  65. package/src/session/harness-control.ts +30 -0
  66. package/src/session/index.ts +3 -0
  67. package/src/session/toolkit.ts +8 -8
  68. package/src/templates/instructions/format.tpl +3 -3
  69. package/src/templates/instructions/system.tpl +10 -12
  70. package/src/templates/system.ts +6 -2
  71. package/src/util/artifact.ts +9 -2
  72. package/src/util/diff.test.ts +4 -4
  73. package/src/util/diff.ts +15 -8
  74. package/src/util/tracing.ts +1 -1
  75. package/dist/types/src/extraction/extraction.d.ts +0 -1677
  76. package/dist/types/src/extraction/extraction.d.ts.map +0 -1
  77. package/dist/types/src/functions/index.d.ts.map +0 -1
  78. package/dist/types/src/functions/services.d.ts.map +0 -1
  79. package/src/extraction/extraction-llm-function/transcription-instructions.tpl +0 -28
  80. package/src/extraction/extraction.ts +0 -105
  81. /package/dist/types/src/{functions → tool-runtime}/index.d.ts +0 -0
  82. /package/dist/types/src/{functions → tool-runtime}/services.d.ts +0 -0
  83. /package/src/{functions → tool-runtime}/index.ts +0 -0
  84. /package/src/{functions → tool-runtime}/services.ts +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/assistant",
3
- "version": "0.9.0",
3
+ "version": "0.9.1-staging.ee54ba693a",
4
4
  "description": "Natural language interface to DXOS",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -30,41 +30,44 @@
30
30
  "src"
31
31
  ],
32
32
  "dependencies": {
33
- "@automerge/automerge": "3.3.0-fragments.1",
33
+ "@automerge/automerge": "3.3.0-fragments.2",
34
34
  "@effect-atom/atom": "^0.5.3",
35
35
  "@effect-atom/atom-react": "^0.5.0",
36
36
  "@effect/ai": "0.36.0",
37
37
  "@effect/experimental": "0.60.0",
38
- "@effect/platform": "0.96.1",
38
+ "@effect/platform": "0.96.2",
39
+ "@effect/rpc": "0.75.1",
39
40
  "@xenova/transformers": "^2.17.2",
40
41
  "lodash.defaultsdeep": "^4.6.1",
41
- "@dxos/async": "0.9.0",
42
- "@dxos/compute": "0.9.0",
43
- "@dxos/context": "0.9.0",
44
- "@dxos/debug": "0.9.0",
45
- "@dxos/echo": "0.9.0",
46
- "@dxos/echo-client": "0.9.0",
47
- "@dxos/echo-protocol": "0.9.0",
48
- "@dxos/effect": "0.9.0",
49
- "@dxos/errors": "0.9.0",
50
- "@dxos/functions": "0.9.0",
51
- "@dxos/graph": "0.9.0",
52
- "@dxos/invariant": "0.9.0",
53
- "@dxos/log": "0.9.0",
54
- "@dxos/keys": "0.9.0",
55
- "@dxos/mcp-client": "0.9.0",
56
- "@dxos/node-std": "0.9.0",
57
- "@dxos/schema": "0.9.0",
58
- "@dxos/types": "0.9.0",
59
- "@dxos/util": "0.9.0",
60
- "@dxos/ai": "0.9.0"
42
+ "@dxos/ai": "0.9.1-staging.ee54ba693a",
43
+ "@dxos/async": "0.9.1-staging.ee54ba693a",
44
+ "@dxos/compute": "0.9.1-staging.ee54ba693a",
45
+ "@dxos/compute-runtime": "0.9.1-staging.ee54ba693a",
46
+ "@dxos/debug": "0.9.1-staging.ee54ba693a",
47
+ "@dxos/context": "0.9.1-staging.ee54ba693a",
48
+ "@dxos/echo-client": "0.9.1-staging.ee54ba693a",
49
+ "@dxos/echo-doc": "0.9.1-staging.ee54ba693a",
50
+ "@dxos/effect": "0.9.1-staging.ee54ba693a",
51
+ "@dxos/echo": "0.9.1-staging.ee54ba693a",
52
+ "@dxos/echo-protocol": "0.9.1-staging.ee54ba693a",
53
+ "@dxos/errors": "0.9.1-staging.ee54ba693a",
54
+ "@dxos/functions": "0.9.1-staging.ee54ba693a",
55
+ "@dxos/graph": "0.9.1-staging.ee54ba693a",
56
+ "@dxos/keys": "0.9.1-staging.ee54ba693a",
57
+ "@dxos/log": "0.9.1-staging.ee54ba693a",
58
+ "@dxos/invariant": "0.9.1-staging.ee54ba693a",
59
+ "@dxos/mcp-client": "0.9.1-staging.ee54ba693a",
60
+ "@dxos/node-std": "0.9.1-staging.ee54ba693a",
61
+ "@dxos/schema": "0.9.1-staging.ee54ba693a",
62
+ "@dxos/types": "0.9.1-staging.ee54ba693a",
63
+ "@dxos/util": "0.9.1-staging.ee54ba693a"
61
64
  },
62
65
  "devDependencies": {
63
66
  "@types/lodash.defaultsdeep": "^4.6.6",
64
- "effect": "3.21.3"
67
+ "effect": "3.21.4"
65
68
  },
66
69
  "peerDependencies": {
67
- "effect": "3.21.3"
70
+ "effect": "3.21.4"
68
71
  },
69
72
  "publishConfig": {
70
73
  "access": "public"
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './proper-noun-extraction';
@@ -0,0 +1,78 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as LanguageModel from '@effect/ai/LanguageModel';
6
+ import * as Effect from 'effect/Effect';
7
+ import * as Schema from 'effect/Schema';
8
+
9
+ import { AiService } from '@dxos/ai';
10
+ import { type Database, Obj } from '@dxos/echo';
11
+ import { Message } from '@dxos/types';
12
+
13
+ import { findReferences, insertReferences } from '../quotes';
14
+
15
+ const EXTRACTION_MODEL = 'com.anthropic.model.claude-haiku-4-5.default';
16
+
17
+ /**
18
+ * Proper nouns extracted from transcript text.
19
+ */
20
+ export const ProperNouns = Schema.Struct({
21
+ properNouns: Schema.Array(Schema.String).annotations({
22
+ description: 'Proper nouns (names of people, organizations, places, products) mentioned in the text.',
23
+ }),
24
+ });
25
+ export type ProperNouns = Schema.Schema.Type<typeof ProperNouns>;
26
+
27
+ /**
28
+ * Extracts proper nouns from transcript text using a small LLM (Haiku). Provides the LanguageModel
29
+ * internally so the residual requirement is just {@link AiService.AiService}.
30
+ */
31
+ export const extractProperNouns = (text: string) =>
32
+ Effect.gen(function* () {
33
+ const { value } = yield* Effect.scoped(
34
+ LanguageModel.generateObject({
35
+ schema: ProperNouns,
36
+ prompt: [
37
+ 'Extract every proper noun (people, organizations, places, products) mentioned in the transcript text below.',
38
+ 'Return only the surface strings exactly as they appear. Exclude common nouns and pronouns.',
39
+ '',
40
+ 'Transcript:',
41
+ text,
42
+ ].join('\n'),
43
+ }),
44
+ );
45
+
46
+ // Drop short tokens (e.g. "IT"): insertReferences replaces case-insensitive substrings, so a
47
+ // short noun would match inside unrelated words ("secur[IT]y") and corrupt neighbouring links.
48
+ return value.properNouns.map((noun) => noun.trim()).filter((noun) => noun.length >= 3);
49
+ }).pipe(Effect.provide(AiService.model(EXTRACTION_MODEL)));
50
+
51
+ /**
52
+ * Enriches a transcript message: for each transcript block, extracts proper nouns, links them to
53
+ * objects in the space via full-text search, and rewrites the text with `[noun](dxn)` references.
54
+ * Requires {@link AiService.AiService}; takes the space database handle.
55
+ */
56
+ export const enrichTranscriptMessage = (message: Message.Message, { db }: { db: Database.Database }) =>
57
+ Effect.gen(function* () {
58
+ const blocks = yield* Effect.forEach(message.blocks, (block) =>
59
+ Effect.gen(function* () {
60
+ if (block._tag !== 'transcript') {
61
+ return block;
62
+ }
63
+
64
+ const nouns = yield* extractProperNouns(block.text);
65
+ const references = yield* Effect.promise(() => findReferences([...nouns], db));
66
+ return { ...block, text: insertReferences(block.text, references) };
67
+ }),
68
+ );
69
+
70
+ // Preserve the message identity and metadata; enrichment only rewrites transcript block text.
71
+ return Obj.make(Message.Message, {
72
+ id: message.id,
73
+ sender: message.sender,
74
+ created: message.created,
75
+ properties: message.properties,
76
+ blocks,
77
+ });
78
+ });
@@ -2,6 +2,6 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- export * from './extraction';
5
+ export * from './extraction-llm-function';
6
6
  export * from './extraction-ner-function';
7
7
  export * from './quotes';
@@ -2,9 +2,13 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { describe, expect, test } from 'vitest';
5
+ import { afterEach, beforeEach, describe, expect, test } from 'vitest';
6
6
 
7
- import { insertReferences } from './quotes';
7
+ import { Obj } from '@dxos/echo';
8
+ import { EchoTestBuilder } from '@dxos/echo-client/testing';
9
+ import { Organization, Person } from '@dxos/types';
10
+
11
+ import { findReferences, insertReferences } from './quotes';
8
12
 
9
13
  describe('insertReferences', () => {
10
14
  test('should replace quotes with DXN references', () => {
@@ -24,4 +28,65 @@ describe('insertReferences', () => {
24
28
  "And what I'd like to talk today about is Steven Wolfram's concept of a [computational irreducibility](echo:/01JTG9JW11XGWJZ32AW8ET93D1).",
25
29
  );
26
30
  });
31
+
32
+ test('does not nest links when one quote is a substring of another', () => {
33
+ const quotes = {
34
+ references: [
35
+ { quote: 'Sarah', id: '01JTG9JW11XGWJZ32AW8ET93D1' },
36
+ { quote: 'Sarah Johnson', id: '01JTG9JW11XGWJZ32AW8ET93D1' },
37
+ ],
38
+ };
39
+
40
+ // "Sarah Johnson" is linked as a whole (processed first); the standalone "Sarah" is linked
41
+ // separately; the "Sarah" inside the first link is left untouched (no nested `[[..](..)](..)`).
42
+ expect(insertReferences('Sarah Johnson spoke. Later Sarah left.', quotes)).toBe(
43
+ '[Sarah Johnson](echo:/01JTG9JW11XGWJZ32AW8ET93D1) spoke. Later [Sarah](echo:/01JTG9JW11XGWJZ32AW8ET93D1) left.',
44
+ );
45
+ });
46
+
47
+ test('does not re-link a repeated quote inside an already-inserted link', () => {
48
+ const quotes = { references: [{ quote: 'Amco', id: '01JTG9JW11XGWJZ32AW8ET93D1' }] };
49
+
50
+ expect(insertReferences('Amco and Amco again.', quotes)).toBe(
51
+ '[Amco](echo:/01JTG9JW11XGWJZ32AW8ET93D1) and [Amco](echo:/01JTG9JW11XGWJZ32AW8ET93D1) again.',
52
+ );
53
+ });
54
+ });
55
+
56
+ describe('findReferences', () => {
57
+ let builder: EchoTestBuilder;
58
+
59
+ beforeEach(async () => {
60
+ builder = await new EchoTestBuilder().open();
61
+ });
62
+
63
+ afterEach(async () => {
64
+ await builder.close();
65
+ });
66
+
67
+ test('links nouns to objects via full-text search, including first-name tokens', async () => {
68
+ const { db } = await builder.createDatabase({ types: [Organization.Organization, Person.Person] });
69
+ const amco = db.add(Obj.make(Organization.Organization, { name: 'Amco' }));
70
+ const sarah = db.add(Obj.make(Person.Person, { fullName: 'Sarah Johnson' }));
71
+ await db.flush({ indexes: true });
72
+
73
+ const { references } = await findReferences(['Amco', 'Sarah', 'Nonexistent'], db);
74
+ const byQuote = new Map(references.map((reference) => [reference.quote, reference.id]));
75
+
76
+ expect(byQuote.get('Amco')).toEqual(amco.id);
77
+ // Full-text matches a single token within a multi-word field, so first-name mentions link to
78
+ // the full-name object — this is also what makes the overlap case in `insertReferences` real.
79
+ expect(byQuote.get('Sarah')).toEqual(sarah.id);
80
+ expect(byQuote.has('Nonexistent')).toBe(false);
81
+ });
82
+
83
+ test('deduplicates and trims nouns before lookup', async () => {
84
+ const { db } = await builder.createDatabase({ types: [Organization.Organization] });
85
+ db.add(Obj.make(Organization.Organization, { name: 'Amco' }));
86
+ await db.flush({ indexes: true });
87
+
88
+ const { references } = await findReferences(['Amco', 'Amco', ' Amco ', ''], db);
89
+
90
+ expect(references.filter((reference) => reference.quote === 'Amco')).toHaveLength(1);
91
+ });
27
92
  });
@@ -6,7 +6,6 @@ import * as Schema from 'effect/Schema';
6
6
 
7
7
  import { type Database, Filter, Query } from '@dxos/echo';
8
8
  import { EID, EntityId } from '@dxos/keys';
9
- import { log } from '@dxos/log';
10
9
 
11
10
  export const ReferencedQuotes = Schema.Struct({
12
11
  references: Schema.Array(
@@ -26,12 +25,30 @@ export const ReferencedQuotes = Schema.Struct({
26
25
  });
27
26
  export type ReferencedQuotes = Schema.Schema.Type<typeof ReferencedQuotes>;
28
27
 
29
- export const findQuotes = async (quotes: string[], db: Database.Database): Promise<ReferencedQuotes> => {
28
+ export type FindReferencesOptions = {
29
+ /** Search backend. Full-text matches proper nouns precisely; vector matches semantically. */
30
+ searchKind?: 'full-text' | 'vector';
31
+ };
32
+
33
+ /**
34
+ * Finds the best-matching context object for each noun and returns it as a reference.
35
+ */
36
+ export const findReferences = async (
37
+ nouns: string[],
38
+ db: Database.Database,
39
+ { searchKind = 'full-text' }: FindReferencesOptions = {},
40
+ ): Promise<ReferencedQuotes> => {
41
+ // Deduplicate and process longer nouns first: `insertReferences` rewrites case-insensitive
42
+ // substrings, so a repeated or shorter overlapping noun would re-replace already-linked text and
43
+ // produce nested/broken markdown links.
44
+ const normalizedNouns = Array.from(new Set(nouns.map((noun) => noun.trim()).filter((noun) => noun.length > 0))).sort(
45
+ (leftNoun, rightNoun) => rightNoun.length - leftNoun.length,
46
+ );
47
+
30
48
  const references = await Promise.all(
31
- quotes.map(async (quote) => {
32
- const objects = await db.query(Query.select(Filter.text(quote, { type: 'vector' }))).run();
33
- log.info('found quotes in vector index', { objects, quote });
34
- return objects.length > 0 ? [{ id: objects[0].id.toString(), quote }] : [];
49
+ normalizedNouns.map(async (noun) => {
50
+ const objects = await db.query(Query.select(Filter.text(noun, { type: searchKind }))).run();
51
+ return objects.length > 0 ? [{ id: objects[0].id.toString(), quote: noun }] : [];
35
52
  }),
36
53
  );
37
54
  return { references: references.flat() };
@@ -41,16 +58,33 @@ export const findQuotes = async (quotes: string[], db: Database.Database): Promi
41
58
  * Finds and replaces all quotes with DXNs references.
42
59
  */
43
60
  // TODO(dmaretskyi): Lookup and verifiy ids from provided context.
44
- export const insertReferences = (text: string, quotes: ReferencedQuotes) => {
45
- for (const quote of quotes.references) {
46
- if (!EntityId.isValid(quote.id)) {
47
- continue;
48
- }
61
+ export const insertReferences = (text: string, quotes: ReferencedQuotes): string => {
62
+ // Process longer quotes first so e.g. "Sarah Johnson" is linked before "Sarah". Combined with the
63
+ // link-aware replacement below this stops a shorter or overlapping quote from rewriting text that
64
+ // is already part of an inserted link (which would produce nested `[[..](..)](..)` markdown).
65
+ const references = quotes.references
66
+ .filter((quote) => EntityId.isValid(quote.id))
67
+ .sort((left, right) => right.quote.length - left.quote.length);
49
68
 
50
- // Use a case-insensitive regular expression to replace the quote.
69
+ for (const quote of references) {
70
+ // Case-insensitive match of the (regex-escaped) quote.
51
71
  const regex = new RegExp(quote.quote.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
52
- text = text.replace(regex, `[${quote.quote}](${EID.make({ entityId: quote.id })})`);
72
+ text = replaceOutsideLinks(text, regex, `[${quote.quote}](${EID.make({ entityId: quote.id })})`);
53
73
  }
54
74
 
55
75
  return text;
56
76
  };
77
+
78
+ const MARKDOWN_LINK = /\[[^\]]*\]\([^)]*\)/g;
79
+
80
+ /** Applies a replacement to every span of `text` that is not already inside a `[label](url)` link. */
81
+ const replaceOutsideLinks = (text: string, regex: RegExp, replacement: string): string => {
82
+ let result = '';
83
+ let plainStart = 0;
84
+ for (const link of text.matchAll(MARKDOWN_LINK)) {
85
+ const index = link.index ?? 0;
86
+ result += text.slice(plainStart, index).replace(regex, replacement) + link[0];
87
+ plainStart = index + link[0].length;
88
+ }
89
+ return result + text.slice(plainStart).replace(regex, replacement);
90
+ };
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- export * from './functions';
5
+ export * from './tool-runtime';
6
6
  export * from './request';
7
7
  export * from './session';
8
8
  export * from './templates';
@@ -19,19 +19,19 @@ import {
19
19
  AiPreprocessor,
20
20
  AiSummarizer,
21
21
  type AiToolNotFoundError,
22
- callTool,
23
22
  type OpaqueToolkit,
24
23
  type PromptPreprocessingError,
25
24
  type ToolExecutionService,
26
25
  type ToolResolverService,
26
+ callTool,
27
27
  withoutToolCallParising,
28
28
  } from '@dxos/ai';
29
- import { type Blueprint, Trace, Operation } from '@dxos/compute';
29
+ import { Operation, type Skill, Trace } from '@dxos/compute';
30
30
  import { Database, Obj, Registry } from '@dxos/echo';
31
31
  import { log } from '@dxos/log';
32
32
  import { ContentBlock, Message } from '@dxos/types';
33
33
 
34
- import { getOperationFromTool } from '../functions/services';
34
+ import { getOperationFromTool } from '../tool-runtime/services';
35
35
  import { type AiAssistantError, CompleteBlock, PartialBlock } from '../util';
36
36
  import { formatSystemPrompt, formatUserPrompt } from './format';
37
37
  import { GenerationObserver } from './observer';
@@ -60,6 +60,13 @@ export type Options = {
60
60
  * This is useful for streaming the output to a queue.
61
61
  */
62
62
  onOutput?: (message: Message.Message) => Effect.Effect<void, never, never>;
63
+
64
+ /**
65
+ * When false, turn messages are not appended to the feed or written as persisted trace blocks.
66
+ *
67
+ * @default true
68
+ */
69
+ persist?: boolean;
63
70
  };
64
71
 
65
72
  export type RunProps<R = never> = {
@@ -68,7 +75,7 @@ export type RunProps<R = never> = {
68
75
  system?: string;
69
76
  history?: Message.Message[];
70
77
  objects?: Obj.Unknown[];
71
- blueprints?: readonly Blueprint.Blueprint[];
78
+ skills?: readonly Skill.Skill[];
72
79
  toolkit?: OpaqueToolkit.OpaqueToolkit<R>;
73
80
  };
74
81
 
@@ -77,7 +84,7 @@ export type BeginProps = {
77
84
  system?: string;
78
85
  history?: Message.Message[];
79
86
  objects?: Obj.Unknown[];
80
- blueprints?: readonly Blueprint.Blueprint[];
87
+ skills?: readonly Skill.Skill[];
81
88
  };
82
89
 
83
90
  export type TurnProps<R = never> = {
@@ -88,6 +95,12 @@ export type TurnProps<R = never> = {
88
95
  export type TurnResult = {
89
96
  messages: Message.Message[];
90
97
  done: boolean;
98
+ /**
99
+ * Provider finish reason for this turn. `pause` means the provider paused a server-tool turn
100
+ * mid-execution (e.g. Anthropic `pause_turn`) and the turn must be resumed by issuing another
101
+ * request without mutating the trailing assistant content.
102
+ */
103
+ finishReason?: ContentBlock.FinishReason;
91
104
  };
92
105
 
93
106
  /**
@@ -139,6 +152,9 @@ export class Request {
139
152
  Effect.gen(this, function* () {
140
153
  this._pending.push(message);
141
154
  yield* this._observer.onMessage(message);
155
+ if (this._options.persist === false) {
156
+ return message;
157
+ }
142
158
  for (const block of message.blocks) {
143
159
  log('write complete block', {
144
160
  messageId: message.id,
@@ -173,7 +189,7 @@ export class Request {
173
189
  prompt,
174
190
  system,
175
191
  history = [],
176
- blueprints = [],
192
+ skills = [],
177
193
  objects = [],
178
194
  }: BeginProps): Effect.Effect<void, RunError, RunRequirements> =>
179
195
  Effect.gen(this, function* () {
@@ -181,7 +197,7 @@ export class Request {
181
197
  this._history = [...history];
182
198
  this._pending = [];
183
199
 
184
- const systemPrompt = yield* formatSystemPrompt({ system, blueprints, objects }).pipe(Effect.orDie);
200
+ const systemPrompt = yield* formatSystemPrompt({ system, skills, objects }).pipe(Effect.orDie);
185
201
 
186
202
  if (this._options.summarizationThreshold !== undefined) {
187
203
  const tokenCount = yield* AiPreprocessor.estimateTokens(
@@ -200,7 +216,7 @@ export class Request {
200
216
 
201
217
  /**
202
218
  * Execute a single turn: one LLM generation followed by tool execution.
203
- * The toolkit and system prompt can be updated between turns to reflect context changes (e.g. dynamically enabled blueprints).
219
+ * The toolkit and system prompt can be updated between turns to reflect context changes (e.g. dynamically enabled skills).
204
220
  */
205
221
  runAgentTurn = <const R = never>({
206
222
  system,
@@ -222,6 +238,7 @@ export class Request {
222
238
 
223
239
  const observer = this._observer;
224
240
  let currentMessageId: Obj.ID | null = null;
241
+ let finishReason: ContentBlock.FinishReason | undefined;
225
242
 
226
243
  const messages = yield* LanguageModel.streamText({
227
244
  prompt,
@@ -240,6 +257,9 @@ export class Request {
240
257
  Stream.mapEffect(
241
258
  (block) =>
242
259
  Effect.gen(this, function* () {
260
+ if (block._tag === 'stats' && block.finishReason !== undefined) {
261
+ finishReason = block.finishReason;
262
+ }
243
263
  if (block.pending) {
244
264
  currentMessageId ??= Obj.ID.random();
245
265
  log('emit ephemeral message', { id: currentMessageId, type: block._tag });
@@ -271,16 +291,23 @@ export class Request {
271
291
  );
272
292
  log('messages', { messages });
273
293
 
294
+ // A paused server-tool turn (e.g. Anthropic `pause_turn`) is not complete: the provider
295
+ // expects the assistant content to be resent so it can finish executing the server tool.
296
+ // No local tool execution is needed — just another request.
297
+ if (finishReason === 'pause') {
298
+ return { messages, done: false, finishReason };
299
+ }
300
+
274
301
  const toolCalls = this.getToolCalls();
275
302
 
276
303
  if (toolCalls.length === 0) {
277
304
  this._ended = Date.now();
278
- return { messages, done: true };
305
+ return { messages, done: true, finishReason };
279
306
  } else if (!toolkit) {
280
307
  throw new Error('No toolkit provided');
281
308
  }
282
309
 
283
- return { messages, done: false };
310
+ return { messages, done: false, finishReason };
284
311
  }).pipe(Effect.withSpan('AiRequest.runAgentTurn'));
285
312
 
286
313
  runTools = <const R = never>({
@@ -317,19 +344,23 @@ export class Request {
317
344
  system: systemTemplate,
318
345
  history = [],
319
346
  objects = [],
320
- blueprints = [],
347
+ skills = [],
321
348
  toolkit,
322
349
  }: RunProps<R>): Effect.Effect<Message.Message[], RunError, RunRequirements | R> =>
323
350
  Effect.gen(this, function* () {
324
- yield* this.begin({ prompt, system: systemTemplate, history, objects, blueprints });
351
+ yield* this.begin({ prompt, system: systemTemplate, history, objects, skills });
325
352
 
326
- const system = yield* formatSystemPrompt({ system: systemTemplate, blueprints, objects }).pipe(Effect.orDie);
353
+ const system = yield* formatSystemPrompt({ system: systemTemplate, skills, objects }).pipe(Effect.orDie);
327
354
 
328
355
  do {
329
- const { done } = yield* this.runAgentTurn({ system, toolkit });
356
+ const { done, finishReason } = yield* this.runAgentTurn({ system, toolkit });
330
357
  if (done) {
331
358
  break;
332
359
  }
360
+ // A paused server-tool turn resumes with another request and no local tool execution.
361
+ if (finishReason === 'pause') {
362
+ continue;
363
+ }
333
364
  yield* this.runTools({ toolkit });
334
365
  } while (true);
335
366
 
@@ -25,31 +25,29 @@ import { ArtifactDiffResolver } from './artifact-diff';
25
25
  // TODO(burdon): Move to AiPreprocessor.
26
26
  export const formatSystemPrompt = ({
27
27
  system,
28
- blueprints = [],
28
+ skills = [],
29
29
  objects = [],
30
- }: Pick<AiRequest.RunProps, 'system' | 'blueprints' | 'objects'>): Effect.Effect<
30
+ }: Pick<AiRequest.RunProps, 'system' | 'skills' | 'objects'>): Effect.Effect<
31
31
  string,
32
32
  FunctionNotFoundError | EntityNotFoundError,
33
33
  Database.Service | Registry.Service | Operation.Service
34
34
  > =>
35
35
  Effect.gen(function* () {
36
- const blueprintDefs = yield* Function.pipe(
37
- blueprints,
38
- Effect.forEach((blueprint) => Effect.succeed(blueprint.instructions)),
36
+ const skillDefs = yield* Function.pipe(
37
+ skills,
38
+ Effect.forEach((skill) => Effect.succeed(skill.instructions)),
39
39
  Effect.flatMap(
40
40
  Effect.forEach((template) =>
41
41
  Effect.gen(function* () {
42
42
  return trim`
43
- <blueprint>
43
+ <skill>
44
44
  ${yield* Template.processTemplate(template)}
45
- </blueprint>
45
+ </skill>
46
46
  `;
47
47
  }),
48
48
  ),
49
49
  ),
50
- Effect.map((blueprints) =>
51
- blueprints.length > 0 ? ['## Blueprints Definitions', ...blueprints].join('\n\n') : undefined,
52
- ),
50
+ Effect.map((skills) => (skills.length > 0 ? ['## Skills Definitions', ...skills].join('\n\n') : undefined)),
53
51
  );
54
52
 
55
53
  const objectDefs = yield* Function.pipe(
@@ -66,7 +64,7 @@ export const formatSystemPrompt = ({
66
64
  );
67
65
 
68
66
  return yield* Function.pipe(
69
- Effect.succeed([system, blueprintDefs, objectDefs].filter((def): def is string => def !== undefined)),
67
+ Effect.succeed([system, skillDefs, objectDefs].filter((def): def is string => def !== undefined)),
70
68
  Effect.map((parts) => parts.join('\n\n')),
71
69
  );
72
70
  }).pipe(Effect.withSpan('formatSystemPrompt'));