From dd919d72978d135122b28bb1b294c5d3533ab5c1 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 8 Jul 2026 20:57:57 -0700 Subject: [PATCH] chore: remove signal resource adapter package --- .github/workflows/ci.yml | 2 +- .github/workflows/publish.yml | 6 +- .github/workflows/release-provenance.yml | 4 +- .../docs/signal-resource/api/api-docs.json | 172 ------------ apps/website/scripts/generate-api-docs.ts | 1 - .../agent/signal-chat-resource-agent.spec.ts | 246 ----------------- .../lib/agent/signal-chat-resource-agent.ts | 157 ----------- libs/signal-resource/README.md | 75 ------ libs/signal-resource/eslint.config.mjs | 53 ---- libs/signal-resource/ng-package.json | 7 - libs/signal-resource/package.json | 28 -- libs/signal-resource/project.json | 57 ---- .../src/lib/provide-agent.spec.ts | 59 ----- libs/signal-resource/src/lib/provide-agent.ts | 73 ------ .../src/lib/provide-agent.type-spec.ts | 32 --- libs/signal-resource/src/lib/to-agent.spec.ts | 247 ------------------ libs/signal-resource/src/lib/to-agent.ts | 177 ------------- libs/signal-resource/src/public-api.ts | 9 - libs/signal-resource/src/test-setup.ts | 11 - .../src/testing/fake-signal-chat-resource.ts | 97 ------- .../src/testing/type-assert.ts | 4 - libs/signal-resource/tsconfig.json | 23 -- libs/signal-resource/tsconfig.lib.json | 18 -- libs/signal-resource/tsconfig.lib.prod.json | 9 - libs/signal-resource/tsconfig.spec.json | 8 - libs/signal-resource/tsconfig.type-tests.json | 18 -- libs/signal-resource/vite.config.mts | 13 - nx.json | 3 +- package-lock.json | 14 - tsconfig.base.json | 1 - 30 files changed, 7 insertions(+), 1617 deletions(-) delete mode 100644 apps/website/content/docs/signal-resource/api/api-docs.json delete mode 100644 libs/chat/src/lib/agent/signal-chat-resource-agent.spec.ts delete mode 100644 libs/chat/src/lib/agent/signal-chat-resource-agent.ts delete mode 100644 libs/signal-resource/README.md delete mode 100644 libs/signal-resource/eslint.config.mjs delete mode 100644 libs/signal-resource/ng-package.json delete mode 100644 libs/signal-resource/package.json delete mode 100644 libs/signal-resource/project.json delete mode 100644 libs/signal-resource/src/lib/provide-agent.spec.ts delete mode 100644 libs/signal-resource/src/lib/provide-agent.ts delete mode 100644 libs/signal-resource/src/lib/provide-agent.type-spec.ts delete mode 100644 libs/signal-resource/src/lib/to-agent.spec.ts delete mode 100644 libs/signal-resource/src/lib/to-agent.ts delete mode 100644 libs/signal-resource/src/public-api.ts delete mode 100644 libs/signal-resource/src/test-setup.ts delete mode 100644 libs/signal-resource/src/testing/fake-signal-chat-resource.ts delete mode 100644 libs/signal-resource/src/testing/type-assert.ts delete mode 100644 libs/signal-resource/tsconfig.json delete mode 100644 libs/signal-resource/tsconfig.lib.json delete mode 100644 libs/signal-resource/tsconfig.lib.prod.json delete mode 100644 libs/signal-resource/tsconfig.spec.json delete mode 100644 libs/signal-resource/tsconfig.type-tests.json delete mode 100644 libs/signal-resource/vite.config.mts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee7bdc379..e75e466da 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,7 +82,7 @@ jobs: if: github.event_name == 'push' || needs.ci-scope.outputs.library == 'true' runs-on: ubuntu-latest env: - LIBS: chat,langgraph,ag-ui,render,a2ui,licensing,signal-resource,telemetry + LIBS: chat,langgraph,ag-ui,render,a2ui,licensing,telemetry steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9ce10902c..5567d0d02 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -28,7 +28,7 @@ jobs: contents: read id-token: write # required for npm trusted publishing + provenance env: - NPM_PUBLISHABLE_PROJECTS: chat,langgraph,ag-ui,render,a2ui,licensing,signal-resource,telemetry + NPM_PUBLISHABLE_PROJECTS: chat,langgraph,ag-ui,render,a2ui,licensing,telemetry steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # Node 24 ships npm 11+ which fully implements npm trusted publishing @@ -57,10 +57,10 @@ jobs: run: npx nx run-many -t lint,test,build --projects=$NPM_PUBLISHABLE_PROJECTS --skip-nx-cache - name: Patch install telemetry into publishable manifests - run: node libs/telemetry/scripts/apply-install-telemetry.mjs dist/libs/chat dist/libs/langgraph dist/libs/ag-ui dist/libs/render dist/libs/a2ui dist/libs/licensing dist/libs/signal-resource + run: node libs/telemetry/scripts/apply-install-telemetry.mjs dist/libs/chat dist/libs/langgraph dist/libs/ag-ui dist/libs/render dist/libs/a2ui dist/libs/licensing - name: Verify install telemetry in publishable manifests - run: node libs/telemetry/scripts/verify-install-telemetry.mjs dist/libs/chat dist/libs/langgraph dist/libs/ag-ui dist/libs/render dist/libs/a2ui dist/libs/licensing dist/libs/signal-resource dist/libs/telemetry + run: node libs/telemetry/scripts/verify-install-telemetry.mjs dist/libs/chat dist/libs/langgraph dist/libs/ag-ui dist/libs/render dist/libs/a2ui dist/libs/licensing dist/libs/telemetry - name: Verify atomic release versions run: node scripts/verify-release-versions.mjs --tag "$RELEASE_TAG" diff --git a/.github/workflows/release-provenance.yml b/.github/workflows/release-provenance.yml index 4513eb021..3cce243c7 100644 --- a/.github/workflows/release-provenance.yml +++ b/.github/workflows/release-provenance.yml @@ -33,7 +33,7 @@ jobs: hashes: ${{ steps.hash.outputs.hashes }} tag: ${{ steps.tag.outputs.tag }} env: - NPM_PUBLISHABLE_PROJECTS: chat,langgraph,ag-ui,render,a2ui,licensing,signal-resource,telemetry + NPM_PUBLISHABLE_PROJECTS: chat,langgraph,ag-ui,render,a2ui,licensing,telemetry steps: - name: Resolve target tag id: tag @@ -53,7 +53,7 @@ jobs: - name: Pack tarballs run: | mkdir -p release-artifacts - for p in chat langgraph ag-ui render a2ui licensing signal-resource telemetry; do + for p in chat langgraph ag-ui render a2ui licensing telemetry; do npm pack "dist/libs/$p" --pack-destination release-artifacts done - name: Generate subject hashes diff --git a/apps/website/content/docs/signal-resource/api/api-docs.json b/apps/website/content/docs/signal-resource/api/api-docs.json deleted file mode 100644 index 035d11c83..000000000 --- a/apps/website/content/docs/signal-resource/api/api-docs.json +++ /dev/null @@ -1,172 +0,0 @@ -[ - { - "name": "SignalChatResourceLike", - "kind": "interface", - "description": "Minimal structural contract required to adapt a signal-backed chat resource.", - "properties": [ - { - "name": "error", - "type": "Signal", - "description": "Current resource error, if any.", - "optional": false - }, - { - "name": "isLoading", - "type": "Signal", - "description": "Whether the resource is currently producing a response.", - "optional": false - }, - { - "name": "reload", - "type": "() => boolean", - "description": "Reload the resource, used as a fallback for `Agent.regenerate()`.", - "optional": true - }, - { - "name": "resendMessages", - "type": "() => void", - "description": "Re-run against the current history, if supported by the resource.", - "optional": true - }, - { - "name": "sendMessage", - "type": "(message: object) => void", - "description": "Send a new user message.", - "optional": false - }, - { - "name": "setMessages", - "type": "(messages: SignalChatResourceMessage[]) => void", - "description": "Replace resource message history. Used by `Agent.regenerate()`.", - "optional": false - }, - { - "name": "stop", - "type": "() => void", - "description": "Stop the current response. Called only while `isLoading()` is true.", - "optional": false - }, - { - "name": "value", - "type": "Signal", - "description": "Current resource message history.", - "optional": false - } - ], - "examples": [] - }, - { - "name": "SignalChatResourceToolCall", - "kind": "interface", - "description": "Tool-call shape exposed by a signal-backed chat resource.", - "properties": [ - { - "name": "args", - "type": "unknown", - "description": "Parsed tool arguments.", - "optional": false - }, - { - "name": "name", - "type": "string", - "description": "Tool name shown in Threadplane tool-call UI.", - "optional": false - }, - { - "name": "result", - "type": "PromiseSettledResult", - "description": "Settled tool result, when the resource exposes one.", - "optional": true - }, - { - "name": "status", - "type": "\"pending\" | \"done\"", - "description": "Resource-specific lifecycle state for the tool call.", - "optional": false - }, - { - "name": "toolCallId", - "type": "string", - "description": "Stable identifier for linking assistant messages to tool-call details.", - "optional": false - } - ], - "examples": [] - }, - { - "name": "SignalChatResourceMessage", - "kind": "type", - "description": "Message shape consumed by the signal-resource adapter.", - "signature": "object | object | object", - "examples": [] - }, - { - "name": "SignalResourceAgentSource", - "kind": "type", - "description": "Resource instance or factory accepted by `provideAgent()`.", - "signature": "SignalChatResourceLike | () => SignalChatResourceLike", - "examples": [] - }, - { - "name": "injectAgent", - "kind": "function", - "description": "Injects the signal-resource-backed Agent registered by provideAgent().", - "signature": "injectAgent(): Agent<>", - "params": [], - "returns": { - "type": "Agent<>", - "description": "" - }, - "examples": [ - "```ts\nimport { injectAgent } from '@threadplane/signal-resource';\n\nconst agent = injectAgent();\n```" - ] - }, - { - "name": "provideAgent", - "kind": "function", - "description": "Provides a signal-resource-backed Agent through Angular dependency injection.", - "signature": "provideAgent(ref: AgentRef, sourceOrFactory: SignalResourceAgentSource): Provider[]", - "params": [ - { - "name": "ref", - "type": "AgentRef", - "description": "", - "optional": false - }, - { - "name": "sourceOrFactory", - "type": "SignalResourceAgentSource", - "description": "", - "optional": false - } - ], - "returns": { - "type": "Provider[]", - "description": "" - }, - "examples": [ - "```ts\nimport { provideAgent } from '@threadplane/signal-resource';\n\nbootstrapApplication(AppComponent, {\n providers: [provideAgent(() => resource)],\n});\n```" - ] - }, - { - "name": "toAgent", - "kind": "function", - "description": "Wrap a signal-backed chat resource in the runtime-neutral Agent contract.", - "signature": "toAgent(resource: SignalChatResourceLike): Agent<>", - "params": [ - { - "name": "resource", - "type": "SignalChatResourceLike", - "description": "", - "optional": false - } - ], - "returns": { - "type": "Agent<>", - "description": "" - }, - "examples": [ - "```ts\nimport { toAgent } from '@threadplane/signal-resource';\n\nconst agent = toAgent(resource);\n```" - ] - } -] \ No newline at end of file diff --git a/apps/website/scripts/generate-api-docs.ts b/apps/website/scripts/generate-api-docs.ts index 047085319..9a9e0506a 100644 --- a/apps/website/scripts/generate-api-docs.ts +++ b/apps/website/scripts/generate-api-docs.ts @@ -182,7 +182,6 @@ const LIBRARIES: LibraryEntryConfig[] = [ { docSlug: 'chat', entryPoints: ['libs/chat/src/public-api.ts', 'libs/chat/testing/public-api.ts'] }, { docSlug: 'render', entryPoints: ['libs/render/src/public-api.ts'] }, { docSlug: 'ag-ui', entryPoints: ['libs/ag-ui/src/public-api.ts'] }, - { docSlug: 'signal-resource', entryPoints: ['libs/signal-resource/src/public-api.ts'] }, { docSlug: 'a2ui', entryPoints: ['libs/a2ui/src/index.ts'] }, { docSlug: 'middleware', entryPoints: ['libs/middleware/src/langgraph/index.ts'] }, { docSlug: 'licensing', entryPoints: ['libs/licensing/src/index.ts'] }, diff --git a/libs/chat/src/lib/agent/signal-chat-resource-agent.spec.ts b/libs/chat/src/lib/agent/signal-chat-resource-agent.spec.ts deleted file mode 100644 index d240c8b8b..000000000 --- a/libs/chat/src/lib/agent/signal-chat-resource-agent.spec.ts +++ /dev/null @@ -1,246 +0,0 @@ -// SPDX-License-Identifier: MIT -import { signal, type Signal, type WritableSignal } from '@angular/core'; -import { describe, expect, it } from 'vitest'; -import { runAgentConformance } from '@threadplane/chat/testing'; -import { toSignalChatResourceAgent } from './signal-chat-resource-agent'; - -interface ResourceToolCall { - status: 'pending' | 'done'; - name: string; - toolCallId: string; - args: unknown; - result?: PromiseSettledResult; -} - -type ResourceMessage = - | { role: 'user'; content: unknown } - | { role: 'assistant'; content?: unknown; toolCalls: ResourceToolCall[] } - | { role: 'error'; content: string }; - -interface FakeSignalChatResource { - value: Signal; - isLoading: Signal; - error: Signal; - sendMessage: (message: { role: 'user'; content: unknown }) => void; - stop: () => void; - setMessages: (messages: ResourceMessage[]) => void; - resendMessages?: () => void; -} - -function makeResource(options: { - messages?: ResourceMessage[]; - isLoading?: boolean; - error?: Error; - withResend?: boolean; -} = {}) { - const messages = signal(options.messages ?? []); - const isLoading = signal(options.isLoading ?? false); - const error = signal(options.error); - const sent: Array<{ role: 'user'; content: unknown }> = []; - const setMessagesCalls: ResourceMessage[][] = []; - let stopCount = 0; - let resendCount = 0; - - const resource: FakeSignalChatResource = { - value: messages.asReadonly(), - isLoading: isLoading.asReadonly(), - error: error.asReadonly(), - sendMessage: (message) => { - sent.push(message); - messages.update((prev) => [...prev, message]); - }, - stop: () => { - if (!isLoading()) throw new Error('Cannot stop when idle'); - stopCount++; - isLoading.set(false); - }, - setMessages: (next) => { - setMessagesCalls.push(next); - messages.set(next); - }, - ...(options.withResend - ? { - resendMessages: () => { - resendCount++; - }, - } - : {}), - }; - - return { - resource, - messages: messages as WritableSignal, - isLoading, - error, - sent, - setMessagesCalls, - get stopCount() { - return stopCount; - }, - get resendCount() { - return resendCount; - }, - }; -} - -describe('toSignalChatResourceAgent', () => { - runAgentConformance('toSignalChatResourceAgent', () => { - return toSignalChatResourceAgent(makeResource().resource); - }); - - it('maps resource messages and tool calls to the Agent contract', () => { - const fixture = makeResource({ - messages: [ - { role: 'user', content: 'hello' }, - { - role: 'assistant', - content: 'checking', - toolCalls: [ - { - status: 'pending', - name: 'lookup', - toolCallId: 'tool-1', - args: { city: 'Paris' }, - }, - { - status: 'done', - name: 'weather', - toolCallId: 'tool-2', - args: { city: 'Paris' }, - result: { status: 'fulfilled', value: { temp: 72 } }, - }, - { - status: 'done', - name: 'alerts', - toolCallId: 'tool-3', - args: { city: 'Paris' }, - result: { status: 'rejected', reason: new Error('offline') }, - }, - ], - }, - { role: 'error', content: 'model failed' }, - ], - }); - - const agent = toSignalChatResourceAgent(fixture.resource); - - expect(agent.messages()).toEqual([ - { id: 'resource-message-0', role: 'user', content: 'hello' }, - { - id: 'resource-message-1', - role: 'assistant', - content: 'checking', - toolCallIds: ['tool-1', 'tool-2', 'tool-3'], - }, - { - id: 'resource-message-2', - role: 'assistant', - content: 'model failed', - }, - ]); - expect(agent.toolCalls()).toEqual([ - { - id: 'tool-1', - name: 'lookup', - args: { city: 'Paris' }, - status: 'running', - }, - { - id: 'tool-2', - name: 'weather', - args: { city: 'Paris' }, - status: 'complete', - result: { temp: 72 }, - }, - { - id: 'tool-3', - name: 'alerts', - args: { city: 'Paris' }, - status: 'error', - error: expect.any(Error), - }, - ]); - }); - - it('derives status from loading and error signals', () => { - const fixture = makeResource(); - const agent = toSignalChatResourceAgent(fixture.resource); - - expect(agent.status()).toBe('idle'); - fixture.isLoading.set(true); - expect(agent.status()).toBe('running'); - fixture.isLoading.set(false); - fixture.error.set(new Error('HTTP 500')); - expect(agent.status()).toBe('error'); - expect(agent.error()?.status).toBe(500); - }); - - it('submits string user messages to the resource', async () => { - const fixture = makeResource(); - const agent = toSignalChatResourceAgent(fixture.resource); - - await agent.submit({ message: 'hello' }); - - expect(fixture.sent).toEqual([{ role: 'user', content: 'hello' }]); - }); - - it('does not call resource stop while idle', async () => { - const fixture = makeResource(); - const agent = toSignalChatResourceAgent(fixture.resource); - - await expect(agent.stop()).resolves.toBeUndefined(); - - expect(fixture.stopCount).toBe(0); - }); - - it('delegates stop while loading', async () => { - const fixture = makeResource({ isLoading: true }); - const agent = toSignalChatResourceAgent(fixture.resource); - - await agent.stop(); - - expect(fixture.stopCount).toBe(1); - }); - - it('delegates retry when the resource supports resend', async () => { - const fixture = makeResource({ withResend: true }); - const agent = toSignalChatResourceAgent(fixture.resource); - - await agent.retry(); - - expect(fixture.resendCount).toBe(1); - }); - - it('trims through the previous user message and resends on regenerate', async () => { - const messages: ResourceMessage[] = [ - { role: 'user', content: 'first' }, - { role: 'assistant', content: 'first answer', toolCalls: [] }, - { role: 'user', content: 'second' }, - { role: 'assistant', content: 'second answer', toolCalls: [] }, - ]; - const fixture = makeResource({ messages, withResend: true }); - const agent = toSignalChatResourceAgent(fixture.resource); - - await agent.regenerate(3); - - expect(fixture.setMessagesCalls).toEqual([ - [ - { role: 'user', content: 'first' }, - { role: 'assistant', content: 'first answer', toolCalls: [] }, - { role: 'user', content: 'second' }, - ], - ]); - expect(fixture.resendCount).toBe(1); - }); - - it('throws when regenerating a non-assistant message', async () => { - const fixture = makeResource({ - messages: [{ role: 'user', content: 'hello' }], - }); - const agent = toSignalChatResourceAgent(fixture.resource); - - await expect(agent.regenerate(0)).rejects.toThrow( - 'Message at index 0 is not an assistant message', - ); - }); -}); diff --git a/libs/chat/src/lib/agent/signal-chat-resource-agent.ts b/libs/chat/src/lib/agent/signal-chat-resource-agent.ts deleted file mode 100644 index 54ef28d86..000000000 --- a/libs/chat/src/lib/agent/signal-chat-resource-agent.ts +++ /dev/null @@ -1,157 +0,0 @@ -// SPDX-License-Identifier: MIT -import { computed, signal, type Signal } from '@angular/core'; -import { EMPTY } from 'rxjs'; -import type { Agent } from './agent'; -import type { Message } from './message'; -import type { ToolCall } from './tool-call'; -import type { AgentStatus } from './agent-status'; -import type { AgentSubmitInput } from './agent-submit'; -import { toAgentError } from './to-agent-error'; - -interface SignalChatResourceToolCall { - status: 'pending' | 'done'; - name: string; - toolCallId: string; - args: unknown; - result?: PromiseSettledResult; -} - -type SignalChatResourceMessage = - | { role: 'user'; content: unknown } - | { role: 'assistant'; content?: unknown; toolCalls: SignalChatResourceToolCall[] } - | { role: 'error'; content: string }; - -interface SignalChatResourceLike { - value: Signal; - isLoading: Signal; - error: Signal; - sendMessage: (message: { role: 'user'; content: unknown }) => void; - stop: () => void; - setMessages: (messages: SignalChatResourceMessage[]) => void; - resendMessages?: () => void; - reload?: () => boolean; -} - -/** - * Wrap a signal-backed chat resource in the runtime-neutral Agent contract. - */ -export function toSignalChatResourceAgent(resource: SignalChatResourceLike): Agent { - const messages = computed(() => { - return resource.value().map((message, index) => toAgentMessage(message, index)); - }); - - const toolCalls = computed(() => { - return resource.value().flatMap((message) => { - if (message.role !== 'assistant') return []; - return message.toolCalls.map(toAgentToolCall); - }); - }); - - const isLoading = computed(() => resource.isLoading()); - const error = computed(() => { - const raw = resource.error(); - return raw ? toAgentError(raw) : undefined; - }); - const status = computed(() => { - if (isLoading()) return 'running'; - if (error()) return 'error'; - return 'idle'; - }); - - return { - messages, - status, - isLoading, - error, - toolCalls, - state: signal>({}).asReadonly(), - events$: EMPTY, - submit: async (input: AgentSubmitInput) => { - if (input.message === undefined) return; - resource.sendMessage({ role: 'user', content: input.message }); - }, - stop: async () => { - if (!resource.isLoading()) return; - resource.stop(); - }, - retry: async () => { - resource.resendMessages?.(); - }, - regenerate: async (assistantMessageIndex: number) => { - if (resource.isLoading()) { - throw new Error('Cannot regenerate while agent is loading another response'); - } - const current = resource.value(); - const target = current[assistantMessageIndex]; - if (!target || target.role !== 'assistant') { - throw new Error(`Message at index ${assistantMessageIndex} is not an assistant message`); - } - const userIdx = current - .slice(0, assistantMessageIndex) - .map((m, i) => ({ m, i })) - .reverse() - .find(({ m }) => m.role === 'user')?.i; - if (userIdx === undefined) { - throw new Error('No user message found before the target assistant message'); - } - resource.setMessages(current.slice(0, userIdx + 1)); - if (resource.resendMessages) { - resource.resendMessages(); - } else { - resource.reload?.(); - } - }, - }; -} - -function toAgentMessage(message: SignalChatResourceMessage, index: number): Message { - const id = `resource-message-${index}`; - switch (message.role) { - case 'user': - return { id, role: 'user', content: normalizeContent(message.content) }; - case 'assistant': - return { - id, - role: 'assistant', - content: normalizeContent(message.content ?? ''), - toolCallIds: message.toolCalls.map((toolCall) => toolCall.toolCallId), - }; - case 'error': - return { id, role: 'assistant', content: message.content }; - } -} - -function toAgentToolCall(toolCall: SignalChatResourceToolCall): ToolCall { - if (toolCall.status === 'pending') { - return { - id: toolCall.toolCallId, - name: toolCall.name, - args: toolCall.args, - status: 'running', - }; - } - - if (toolCall.result?.status === 'rejected') { - return { - id: toolCall.toolCallId, - name: toolCall.name, - args: toolCall.args, - status: 'error', - error: toolCall.result.reason, - }; - } - - return { - id: toolCall.toolCallId, - name: toolCall.name, - args: toolCall.args, - status: 'complete', - result: toolCall.result?.value, - }; -} - -function normalizeContent(content: unknown): Message['content'] { - if (typeof content === 'string') return content; - if (content === undefined) return ''; - return JSON.stringify(content); -} diff --git a/libs/signal-resource/README.md b/libs/signal-resource/README.md deleted file mode 100644 index e8b8bd449..000000000 --- a/libs/signal-resource/README.md +++ /dev/null @@ -1,75 +0,0 @@ -# @threadplane/signal-resource - -Signal-backed chat resource adapter for `@threadplane/chat`. - -Use this package when your chat runtime already exposes Angular signals for -message history, loading state, errors, and send/stop/retry methods. The adapter -wraps that structural resource shape in Threadplane's runtime-neutral `Agent` -contract, so the same `` UI can render it. - -## Install - -```bash -npm install @threadplane/chat @threadplane/signal-resource -``` - -## Provide An Agent - -```ts -import { bootstrapApplication } from '@angular/platform-browser'; -import { provideAgent } from '@threadplane/signal-resource'; -import { AppComponent } from './app.component'; -import { createChatResource } from './chat-resource'; - -bootstrapApplication(AppComponent, { - providers: [ - provideAgent(() => { - const resource = createChatResource(); - return resource; - }), - ], -}); -``` - -## Use With Chat - -```ts -import { Component } from '@angular/core'; -import { ChatComponent } from '@threadplane/chat'; -import { injectAgent } from '@threadplane/signal-resource'; - -@Component({ - selector: 'app-root', - imports: [ChatComponent], - template: ``, -}) -export class AppComponent { - readonly agent = injectAgent(); -} -``` - -## Resource Shape - -The resource is structural. It does not need to inherit from a Threadplane base -class. - -```ts -import type { Signal } from '@angular/core'; -import type { - SignalChatResourceMessage, -} from '@threadplane/signal-resource'; - -interface ChatResource { - value: Signal; - isLoading: Signal; - error: Signal; - sendMessage(message: { role: 'user'; content: unknown }): void; - stop(): void; - setMessages(messages: SignalChatResourceMessage[]): void; - resendMessages?(): void; - reload?(): boolean; -} -``` - -The adapter projects resource tool-call state into `Agent.toolCalls`, but it -does not take over provider-owned tool execution or result submission. diff --git a/libs/signal-resource/eslint.config.mjs b/libs/signal-resource/eslint.config.mjs deleted file mode 100644 index 7ba8e81ef..000000000 --- a/libs/signal-resource/eslint.config.mjs +++ /dev/null @@ -1,53 +0,0 @@ -import nx from '@nx/eslint-plugin'; -import baseConfig from '../../eslint.config.mjs'; - -export default [ - ...baseConfig, - { - files: ['**/*.json'], - rules: { - '@nx/dependency-checks': [ - 'error', - { - ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}'], - ignoredDependencies: [ - 'vite', - '@nx/vite', - 'vitest', - 'rxjs', - ], - }, - ], - }, - languageOptions: { - parser: await import('jsonc-eslint-parser'), - }, - }, - ...nx.configs['flat/angular'], - ...nx.configs['flat/angular-template'], - { - files: ['**/*.ts'], - rules: { - '@angular-eslint/directive-selector': [ - 'error', - { - type: 'attribute', - prefix: ['signalResource'], - style: 'camelCase', - }, - ], - '@angular-eslint/component-selector': [ - 'error', - { - type: 'element', - prefix: ['signal-resource'], - style: 'kebab-case', - }, - ], - }, - }, - { - files: ['**/*.html'], - rules: {}, - }, -]; diff --git a/libs/signal-resource/ng-package.json b/libs/signal-resource/ng-package.json deleted file mode 100644 index 82c883a53..000000000 --- a/libs/signal-resource/ng-package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", - "dest": "../../dist/libs/signal-resource", - "lib": { - "entryFile": "src/public-api.ts" - } -} diff --git a/libs/signal-resource/package.json b/libs/signal-resource/package.json deleted file mode 100644 index 8892dee01..000000000 --- a/libs/signal-resource/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "@threadplane/signal-resource", - "version": "0.0.56", - "description": "Signal-backed chat resource adapter for @threadplane/chat.", - "keywords": [ - "angular", - "signals", - "agent", - "adapter", - "threadplane" - ], - "peerDependencies": { - "@threadplane/chat": "*", - "@angular/core": "^20.0.0 || ^21.0.0", - "rxjs": "~7.8.0" - }, - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/cacheplane/angular-agent-framework.git", - "directory": "libs/signal-resource" - }, - "homepage": "https://github.com/cacheplane/angular-agent-framework#readme", - "bugs": { - "url": "https://github.com/cacheplane/angular-agent-framework/issues" - }, - "sideEffects": false -} diff --git a/libs/signal-resource/project.json b/libs/signal-resource/project.json deleted file mode 100644 index 84783c052..000000000 --- a/libs/signal-resource/project.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "name": "signal-resource", - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "libs/signal-resource/src", - "prefix": "signal-resource", - "projectType": "library", - "tags": [ - "scope:cockpit", - "scope:cockpit-deploy-smoke", - "scope:cockpit-e2e", - "scope:cockpit-examples", - "scope:cockpit-smoke", - "scope:examples-chat", - "scope:library", - "scope:website", - "scope:website-e2e" - ], - "targets": { - "build": { - "executor": "@nx/angular:package", - "outputs": [ - "{workspaceRoot}/dist/{projectRoot}" - ], - "options": { - "project": "libs/signal-resource/ng-package.json", - "tsConfig": "libs/signal-resource/tsconfig.lib.json" - }, - "configurations": { - "production": { - "tsConfig": "libs/signal-resource/tsconfig.lib.prod.json" - }, - "development": {} - }, - "defaultConfiguration": "production" - }, - "nx-release-publish": { - "options": { - "packageRoot": "dist/{projectRoot}" - } - }, - "lint": { - "executor": "@nx/eslint:lint" - }, - "test": { - "executor": "@nx/vitest:test", - "options": { - "configFile": "libs/signal-resource/vite.config.mts" - } - }, - "type-tests": { - "executor": "nx:run-commands", - "options": { - "command": "npx tsc --noEmit -p libs/signal-resource/tsconfig.type-tests.json" - } - } - } -} diff --git a/libs/signal-resource/src/lib/provide-agent.spec.ts b/libs/signal-resource/src/lib/provide-agent.spec.ts deleted file mode 100644 index fd64337c3..000000000 --- a/libs/signal-resource/src/lib/provide-agent.spec.ts +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-License-Identifier: MIT -import { inject, InjectionToken } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; -import { createAgentRef } from '@threadplane/chat'; -import { describe, expect, it } from 'vitest'; -import { makeFakeSignalChatResource } from '../testing/fake-signal-chat-resource'; -import { AGENT, injectAgent, provideAgent } from './provide-agent'; - -describe('provideAgent', () => { - it('registers an injectable Agent from a resource', () => { - const fixture = makeFakeSignalChatResource(); - TestBed.configureTestingModule({ - providers: [provideAgent(fixture.resource)], - }); - - const agent = TestBed.runInInjectionContext(() => injectAgent()); - - expect(agent).toBe(TestBed.inject(AGENT)); - expect(agent.messages()).toEqual([]); - }); - - it('resolves resource factories inside an injection context', () => { - const fixture = makeFakeSignalChatResource(); - const RESOURCE = new InjectionToken('RESOURCE', { - factory: () => fixture.resource, - }); - let calls = 0; - TestBed.configureTestingModule({ - providers: [ - { provide: RESOURCE, useValue: fixture.resource }, - provideAgent(() => { - calls++; - return inject(RESOURCE); - }), - ], - }); - - const agent = TestBed.runInInjectionContext(() => injectAgent()); - - expect(agent).toBeDefined(); - expect(calls).toBe(1); - }); - - it('binds an AgentRef token when provided', () => { - interface TripState { - day: number; - } - const TRIP = createAgentRef('trip'); - const fixture = makeFakeSignalChatResource(); - TestBed.configureTestingModule({ - providers: [provideAgent(TRIP, fixture.resource)], - }); - - const defaultAgent = TestBed.runInInjectionContext(() => injectAgent()); - const typedAgent = TestBed.runInInjectionContext(() => injectAgent(TRIP)); - - expect(typedAgent).toBe(defaultAgent); - }); -}); diff --git a/libs/signal-resource/src/lib/provide-agent.ts b/libs/signal-resource/src/lib/provide-agent.ts deleted file mode 100644 index f310b7e47..000000000 --- a/libs/signal-resource/src/lib/provide-agent.ts +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-License-Identifier: MIT -import { InjectionToken, inject, type Provider } from '@angular/core'; -import type { Agent, AgentRef } from '@threadplane/chat'; -import { toAgent, type SignalChatResourceLike } from './to-agent'; - -/** Resource instance or factory accepted by `provideAgent()`. */ -export type SignalResourceAgentSource = - | SignalChatResourceLike - | (() => SignalChatResourceLike); - -/** - * @internal — exported for spec access only. Consumers should use injectAgent(). - */ -export const AGENT = new InjectionToken('SIGNAL_RESOURCE_AGENT'); - -function resolveSource(sourceOrFactory: SignalResourceAgentSource): SignalChatResourceLike { - return typeof sourceOrFactory === 'function' ? sourceOrFactory() : sourceOrFactory; -} - -function buildAgent(sourceOrFactory: SignalResourceAgentSource): Agent { - return toAgent(resolveSource(sourceOrFactory)); -} - -function isAgentRef(x: unknown): x is AgentRef { - return typeof x === 'object' && x !== null && 'token' in x; -} - -/** - * Provides a signal-resource-backed Agent through Angular dependency injection. - * - * @example - * ```ts - * import { provideAgent } from '@threadplane/signal-resource'; - * - * bootstrapApplication(AppComponent, { - * providers: [provideAgent(() => resource)], - * }); - * ``` - */ -export function provideAgent>( - ref: AgentRef, - sourceOrFactory: SignalResourceAgentSource, -): Provider[]; -export function provideAgent(sourceOrFactory: SignalResourceAgentSource): Provider[]; -export function provideAgent>( - refOrSource: AgentRef | SignalResourceAgentSource, - maybeSource?: SignalResourceAgentSource, -): Provider[] { - const ref = isAgentRef(refOrSource) ? refOrSource : undefined; - const sourceOrFactory = (ref ? maybeSource : refOrSource) as SignalResourceAgentSource; - - const providers: Provider[] = [ - { provide: AGENT, useFactory: () => buildAgent(sourceOrFactory) }, - ]; - if (ref) providers.push({ provide: ref.token, useExisting: AGENT }); - return providers; -} - -/** - * Injects the signal-resource-backed Agent registered by provideAgent(). - * - * @example - * ```ts - * import { injectAgent } from '@threadplane/signal-resource'; - * - * const agent = injectAgent(); - * ``` - */ -export function injectAgent(): Agent; -export function injectAgent(ref: AgentRef): Agent; -export function injectAgent(ref?: AgentRef): Agent { - return inject(ref ? ref.token : AGENT) as Agent; -} diff --git a/libs/signal-resource/src/lib/provide-agent.type-spec.ts b/libs/signal-resource/src/lib/provide-agent.type-spec.ts deleted file mode 100644 index 950e555ca..000000000 --- a/libs/signal-resource/src/lib/provide-agent.type-spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: MIT -import type { Agent } from '@threadplane/chat'; -import { createAgentRef } from '@threadplane/chat'; -import { makeFakeSignalChatResource } from '../testing/fake-signal-chat-resource'; -import type { Equal, Expect } from '../testing/type-assert'; -import { injectAgent, provideAgent } from './provide-agent'; - -interface TripState { - day: number; - places: string[]; -} - -const TRIP = createAgentRef('trip'); -const fixture = makeFakeSignalChatResource(); - -declare function ctx(fn: () => T): T; - -const typed = ctx(() => injectAgent(TRIP)); -export type TypedAgentCheck = Expect>>; -export type TypedStateCheck = Expect, TripState>>; - -const plain = ctx(() => injectAgent()); -export type PlainAgentCheck = Expect>>>; -export type PlainStateCheck = Expect, Record>>; - -void typed; -void plain; - -provideAgent(TRIP, fixture.resource); -provideAgent(TRIP, () => fixture.resource); -provideAgent(fixture.resource); -provideAgent(() => fixture.resource); diff --git a/libs/signal-resource/src/lib/to-agent.spec.ts b/libs/signal-resource/src/lib/to-agent.spec.ts deleted file mode 100644 index 4825fbb8e..000000000 --- a/libs/signal-resource/src/lib/to-agent.spec.ts +++ /dev/null @@ -1,247 +0,0 @@ -// SPDX-License-Identifier: MIT -import { describe, expect, it } from 'vitest'; -import { runAgentConformance } from '@threadplane/chat/testing'; -import { toAgent, type SignalChatResourceMessage } from './to-agent'; -import { makeFakeSignalChatResource } from '../testing/fake-signal-chat-resource'; - -describe('toAgent', () => { - runAgentConformance('signal-resource toAgent', () => { - return toAgent(makeFakeSignalChatResource().resource); - }); - - it('maps resource messages and tool calls to the Agent contract', () => { - const fixture = makeFakeSignalChatResource({ - messages: [ - { role: 'user', content: 'hello' }, - { - role: 'assistant', - content: 'checking', - toolCalls: [ - { - status: 'pending', - name: 'lookup', - toolCallId: 'tool-1', - args: { city: 'Paris' }, - }, - { - status: 'done', - name: 'weather', - toolCallId: 'tool-2', - args: { city: 'Paris' }, - result: { status: 'fulfilled', value: { temp: 72 } }, - }, - { - status: 'done', - name: 'alerts', - toolCallId: 'tool-3', - args: { city: 'Paris' }, - result: { status: 'rejected', reason: new Error('offline') }, - }, - ], - }, - { role: 'error', content: 'model failed' }, - ], - }); - - const agent = toAgent(fixture.resource); - - expect(agent.messages()).toEqual([ - { id: 'resource-message-0', role: 'user', content: 'hello' }, - { - id: 'resource-message-1', - role: 'assistant', - content: 'checking', - toolCallIds: ['tool-1', 'tool-2', 'tool-3'], - }, - { - id: 'resource-message-2', - role: 'assistant', - content: 'model failed', - }, - ]); - expect(agent.toolCalls()).toEqual([ - { - id: 'tool-1', - name: 'lookup', - args: { city: 'Paris' }, - status: 'running', - }, - { - id: 'tool-2', - name: 'weather', - args: { city: 'Paris' }, - status: 'complete', - result: { temp: 72 }, - }, - { - id: 'tool-3', - name: 'alerts', - args: { city: 'Paris' }, - status: 'error', - error: expect.any(Error), - }, - ]); - }); - - it('derives status from loading and error signals', () => { - const fixture = makeFakeSignalChatResource(); - const agent = toAgent(fixture.resource); - - expect(agent.status()).toBe('idle'); - fixture.isLoading.set(true); - expect(agent.status()).toBe('running'); - fixture.isLoading.set(false); - fixture.error.set(new Error('HTTP 500')); - expect(agent.status()).toBe('error'); - expect(agent.error()?.status).toBe(500); - }); - - it('submits string user messages to the resource', async () => { - const fixture = makeFakeSignalChatResource(); - const agent = toAgent(fixture.resource); - - await agent.submit({ message: 'hello' }); - - expect(fixture.sent).toEqual([{ role: 'user', content: 'hello' }]); - }); - - it('does not submit when message is undefined', async () => { - const fixture = makeFakeSignalChatResource(); - const agent = toAgent(fixture.resource); - - await agent.submit({}); - - expect(fixture.sent).toEqual([]); - }); - - it('does not call resource stop while idle', async () => { - const fixture = makeFakeSignalChatResource(); - const agent = toAgent(fixture.resource); - - await expect(agent.stop()).resolves.toBeUndefined(); - - expect(fixture.stopCount).toBe(0); - }); - - it('delegates stop while loading', async () => { - const fixture = makeFakeSignalChatResource({ isLoading: true }); - const agent = toAgent(fixture.resource); - - await agent.stop(); - - expect(fixture.stopCount).toBe(1); - }); - - it('delegates retry when the resource supports resend', async () => { - const fixture = makeFakeSignalChatResource({ withResend: true }); - const agent = toAgent(fixture.resource); - - await agent.retry(); - - expect(fixture.resendCount).toBe(1); - }); - - it('does not throw on retry when resend is unavailable', async () => { - const fixture = makeFakeSignalChatResource(); - const agent = toAgent(fixture.resource); - - await expect(agent.retry()).resolves.toBeUndefined(); - }); - - it('throws when regenerating while loading', async () => { - const fixture = makeFakeSignalChatResource({ - isLoading: true, - messages: [ - { role: 'user', content: 'hello' }, - { role: 'assistant', content: 'hi', toolCalls: [] }, - ], - }); - const agent = toAgent(fixture.resource); - - await expect(agent.regenerate(1)).rejects.toThrow( - 'Cannot regenerate while agent is loading another response', - ); - }); - - it('throws when regenerating a non-assistant message', async () => { - const fixture = makeFakeSignalChatResource({ - messages: [{ role: 'user', content: 'hello' }], - }); - const agent = toAgent(fixture.resource); - - await expect(agent.regenerate(0)).rejects.toThrow( - 'Message at index 0 is not an assistant message', - ); - }); - - it('throws when regenerating an assistant message without a previous user message', async () => { - const fixture = makeFakeSignalChatResource({ - messages: [{ role: 'assistant', content: 'hello', toolCalls: [] }], - }); - const agent = toAgent(fixture.resource); - - await expect(agent.regenerate(0)).rejects.toThrow( - 'No user message found before the target assistant message', - ); - }); - - it('trims through the previous user message and resends on regenerate', async () => { - const messages: SignalChatResourceMessage[] = [ - { role: 'user', content: 'first' }, - { role: 'assistant', content: 'first answer', toolCalls: [] }, - { role: 'user', content: 'second' }, - { role: 'assistant', content: 'second answer', toolCalls: [] }, - ]; - const fixture = makeFakeSignalChatResource({ messages, withResend: true }); - const agent = toAgent(fixture.resource); - - await agent.regenerate(3); - - expect(fixture.setMessagesCalls).toEqual([ - [ - { role: 'user', content: 'first' }, - { role: 'assistant', content: 'first answer', toolCalls: [] }, - { role: 'user', content: 'second' }, - ], - ]); - expect(fixture.resendCount).toBe(1); - }); - - it('falls back to reload on regenerate when resend is unavailable', async () => { - const messages: SignalChatResourceMessage[] = [ - { role: 'user', content: 'hello' }, - { role: 'assistant', content: 'hi', toolCalls: [] }, - ]; - const fixture = makeFakeSignalChatResource({ messages, withReload: true }); - const agent = toAgent(fixture.resource); - - await agent.regenerate(1); - - expect(fixture.setMessagesCalls).toEqual([[{ role: 'user', content: 'hello' }]]); - expect(fixture.reloadCount).toBe(1); - }); - - it('serializes non-string message content deterministically', () => { - const fixture = makeFakeSignalChatResource({ - messages: [ - { role: 'user', content: { nested: ['value'] } }, - { role: 'assistant', content: { answer: 42 }, toolCalls: [] }, - ], - }); - const agent = toAgent(fixture.resource); - - expect(agent.messages()).toEqual([ - { - id: 'resource-message-0', - role: 'user', - content: '{"nested":["value"]}', - }, - { - id: 'resource-message-1', - role: 'assistant', - content: '{"answer":42}', - toolCallIds: [], - }, - ]); - }); -}); diff --git a/libs/signal-resource/src/lib/to-agent.ts b/libs/signal-resource/src/lib/to-agent.ts deleted file mode 100644 index cc4e94d6f..000000000 --- a/libs/signal-resource/src/lib/to-agent.ts +++ /dev/null @@ -1,177 +0,0 @@ -// SPDX-License-Identifier: MIT -import { computed, signal, type Signal } from '@angular/core'; -import type { Agent, AgentSubmitInput, AgentStatus, Message, ToolCall } from '@threadplane/chat'; -import { toAgentError } from '@threadplane/chat'; -import { EMPTY } from 'rxjs'; - -/** Tool-call shape exposed by a signal-backed chat resource. */ -export interface SignalChatResourceToolCall { - /** Resource-specific lifecycle state for the tool call. */ - status: 'pending' | 'done'; - /** Tool name shown in Threadplane tool-call UI. */ - name: string; - /** Stable identifier for linking assistant messages to tool-call details. */ - toolCallId: string; - /** Parsed tool arguments. */ - args: unknown; - /** Settled tool result, when the resource exposes one. */ - result?: PromiseSettledResult; -} - -/** Message shape consumed by the signal-resource adapter. */ -export type SignalChatResourceMessage = - | { role: 'user'; content: unknown } - | { role: 'assistant'; content?: unknown; toolCalls: SignalChatResourceToolCall[] } - | { role: 'error'; content: string }; - -/** Minimal structural contract required to adapt a signal-backed chat resource. */ -export interface SignalChatResourceLike { - /** Current resource message history. */ - value: Signal; - /** Whether the resource is currently producing a response. */ - isLoading: Signal; - /** Current resource error, if any. */ - error: Signal; - /** Send a new user message. */ - sendMessage: (message: { role: 'user'; content: unknown }) => void; - /** Stop the current response. Called only while `isLoading()` is true. */ - stop: () => void; - /** Replace resource message history. Used by `Agent.regenerate()`. */ - setMessages: (messages: SignalChatResourceMessage[]) => void; - /** Re-run against the current history, if supported by the resource. */ - resendMessages?: () => void; - /** Reload the resource, used as a fallback for `Agent.regenerate()`. */ - reload?: () => boolean; -} - -/** - * Wrap a signal-backed chat resource in the runtime-neutral Agent contract. - * - * @example - * ```ts - * import { toAgent } from '@threadplane/signal-resource'; - * - * const agent = toAgent(resource); - * ``` - */ -export function toAgent(resource: SignalChatResourceLike): Agent { - const messages = computed(() => { - return resource.value().map((message, index) => toAgentMessage(message, index)); - }); - - const toolCalls = computed(() => { - return resource.value().flatMap((message) => { - if (message.role !== 'assistant') return []; - return message.toolCalls.map(toAgentToolCall); - }); - }); - - const isLoading = computed(() => resource.isLoading()); - const error = computed(() => { - const raw = resource.error(); - return raw ? toAgentError(raw) : undefined; - }); - const status = computed(() => { - if (isLoading()) return 'running'; - if (error()) return 'error'; - return 'idle'; - }); - - return { - messages, - status, - isLoading, - error, - toolCalls, - state: signal>({}).asReadonly(), - events$: EMPTY, - submit: async (input: AgentSubmitInput) => { - if (input.message === undefined) return; - resource.sendMessage({ role: 'user', content: input.message }); - }, - stop: async () => { - if (!resource.isLoading()) return; - resource.stop(); - }, - retry: async () => { - resource.resendMessages?.(); - }, - regenerate: async (assistantMessageIndex: number) => { - if (resource.isLoading()) { - throw new Error('Cannot regenerate while agent is loading another response'); - } - const current = resource.value(); - const target = current[assistantMessageIndex]; - if (!target || target.role !== 'assistant') { - throw new Error(`Message at index ${assistantMessageIndex} is not an assistant message`); - } - const userIdx = current - .slice(0, assistantMessageIndex) - .map((m, i) => ({ m, i })) - .reverse() - .find(({ m }) => m.role === 'user')?.i; - if (userIdx === undefined) { - throw new Error('No user message found before the target assistant message'); - } - resource.setMessages(current.slice(0, userIdx + 1)); - if (resource.resendMessages) { - resource.resendMessages(); - } else { - resource.reload?.(); - } - }, - }; -} - -function toAgentMessage(message: SignalChatResourceMessage, index: number): Message { - const id = `resource-message-${index}`; - switch (message.role) { - case 'user': - return { id, role: 'user', content: normalizeContent(message.content) }; - case 'assistant': - return { - id, - role: 'assistant', - content: normalizeContent(message.content ?? ''), - toolCallIds: message.toolCalls.map((toolCall) => toolCall.toolCallId), - }; - case 'error': - return { id, role: 'assistant', content: message.content }; - } -} - -function toAgentToolCall(toolCall: SignalChatResourceToolCall): ToolCall { - if (toolCall.status === 'pending') { - return { - id: toolCall.toolCallId, - name: toolCall.name, - args: toolCall.args, - status: 'running', - }; - } - - if (toolCall.result?.status === 'rejected') { - return { - id: toolCall.toolCallId, - name: toolCall.name, - args: toolCall.args, - status: 'error', - error: toolCall.result.reason, - }; - } - - return { - id: toolCall.toolCallId, - name: toolCall.name, - args: toolCall.args, - status: 'complete', - result: toolCall.result?.value, - }; -} - -function normalizeContent(content: unknown): Message['content'] { - if (typeof content === 'string') return content; - if (content === undefined) return ''; - const serialized = JSON.stringify(content); - return serialized ?? String(content); -} diff --git a/libs/signal-resource/src/public-api.ts b/libs/signal-resource/src/public-api.ts deleted file mode 100644 index 3b5bfc14e..000000000 --- a/libs/signal-resource/src/public-api.ts +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: MIT -export { toAgent } from './lib/to-agent'; -export type { - SignalChatResourceLike, - SignalChatResourceMessage, - SignalChatResourceToolCall, -} from './lib/to-agent'; -export { provideAgent, injectAgent } from './lib/provide-agent'; -export type { SignalResourceAgentSource } from './lib/provide-agent'; diff --git a/libs/signal-resource/src/test-setup.ts b/libs/signal-resource/src/test-setup.ts deleted file mode 100644 index ca3d8a2b3..000000000 --- a/libs/signal-resource/src/test-setup.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { getTestBed } from '@angular/core/testing'; -import { - BrowserTestingModule, - platformBrowserTesting, -} from '@angular/platform-browser/testing'; - -getTestBed().initTestEnvironment( - BrowserTestingModule, - platformBrowserTesting(), - { teardown: { destroyAfterEach: true } }, -); diff --git a/libs/signal-resource/src/testing/fake-signal-chat-resource.ts b/libs/signal-resource/src/testing/fake-signal-chat-resource.ts deleted file mode 100644 index 8fc527425..000000000 --- a/libs/signal-resource/src/testing/fake-signal-chat-resource.ts +++ /dev/null @@ -1,97 +0,0 @@ -// SPDX-License-Identifier: MIT -import { signal, type Signal, type WritableSignal } from '@angular/core'; -import type { - SignalChatResourceLike, - SignalChatResourceMessage, -} from '../lib/to-agent'; - -export interface FakeSignalChatResource extends SignalChatResourceLike { - value: Signal; - isLoading: Signal; - error: Signal; -} - -export interface FakeSignalChatResourceFixture { - resource: FakeSignalChatResource; - messages: WritableSignal; - isLoading: WritableSignal; - error: WritableSignal; - sent: Array<{ role: 'user'; content: unknown }>; - setMessagesCalls: SignalChatResourceMessage[][]; - stopCount: number; - resendCount: number; - reloadCount: number; -} - -export interface FakeSignalChatResourceOptions { - messages?: SignalChatResourceMessage[]; - isLoading?: boolean; - error?: Error; - withResend?: boolean; - withReload?: boolean; -} - -export function makeFakeSignalChatResource( - options: FakeSignalChatResourceOptions = {}, -): FakeSignalChatResourceFixture { - const messages = signal(options.messages ?? []); - const isLoading = signal(options.isLoading ?? false); - const error = signal(options.error); - const sent: Array<{ role: 'user'; content: unknown }> = []; - const setMessagesCalls: SignalChatResourceMessage[][] = []; - let stopCount = 0; - let resendCount = 0; - let reloadCount = 0; - - const resource: FakeSignalChatResource = { - value: messages.asReadonly(), - isLoading: isLoading.asReadonly(), - error: error.asReadonly(), - sendMessage: (message) => { - sent.push(message); - messages.update((prev) => [...prev, message]); - }, - stop: () => { - if (!isLoading()) throw new Error('Cannot stop when idle'); - stopCount++; - isLoading.set(false); - }, - setMessages: (next) => { - setMessagesCalls.push(next); - messages.set(next); - }, - ...(options.withResend - ? { - resendMessages: () => { - resendCount++; - }, - } - : {}), - ...(options.withReload - ? { - reload: () => { - reloadCount++; - return true; - }, - } - : {}), - }; - - return { - resource, - messages, - isLoading, - error, - sent, - setMessagesCalls, - get stopCount() { - return stopCount; - }, - get resendCount() { - return resendCount; - }, - get reloadCount() { - return reloadCount; - }, - }; -} diff --git a/libs/signal-resource/src/testing/type-assert.ts b/libs/signal-resource/src/testing/type-assert.ts deleted file mode 100644 index 63a48b0bb..000000000 --- a/libs/signal-resource/src/testing/type-assert.ts +++ /dev/null @@ -1,4 +0,0 @@ -// SPDX-License-Identifier: MIT -export type Equal = - (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? true : false; -export type Expect = T; diff --git a/libs/signal-resource/tsconfig.json b/libs/signal-resource/tsconfig.json deleted file mode 100644 index da190b437..000000000 --- a/libs/signal-resource/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "experimentalDecorators": true, - "noPropertyAccessFromIndexSignature": true, - "module": "preserve", - "emitDeclarationOnly": false, - "composite": false - }, - "angularCompilerOptions": { - "enableI18nLegacyMessageIdFormat": false, - "strictInjectionParameters": true, - "strictInputAccessModifiers": true, - "strictTemplates": true - }, - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.lib.json" - } - ] -} diff --git a/libs/signal-resource/tsconfig.lib.json b/libs/signal-resource/tsconfig.lib.json deleted file mode 100644 index ce7efa590..000000000 --- a/libs/signal-resource/tsconfig.lib.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../dist/out-tsc", - "declaration": true, - "declarationMap": true, - "inlineSources": true, - "lib": ["es2022", "dom"], - "types": [] - }, - "include": ["src/**/*.ts"], - "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"], - "references": [ - { - "path": "../licensing/tsconfig.lib.json" - } - ] -} diff --git a/libs/signal-resource/tsconfig.lib.prod.json b/libs/signal-resource/tsconfig.lib.prod.json deleted file mode 100644 index 2a2faa884..000000000 --- a/libs/signal-resource/tsconfig.lib.prod.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "./tsconfig.lib.json", - "compilerOptions": { - "declarationMap": false - }, - "angularCompilerOptions": { - "compilationMode": "partial" - } -} diff --git a/libs/signal-resource/tsconfig.spec.json b/libs/signal-resource/tsconfig.spec.json deleted file mode 100644 index f18aabc77..000000000 --- a/libs/signal-resource/tsconfig.spec.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "types": ["vitest/globals"], - "lib": ["es2022", "dom"] - }, - "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] -} diff --git a/libs/signal-resource/tsconfig.type-tests.json b/libs/signal-resource/tsconfig.type-tests.json deleted file mode 100644 index 47e1cd415..000000000 --- a/libs/signal-resource/tsconfig.type-tests.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extends": "./tsconfig.lib.json", - "compilerOptions": { - "noEmit": true, - "strict": true, - "strictFunctionTypes": true, - "skipLibCheck": true, - "types": [], - "inlineSources": false, - "declaration": false, - "declarationMap": false, - "noUnusedLocals": false - }, - "files": [], - "include": ["src/**/*.type-spec.ts"], - "exclude": ["src/**/*.spec.ts"], - "references": [] -} diff --git a/libs/signal-resource/vite.config.mts b/libs/signal-resource/vite.config.mts deleted file mode 100644 index ce406638a..000000000 --- a/libs/signal-resource/vite.config.mts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineConfig } from 'vite'; -import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; - -export default defineConfig({ - plugins: [nxViteTsPaths()], - test: { - globals: true, - environment: 'jsdom', - include: ['src/**/*.spec.ts'], - setupFiles: ['src/test-setup.ts'], - passWithNoTests: true, - }, -}); diff --git a/nx.json b/nx.json index 8f0e60bb4..eb610af5c 100644 --- a/nx.json +++ b/nx.json @@ -52,14 +52,13 @@ "render", "a2ui", "licensing", - "signal-resource", "telemetry" ], "projectsRelationship": "fixed" } }, "version": { - "preVersionCommand": "npx nx run-many -t build --projects=chat,langgraph,ag-ui,render,a2ui,licensing,signal-resource,telemetry && node libs/telemetry/scripts/apply-install-telemetry.mjs dist/libs/chat dist/libs/langgraph dist/libs/ag-ui dist/libs/render dist/libs/a2ui dist/libs/licensing dist/libs/signal-resource && node libs/telemetry/scripts/verify-install-telemetry.mjs dist/libs/chat dist/libs/langgraph dist/libs/ag-ui dist/libs/render dist/libs/a2ui dist/libs/licensing dist/libs/signal-resource dist/libs/telemetry && node libs/telemetry/scripts/smoke-install-telemetry.mjs dist/libs/chat dist/libs/langgraph dist/libs/ag-ui dist/libs/render dist/libs/a2ui dist/libs/licensing dist/libs/signal-resource dist/libs/telemetry", + "preVersionCommand": "npx nx run-many -t build --projects=chat,langgraph,ag-ui,render,a2ui,licensing,telemetry && node libs/telemetry/scripts/apply-install-telemetry.mjs dist/libs/chat dist/libs/langgraph dist/libs/ag-ui dist/libs/render dist/libs/a2ui dist/libs/licensing && node libs/telemetry/scripts/verify-install-telemetry.mjs dist/libs/chat dist/libs/langgraph dist/libs/ag-ui dist/libs/render dist/libs/a2ui dist/libs/licensing dist/libs/telemetry && node libs/telemetry/scripts/smoke-install-telemetry.mjs dist/libs/chat dist/libs/langgraph dist/libs/ag-ui dist/libs/render dist/libs/a2ui dist/libs/licensing dist/libs/telemetry", "updateDependents": "auto", "preserveLocalDependencyProtocols": true, "currentVersionResolver": "git-tag", diff --git a/package-lock.json b/package-lock.json index e9f9d8fcd..8b5f8afeb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -375,16 +375,6 @@ "@json-render/core": "^0.16.0" } }, - "libs/signal-resource": { - "name": "@threadplane/signal-resource", - "version": "0.0.56", - "license": "MIT", - "peerDependencies": { - "@angular/core": "^20.0.0 || ^21.0.0", - "@threadplane/chat": "*", - "rxjs": "~7.8.0" - } - }, "libs/telemetry": { "name": "@threadplane/telemetry", "version": "0.0.56", @@ -20576,10 +20566,6 @@ "resolved": "libs/render", "link": true }, - "node_modules/@threadplane/signal-resource": { - "resolved": "libs/signal-resource", - "link": true - }, "node_modules/@threadplane/telemetry": { "resolved": "libs/telemetry", "link": true diff --git a/tsconfig.base.json b/tsconfig.base.json index 3cdbc076c..84e274fd9 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -42,7 +42,6 @@ "@threadplane/marketing-channels": ["marketing/channels/src/index.ts"], "@threadplane/marketing-metrics": ["marketing/metrics/src/index.ts"], "@threadplane/render": ["libs/render/src/public-api.ts"], - "@threadplane/signal-resource": ["libs/signal-resource/src/public-api.ts"], "@threadplane/telemetry": ["libs/telemetry/src/index.ts"], "@threadplane/telemetry/browser": ["libs/telemetry/src/browser/public-api.ts"], "@threadplane/telemetry/node": ["libs/telemetry/src/node/index.ts"],