@popcomputer/structured-chat 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +175 -108
  3. package/dist/adapters/openai-compatible-model.d.ts +4 -4
  4. package/dist/adapters/openai-compatible-model.js +33 -31
  5. package/dist/core/answer.d.ts +13 -13
  6. package/dist/core/answer.js +21 -12
  7. package/dist/core/chat.d.ts +12 -15
  8. package/dist/core/chat.js +36 -27
  9. package/dist/core/collect-stage.d.ts +28 -15
  10. package/dist/core/collect-stage.js +58 -37
  11. package/dist/core/command.d.ts +1 -1
  12. package/dist/core/command.js +1 -1
  13. package/dist/core/debug-protocol.d.ts +126 -0
  14. package/dist/core/debug-protocol.js +19 -0
  15. package/dist/core/debug.d.ts +103 -0
  16. package/dist/core/debug.js +276 -0
  17. package/dist/core/json-value.d.ts +1 -1
  18. package/dist/core/json-value.js +8 -1
  19. package/dist/core/model-guard.d.ts +2 -3
  20. package/dist/core/model-guard.js +4 -4
  21. package/dist/core/model.d.ts +15 -19
  22. package/dist/core/model.js +22 -10
  23. package/dist/core/protocol.d.ts +84 -79
  24. package/dist/core/protocol.js +18 -12
  25. package/dist/core/question.js +17 -15
  26. package/dist/core/repair.js +1 -1
  27. package/dist/core/session.d.ts +22 -28
  28. package/dist/core/session.js +16 -7
  29. package/dist/core/stage-name.d.ts +1 -1
  30. package/dist/core/stage-name.js +1 -1
  31. package/dist/core/stage.d.ts +4 -4
  32. package/dist/core/stage.js +11 -8
  33. package/dist/core/tool-set.js +6 -6
  34. package/dist/core/tool.d.ts +36 -39
  35. package/dist/core/tool.js +58 -31
  36. package/dist/core/view.d.ts +64 -39
  37. package/dist/core/view.js +12 -15
  38. package/dist/index.d.ts +2 -0
  39. package/dist/index.js +2 -0
  40. package/dist/integrations/assistant-ui-debug.d.ts +25 -0
  41. package/dist/integrations/assistant-ui-debug.js +1162 -0
  42. package/dist/integrations/assistant-ui.d.ts +10 -3
  43. package/dist/integrations/assistant-ui.js +48 -18
  44. package/dist/testing/in-memory-session-store.js +1 -1
  45. package/dist/testing/scenario.js +6 -6
  46. package/examples/answer-modes.ts +60 -49
  47. package/examples/prompt-injection-policy.ts +20 -20
  48. package/examples/resource-search.ts +104 -0
  49. package/package.json +15 -3
  50. package/examples/agency-search.ts +0 -101
@@ -1,4 +1,5 @@
1
1
  import { type ComponentType, type FC } from "react";
2
+ import type { StructuredChatDebugSnapshot } from "../core/debug.js";
2
3
  import type { ViewData, ViewDefinitionContract } from "../core/view.js";
3
4
  import { type StructuredChatSessionReference, type StructuredChatAssistantMessage } from "../core/protocol.js";
4
5
  /** Runtime status supplied by assistant-ui to a data-part renderer. */
@@ -52,6 +53,12 @@ export type AssistantChatFetch = (input: string, init: RequestInit) => Promise<R
52
53
  export interface AssistantChatModelAdapterOptions {
53
54
  readonly endpoint: string;
54
55
  readonly fetch?: AssistantChatFetch;
56
+ /**
57
+ * Select the explicit debug response contract and receive its safe state
58
+ * projection after every successful turn. Observer failures are ignored so
59
+ * they cannot change the outcome of an already-persisted chat turn.
60
+ */
61
+ readonly onDebugSnapshot?: (snapshot: StructuredChatDebugSnapshot) => void | Promise<void>;
55
62
  }
56
63
  /** Minimum assistant message shape consumed by the browser adapter. */
57
64
  export interface AssistantChatThreadMessage {
@@ -88,8 +95,8 @@ export interface AssistantChatModelAdapter {
88
95
  /**
89
96
  * Adapt assistant-ui to one server-owned structured chat endpoint.
90
97
  *
91
- * Only the latest text and opaque prior revision cross the browser boundary.
92
- * Tools, instructions, history, answer state, and stage position stay server
93
- * owned.
98
+ * By default, only the latest text and opaque prior revision cross the browser
99
+ * boundary. Supplying `onDebugSnapshot` explicitly selects the separate debug
100
+ * response contract and receives its safe answer-and-stage projection.
94
101
  */
95
102
  export declare const makeAssistantChatModelAdapter: (options: AssistantChatModelAdapterOptions) => AssistantChatModelAdapter;
@@ -1,7 +1,8 @@
1
1
  import { makeAssistantDataUI, } from "@assistant-ui/core/react";
2
- import { Either, Schema } from "effect";
2
+ import { Exit, Result, Schema } from "effect";
3
3
  import { createElement } from "react";
4
4
  import { StructuredChatSessionReferenceSchema, StructuredChatTurnRequestSchema, StructuredChatTurnResponseSchema, } from "../core/protocol.js";
5
+ import { JsonValueSchema } from "../core/json-value.js";
5
6
  /**
6
7
  * Register one defineView contract as a strictly decoded assistant-ui data UI.
7
8
  *
@@ -10,19 +11,25 @@ import { StructuredChatSessionReferenceSchema, StructuredChatTurnRequestSchema,
10
11
  */
11
12
  export const makeAssistantView = (view, config) => {
12
13
  const Renderer = (props) => {
13
- const decoded = Schema.decodeUnknownEither(view.partSchema)({
14
+ const data = Schema.decodeUnknownResult(JsonValueSchema)(props.data);
15
+ if (Result.isFailure(data)) {
16
+ return config.fallback === undefined
17
+ ? null
18
+ : createElement(config.fallback, props);
19
+ }
20
+ const decoded = view.decodeResult({
14
21
  type: "data",
15
22
  name: view.name,
16
- data: props.data,
17
- }, { onExcessProperty: "error" });
18
- if (Either.isLeft(decoded)) {
23
+ data: data.success,
24
+ });
25
+ if (Result.isFailure(decoded)) {
19
26
  return config.fallback === undefined
20
27
  ? null
21
28
  : createElement(config.fallback, props);
22
29
  }
23
30
  return createElement(config.render, {
24
31
  ...props,
25
- data: decoded.right.data,
32
+ data: decoded.success.data,
26
33
  });
27
34
  };
28
35
  return makeAssistantDataUI({
@@ -32,6 +39,15 @@ export const makeAssistantView = (view, config) => {
32
39
  };
33
40
  /** assistant-ui metadata key carrying the latest opaque session reference. */
34
41
  export const assistantChatSessionMetadataKey = "popcomputerStructuredChatSession";
42
+ const notifyDebugSnapshot = (callback, snapshot) => {
43
+ try {
44
+ const notified = callback(snapshot);
45
+ void Promise.resolve(notified).catch(() => undefined);
46
+ }
47
+ catch {
48
+ // Debug observers are deliberately isolated from the persisted turn.
49
+ }
50
+ };
35
51
  const readMessageText = (message) => message.content
36
52
  .flatMap((part) => part.type === "text" && "text" in part ? [part.text] : [])
37
53
  .join("\n")
@@ -42,9 +58,9 @@ const readLatestSession = (messages) => {
42
58
  if (message?.role !== "assistant") {
43
59
  continue;
44
60
  }
45
- const session = Schema.decodeUnknownEither(StructuredChatSessionReferenceSchema)(message.metadata.custom[assistantChatSessionMetadataKey]);
46
- if (Either.isRight(session)) {
47
- return session.right;
61
+ const session = Schema.decodeUnknownExit(StructuredChatSessionReferenceSchema)(message.metadata.custom[assistantChatSessionMetadataKey]);
62
+ if (Exit.isSuccess(session)) {
63
+ return session.value;
48
64
  }
49
65
  }
50
66
  return undefined;
@@ -65,13 +81,14 @@ const readTurnRequest = (messages) => {
65
81
  /**
66
82
  * Adapt assistant-ui to one server-owned structured chat endpoint.
67
83
  *
68
- * Only the latest text and opaque prior revision cross the browser boundary.
69
- * Tools, instructions, history, answer state, and stage position stay server
70
- * owned.
84
+ * By default, only the latest text and opaque prior revision cross the browser
85
+ * boundary. Supplying `onDebugSnapshot` explicitly selects the separate debug
86
+ * response contract and receives its safe answer-and-stage projection.
71
87
  */
72
88
  export const makeAssistantChatModelAdapter = (options) => {
73
89
  const fetch_ = options.fetch ??
74
90
  ((input, init) => globalThis.fetch(input, init));
91
+ const onDebugSnapshot = options.onDebugSnapshot;
75
92
  return {
76
93
  run: async ({ messages, abortSignal }) => {
77
94
  const request = readTurnRequest(messages);
@@ -95,17 +112,30 @@ export const makeAssistantChatModelAdapter = (options) => {
95
112
  catch {
96
113
  throw new Error("Structured chat returned an invalid response");
97
114
  }
98
- const decoded = Schema.decodeUnknownEither(StructuredChatTurnResponseSchema)(body, { onExcessProperty: "error" });
99
- if (Either.isLeft(decoded)) {
100
- throw new Error("Structured chat returned an invalid response");
115
+ let value;
116
+ if (onDebugSnapshot === undefined) {
117
+ const decoded = Schema.decodeUnknownExit(StructuredChatTurnResponseSchema)(body, { onExcessProperty: "error" });
118
+ if (Exit.isFailure(decoded)) {
119
+ throw new Error("Structured chat returned an invalid response");
120
+ }
121
+ value = decoded.value;
122
+ }
123
+ else {
124
+ const { StructuredChatDebugTurnResponseSchema } = await import("../core/debug-protocol.js");
125
+ const decoded = Schema.decodeUnknownExit(StructuredChatDebugTurnResponseSchema)(body, { onExcessProperty: "error" });
126
+ if (Exit.isFailure(decoded)) {
127
+ throw new Error("Structured chat returned an invalid response");
128
+ }
129
+ notifyDebugSnapshot(onDebugSnapshot, decoded.value.debug);
130
+ value = decoded.value;
101
131
  }
102
132
  return {
103
- content: decoded.right.message.content,
133
+ content: value.message.content,
104
134
  metadata: {
105
- custom: decoded.right.session === undefined
135
+ custom: value.session === undefined
106
136
  ? {}
107
137
  : {
108
- [assistantChatSessionMetadataKey]: decoded.right.session,
138
+ [assistantChatSessionMetadataKey]: value.session,
109
139
  },
110
140
  },
111
141
  };
@@ -30,7 +30,7 @@ const replaceSnapshot = (sessions, input) => {
30
30
  return [snapshot, next];
31
31
  };
32
32
  /** In-memory optimistic session store intended for tests and examples. */
33
- export const inMemoryChatSessionStore = Layer.effect(ChatSessionStore, Ref.make(new Map()).pipe(Effect.map((sessions) => ({
33
+ export const inMemoryChatSessionStore = Layer.effect(ChatSessionStore, Ref.make(new Map()).pipe(Effect.map((sessions) => ChatSessionStore.of({
34
34
  load: (scope) => Ref.get(sessions).pipe(Effect.map((current) => current.get(scopeKey(scope)) ?? null)),
35
35
  replace: (input) => Ref.modify(sessions, (current) => replaceSnapshot(current, input)).pipe(Effect.flatMap((result) => result instanceof ChatSessionConflict
36
36
  ? Effect.fail(result)
@@ -2,7 +2,7 @@ import { Effect, Layer, Ref, Schema } from "effect";
2
2
  import { StructuredChatModel, } from "../core/model.js";
3
3
  import { JsonValueSchema, } from "../core/json-value.js";
4
4
  const scenarioQuote = Symbol("@popcomputer/structured-chat/testing/ScenarioQuote");
5
- const ScenarioQuoteSchema = Schema.NonEmptyTrimmedString.pipe(Schema.maxLength(2_000));
5
+ const ScenarioQuoteSchema = Schema.Trimmed.check(Schema.isNonEmpty(), Schema.isMaxLength(2_000));
6
6
  const evidenceIndex = (request, quoted) => {
7
7
  if (quoted.messageIndex !== undefined) {
8
8
  const message = request.untrustedMessages[quoted.messageIndex];
@@ -49,7 +49,7 @@ const answers = (stage, proposed, options = {}) => ({
49
49
  if (value === undefined || answer === undefined) {
50
50
  continue;
51
51
  }
52
- encodedAnswers[field] = Schema.decodeSync(JsonValueSchema)(Schema.encodeSync(answer.schema)(value.value));
52
+ encodedAnswers[field] = Schema.decodeUnknownSync(JsonValueSchema)(Schema.encodeSync(answer.schema)(value.value));
53
53
  evidenceIndex(request, value);
54
54
  evidence.push({
55
55
  field,
@@ -76,7 +76,7 @@ const answers = (stage, proposed, options = {}) => ({
76
76
  const call = (tool, input) => ({
77
77
  respond: () => ({
78
78
  name: tool.name,
79
- arguments: Schema.decodeSync(JsonValueSchema)(Schema.encodeSync(tool.inputSchema)(input)),
79
+ arguments: Schema.decodeUnknownSync(JsonValueSchema)(Schema.encodeSync(tool.inputSchema)(input)),
80
80
  }),
81
81
  });
82
82
  const replace = (stage, field, value, options) => {
@@ -92,7 +92,7 @@ const replace = (stage, field, value, options) => {
92
92
  _tag: "ReplaceAcceptedAnswer",
93
93
  stage: stage.name,
94
94
  field,
95
- value: Schema.decodeSync(JsonValueSchema)(Schema.encodeSync(answer.schema)(value)),
95
+ value: Schema.decodeUnknownSync(JsonValueSchema)(Schema.encodeSync(answer.schema)(value)),
96
96
  evidence: {
97
97
  quote: support.quote,
98
98
  },
@@ -126,14 +126,14 @@ const repairs = (first, ...remaining) => ({
126
126
  });
127
127
  const model = (first, ...remaining) => Layer.effect(StructuredChatModel, Ref.make(0).pipe(Effect.map((cursor) => {
128
128
  const steps = [first, ...remaining];
129
- return {
129
+ return StructuredChatModel.of({
130
130
  requestTool: (request) => Ref.getAndUpdate(cursor, (index) => index + 1).pipe(Effect.flatMap((index) => {
131
131
  const step = steps[index];
132
132
  return step === undefined
133
133
  ? Effect.die(new Error(`Scenario model exhausted after ${steps.length} requests`))
134
134
  : Effect.sync(() => step.respond(request));
135
135
  })),
136
- };
136
+ });
137
137
  })));
138
138
  /** Typed constructors for concise valid transcript scenarios. */
139
139
  export const Scenario = {
@@ -9,84 +9,95 @@ import {
9
9
 
10
10
  const SearchCatalog = defineTool({
11
11
  name: "search_catalog",
12
- description: "Search the catalog for relevant agency matches.",
13
- input: Schema.Struct({ query: Schema.NonEmptyTrimmedString }),
12
+ description: "Search the catalog for relevant resources.",
13
+ input: Schema.Struct({
14
+ query: Schema.Trimmed.check(Schema.isNonEmpty()),
15
+ }),
14
16
  execute: ({ query }) => Effect.succeed({ query }),
15
17
  })
16
18
 
17
19
  const Search = Stage.tools({
18
20
  name: "search",
19
- instructions: ["Search the catalog using the completed project brief."],
21
+ instructions: ["Search the catalog using the completed request details."],
20
22
  tools: [SearchCatalog],
21
23
  })
22
24
 
23
- const ProjectBrief = Stage.collect({
24
- name: "project_brief",
25
+ const RequestDetails = Stage.collect({
26
+ name: "request_details",
25
27
  questions: {
26
28
  guidance: "Ask one concise, conversational question at a time.",
27
29
  escape: "Not sure yet",
28
30
  },
29
31
  fields: {
30
- priority: Answer.confirmed(Schema.NonEmptyTrimmedString, {
31
- description: "Where the client most needs outside help.",
32
- ask: Question.adaptiveChoice(
33
- "Where could an agency make the biggest difference?",
34
- {
35
- minimumOptions: 2,
36
- maximumOptions: 3,
37
- fallbackOptions: [
38
- "Launch or grow a product",
39
- "Build the brand long term",
40
- "Fix a performance problem",
41
- ],
42
- },
43
- ),
44
- }),
45
- location: Answer.confirmed(Schema.NonEmptyTrimmedString, {
46
- description: "Where the client is based.",
47
- ask: Question.adaptive(
48
- "Ask where the client is based and whether location matters.",
49
- {
50
- fallback: "Where are you based, and does location matter?",
51
- },
52
- ),
53
- }),
32
+ goal: Answer.confirmed(
33
+ Schema.Trimmed.check(Schema.isNonEmpty()),
34
+ {
35
+ description: "The outcome the user wants to achieve.",
36
+ ask: Question.adaptiveChoice(
37
+ "What would you like help accomplishing?",
38
+ {
39
+ minimumOptions: 2,
40
+ maximumOptions: 3,
41
+ fallbackOptions: [
42
+ "Understand a topic",
43
+ "Compare available options",
44
+ "Plan the next steps",
45
+ ],
46
+ },
47
+ ),
48
+ },
49
+ ),
50
+ audience: Answer.confirmed(
51
+ Schema.Trimmed.check(Schema.isNonEmpty()),
52
+ {
53
+ description: "Who the requested result is for.",
54
+ ask: Question.adaptive(
55
+ "Ask who will use the result and what they already know.",
56
+ {
57
+ fallback: "Who is this for, and what do they already know?",
58
+ },
59
+ ),
60
+ },
61
+ ),
54
62
  },
55
63
  })
56
64
 
57
65
  /** A required ask-then-answer workflow before catalog search. */
58
- export const Matchmaker = defineChat({
59
- name: "matchmaker",
66
+ export const GuidedResourceFinder = defineChat({
67
+ name: "guided_resource_finder",
60
68
  version: 1,
61
- stages: [ProjectBrief, Search],
69
+ stages: [RequestDetails, Search],
62
70
  })
63
71
 
64
- const ProjectUnderstanding = Stage.collect({
65
- name: "project_understanding",
72
+ const RequestUnderstanding = Stage.collect({
73
+ name: "request_understanding",
66
74
  fields: {
67
- intent: Answer.semantic(Schema.NonEmptyTrimmedString, {
68
- description:
69
- "The client's searchable need, desired outcome, sector, or relevant experience.",
70
- ask: Question.adaptive(
71
- "Ask one useful follow-up only when the request is not searchable yet.",
72
- {
73
- fallback: "What outcome or experience should I search for?",
74
- },
75
- ),
76
- }),
75
+ intent: Answer.semantic(
76
+ Schema.Trimmed.check(Schema.isNonEmpty()),
77
+ {
78
+ description:
79
+ "The user's searchable goal, subject, constraints, or desired outcome.",
80
+ ask: Question.adaptive(
81
+ "Ask one useful follow-up only when the request is not searchable yet.",
82
+ {
83
+ fallback: "What subject or outcome should I search for?",
84
+ },
85
+ ),
86
+ },
87
+ ),
77
88
  },
78
89
  })
79
90
 
80
91
  /** A chat that can infer a searchable intent from the opening request. */
81
- export const FreeformMatchmaker = defineChat({
82
- name: "freeform_matchmaker",
92
+ export const FreeformResourceFinder = defineChat({
93
+ name: "freeform_resource_finder",
83
94
  version: 1,
84
- stages: [ProjectUnderstanding, Search],
95
+ stages: [RequestUnderstanding, Search],
85
96
  })
86
97
 
87
98
  /** A chat whose catalog search tool is available on the opening turn. */
88
- export const OpenSearchChat = defineChat({
89
- name: "open_search_chat",
99
+ export const OpenResourceSearch = defineChat({
100
+ name: "open_resource_search",
90
101
  version: 1,
91
102
  stages: [Search],
92
103
  })
@@ -21,9 +21,7 @@ export class UnsafeConversation extends Schema.TaggedError<UnsafeConversation>()
21
21
  * Application-owned classifier; production may use any local or remote
22
22
  * policy.
23
23
  */
24
- export class ConversationSafety extends Context.Tag(
25
- "ConversationSafety",
26
- )<
24
+ export class ConversationSafety extends Context.Service<
27
25
  ConversationSafety,
28
26
  {
29
27
  readonly checkConversation: (
@@ -33,34 +31,36 @@ export class ConversationSafety extends Context.Tag(
33
31
  context: ModelGuardCallContext,
34
32
  ) => Effect.Effect<void, UnsafeConversation>
35
33
  }
36
- >() {}
34
+ >()("ConversationSafety") {}
37
35
 
38
36
  /** Effect-native guard composed into any model-backed stage. */
39
37
  export const PromptInjectionPolicy = defineModelGuard({
40
38
  name: "prompt_injection_policy",
41
39
  check: (context) =>
42
- ConversationSafety.pipe(
43
- Effect.flatMap((safety) =>
44
- safety.checkConversation(context),
45
- ),
46
- ),
40
+ Effect.gen(function* () {
41
+ const safety = yield* ConversationSafety
42
+ return yield* safety.checkConversation(context)
43
+ }),
47
44
  checkCall: (context) =>
48
- ConversationSafety.pipe(
49
- Effect.flatMap((safety) => safety.checkCall(context)),
50
- ),
45
+ Effect.gen(function* () {
46
+ const safety = yield* ConversationSafety
47
+ return yield* safety.checkCall(context)
48
+ }),
51
49
  })
52
50
 
53
- const SearchAgencies = defineTool({
54
- name: "search_agencies",
55
- description: "Find agencies relevant to the project brief.",
56
- input: Schema.Struct({ query: Schema.NonEmptyTrimmedString }),
51
+ const FindResources = defineTool({
52
+ name: "find_resources",
53
+ description: "Find resources relevant to the request.",
54
+ input: Schema.Struct({
55
+ query: Schema.Trimmed.check(Schema.isNonEmpty()),
56
+ }),
57
57
  execute: ({ query }) => Effect.succeed({ query }),
58
58
  })
59
59
 
60
60
  /** Closed search stage protected by the application policy. */
61
- export const Matching = Stage.tools({
62
- name: "matching",
63
- instructions: ["Route the completed brief to one search."],
64
- tools: [SearchAgencies],
61
+ export const Lookup = Stage.tools({
62
+ name: "lookup",
63
+ instructions: ["Route the completed request to one resource lookup."],
64
+ tools: [FindResources],
65
65
  guards: [PromptInjectionPolicy],
66
66
  })
@@ -0,0 +1,104 @@
1
+ import {
2
+ Context,
3
+ Effect,
4
+ Layer,
5
+ Schema,
6
+ } from "effect"
7
+ import {
8
+ defineTool,
9
+ defineView,
10
+ Message,
11
+ Stage,
12
+ Tool,
13
+ } from "@popcomputer/structured-chat"
14
+
15
+ interface ResourceMatch {
16
+ readonly id: string
17
+ readonly title: string
18
+ readonly summary: string
19
+ readonly internalScore: number
20
+ readonly retrievedText: string
21
+ }
22
+
23
+ class ResourceCatalog extends Context.Service<
24
+ ResourceCatalog,
25
+ {
26
+ readonly search: (
27
+ query: string,
28
+ ) => Effect.Effect<ReadonlyArray<ResourceMatch>>
29
+ }
30
+ >()("ResourceCatalog") {}
31
+
32
+ const ResultCards = defineView({
33
+ name: "result_cards",
34
+ version: 1,
35
+ schema: Schema.Struct({
36
+ results: Schema.Array(
37
+ Schema.Struct({
38
+ id: Schema.String,
39
+ title: Schema.String,
40
+ summary: Schema.String,
41
+ }),
42
+ ),
43
+ }),
44
+ })
45
+
46
+ const ResourceEvidence = Schema.Struct({
47
+ matches: Schema.Array(
48
+ Schema.Struct({
49
+ id: Schema.String,
50
+ summary: Schema.String,
51
+ }),
52
+ ),
53
+ })
54
+
55
+ export const FindResources = defineTool({
56
+ name: "find_resources",
57
+ description: "Find resources relevant to the request.",
58
+ input: Schema.Struct({
59
+ query: Schema.Trimmed.check(Schema.isNonEmpty()),
60
+ }),
61
+ execute: ({ query }) =>
62
+ Effect.gen(function* () {
63
+ const catalog = yield* ResourceCatalog
64
+ return yield* catalog.search(query)
65
+ }),
66
+ }).pipe(
67
+ Tool.modelResult(ResourceEvidence, (matches) => ({
68
+ matches: matches.map(({ id, summary }) => ({ id, summary })),
69
+ })),
70
+ Tool.present(ResultCards, (matches) => ({
71
+ results: matches.map(({ id, title, summary }) => ({
72
+ id,
73
+ title,
74
+ summary,
75
+ })),
76
+ })),
77
+ )
78
+
79
+ export const Lookup = Stage.tools({
80
+ name: "lookup",
81
+ instructions: ["Route the completed request to one resource lookup."],
82
+ tools: [FindResources],
83
+ })
84
+
85
+ const ResourceCatalogLive = Layer.succeed(ResourceCatalog, {
86
+ search: () =>
87
+ Effect.succeed([
88
+ {
89
+ id: "resource:1",
90
+ title: "Onboarding checklist",
91
+ summary: "A concise sequence of practical onboarding steps.",
92
+ internalScore: 0.93,
93
+ retrievedText: "Untrusted source text remains server-side.",
94
+ },
95
+ ]),
96
+ })
97
+
98
+ export const example = FindResources.execute({
99
+ query: "A practical onboarding guide",
100
+ }).pipe(Effect.provide(ResourceCatalogLive))
101
+
102
+ export const stageExample = Lookup.run([
103
+ Message.user("We need a practical onboarding guide."),
104
+ ])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@popcomputer/structured-chat",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Schema-defined structured chats with typed tools, stages, and UI views",
5
5
  "type": "module",
6
6
  "packageManager": "bun@1.3.1",
@@ -23,6 +23,10 @@
23
23
  "./assistant-ui": {
24
24
  "types": "./dist/integrations/assistant-ui.d.ts",
25
25
  "import": "./dist/integrations/assistant-ui.js"
26
+ },
27
+ "./assistant-ui/debug": {
28
+ "types": "./dist/integrations/assistant-ui-debug.d.ts",
29
+ "import": "./dist/integrations/assistant-ui-debug.js"
26
30
  }
27
31
  },
28
32
  "files": [
@@ -57,13 +61,21 @@
57
61
  ],
58
62
  "author": "Patrick Ogilvie",
59
63
  "license": "MIT",
64
+ "repository": {
65
+ "type": "git",
66
+ "url": "git+https://github.com/PatrickOgilvie/popcomputer-structured-chat.git"
67
+ },
68
+ "homepage": "https://github.com/PatrickOgilvie/popcomputer-structured-chat#readme",
69
+ "bugs": {
70
+ "url": "https://github.com/PatrickOgilvie/popcomputer-structured-chat/issues"
71
+ },
60
72
  "publishConfig": {
61
73
  "access": "public"
62
74
  },
63
75
  "peerDependencies": {
64
76
  "@assistant-ui/core": "^0.3.1",
65
77
  "react": "^18.0.0 || ^19.0.0",
66
- "effect": "^3.21.2"
78
+ "effect": "^4.0.0-rc.109"
67
79
  },
68
80
  "peerDependenciesMeta": {
69
81
  "@assistant-ui/core": {
@@ -80,7 +92,7 @@
80
92
  "@types/bun": "^1.3.14",
81
93
  "@types/react": "^19.2.17",
82
94
  "@types/react-dom": "^19.2.3",
83
- "effect": "3.21.2",
95
+ "effect": "4.0.0-rc.109",
84
96
  "oxlint": "1.78.0",
85
97
  "react": "^19.2.4",
86
98
  "react-dom": "^19.2.4",