From 75192fe7f0e3d4d0f14f4afa5d3d43cda9a21ef8 Mon Sep 17 00:00:00 2001 From: Bobby Christopher Date: Mon, 29 Sep 2025 16:57:42 -0400 Subject: [PATCH 1/8] added some base --- src/apis/api.ts | 7 +++++- src/apis/patchportal.ts | 48 +++++++++++++++++++++++++++++++++++++ src/apis/prompt.ts | 10 ++++++++ src/autostart/index.ts | 2 +- src/index.ts | 6 +++-- src/server/server.ts | 17 ++++++++++++- src/types.ts | 10 ++++++++ test/server/context.test.ts | 26 ++++++++++---------- 8 files changed, 108 insertions(+), 18 deletions(-) create mode 100644 src/apis/patchportal.ts create mode 100644 src/apis/prompt.ts diff --git a/src/apis/api.ts b/src/apis/api.ts index 788bd597..182160f8 100644 --- a/src/apis/api.ts +++ b/src/apis/api.ts @@ -32,7 +32,8 @@ export type ServiceName = | 'vector' | 'keyvalue' | 'stream' - | 'objectstore'; + | 'objectstore' + | 'prompt'; interface ApiRequestBase { method: 'POST' | 'GET' | 'PUT' | 'DELETE'; @@ -105,6 +106,10 @@ export const getBaseUrlForService = (service?: ServiceName) => { process.env.AGENTUITY_OBJECTSTORE_URL || process.env.AGENTUITY_TRANSPORT_URL; break; + case 'prompt': + value = + process.env.AGENTUITY_PROMPT_URL || process.env.AGENTUITY_TRANSPORT_URL; + break; default: break; } diff --git a/src/apis/patchportal.ts b/src/apis/patchportal.ts new file mode 100644 index 00000000..b7f550ae --- /dev/null +++ b/src/apis/patchportal.ts @@ -0,0 +1,48 @@ +/** + * Thread-safe singleton class for PatchPortal + */ +export default class PatchPortal { + private static instance: PatchPortal | null = null; + private static lock: Promise | null = null; + private static state: { [key: string]: any } = {}; + + private constructor() { + // Private constructor to prevent direct instantiation + } + + /** + * Get the singleton instance of PatchPortal in a thread-safe manner + */ + public static async getInstance(): Promise { + if (PatchPortal.instance) { + return PatchPortal.instance; + } + + // Double-checked locking pattern for thread safety + if (!PatchPortal.lock) { + PatchPortal.lock = (async () => { + if (!PatchPortal.instance) { + PatchPortal.instance = new PatchPortal(); + } + return PatchPortal.instance; + })(); + } + + return PatchPortal.lock; + } + + /** + * Example method - you can add your specific functionality here + */ + public async process(key: string, data: unknown): Promise { + PatchPortal.state[key] = data; + return data; + } + + /** + * Example method for demonstrating the singleton + */ + public getInstanceId(): string { + return `PatchPortal-${Date.now()}`; + } +} diff --git a/src/apis/prompt.ts b/src/apis/prompt.ts new file mode 100644 index 00000000..3b64ca4f --- /dev/null +++ b/src/apis/prompt.ts @@ -0,0 +1,10 @@ +import type { PromptStorage } from '../types'; + +/** + * This class get patched in the CLI to get its prompt function. + */ +export default class PromptAPI implements PromptStorage { + test(): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/src/autostart/index.ts b/src/autostart/index.ts index e1ab5b86..ce4c1ab7 100644 --- a/src/autostart/index.ts +++ b/src/autostart/index.ts @@ -106,7 +106,7 @@ export async function run(config: AutostartConfig) { environment: config.devmode ? 'development' : config.environment, }); const server = await createServer({ - context: createServerContext({ + context: await createServerContext({ devmode: config.devmode, deploymentId: config.deploymentId, projectId: config.projectId, diff --git a/src/index.ts b/src/index.ts index 47e1c307..3e15adee 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,15 +5,17 @@ export * from './types'; import DiscordAPI from './apis/discord'; // Export APIs import EmailAPI from './apis/email'; +import PatchPortal from './apis/patchportal'; +import PromptAPI from './apis/prompt'; import StreamAPIImpl from './apis/stream'; -export { EmailAPI, DiscordAPI, StreamAPIImpl }; +export { EmailAPI, DiscordAPI, PatchPortal, PromptAPI, StreamAPIImpl }; import { TeamsActivityHandler } from 'botbuilder'; import { run } from './autostart'; +import { UnsupportedSlackPayload } from './io/slack'; import { AgentuityTeamsActivityHandler } from './io/teams/AgentuityTeamsActivityHandler'; import { AgentuityTeamsAdapter } from './io/teams/AgentuityTeamsAdapter'; import type { AgentConfig } from './types'; -import { UnsupportedSlackPayload } from './io/slack'; export { AgentuityTeamsAdapter, diff --git a/src/server/server.ts b/src/server/server.ts index 127fe4da..cb3acfc2 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -5,6 +5,8 @@ import DiscordAPI from '../apis/discord'; import EmailAPI from '../apis/email'; import KeyValueAPI from '../apis/keyvalue'; import ObjectStoreAPI from '../apis/objectstore'; +import PatchPortal from '../apis/patchportal'; +import PromptAPI from '../apis/prompt'; import StreamAPIImpl from '../apis/stream'; import VectorAPI from '../apis/vector'; import type { Logger } from '../logger'; @@ -178,6 +180,10 @@ const stream = new StreamAPIImpl(); const email = new EmailAPI(); const discord = new DiscordAPI(); const objectstore = new ObjectStoreAPI(); +const prompt = new PromptAPI(); + +// PatchPortal will be initialized lazily since it's async +let patchportal: PatchPortal | null = null; /** * Creates an agent context for server operations @@ -185,10 +191,17 @@ const objectstore = new ObjectStoreAPI(); * @param req - The server context request parameters * @returns An agent context object */ -export function createServerContext(req: ServerContextRequest): AgentContext { +export async function createServerContext( + req: ServerContextRequest +): Promise { // Use sessionId if provided, otherwise fallback to runId, and ensure sess_ prefix const sessionId = ensureSessionIdPrefix(req.sessionId || req.runId || ''); + // Initialize PatchPortal if not already done + if (!patchportal) { + patchportal = await PatchPortal.getInstance(); + } + return { devmode: req.devmode, sessionId, @@ -205,6 +218,8 @@ export function createServerContext(req: ServerContextRequest): AgentContext { email, discord, objectstore, + prompt, + patchportal, sdkVersion: req.sdkVersion, agents: req.agents, scope: 'local', diff --git a/src/types.ts b/src/types.ts index 25cead85..f0235ed3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,6 +2,7 @@ import type { ReadableStream, WritableStream } from 'node:stream/web'; import type { Meter, Tracer } from '@opentelemetry/api'; import type { DiscordMessage } from './io/discord'; import type { Email } from './io/email'; + import type { Slack, SlackAttachmentsMessage, @@ -633,6 +634,10 @@ export interface SlackService { ): Promise; } +export interface PromptStorage { + test(): Promise; +} + export interface ObjectStorePutParams { /** * the content type of the object @@ -902,6 +907,11 @@ export interface AgentContext { * the slack service */ slack: SlackService; + + /** + * the prompt storage + */ + prompt: PromptStorage; } /** diff --git a/test/server/context.test.ts b/test/server/context.test.ts index 6c76e906..73d95eb6 100644 --- a/test/server/context.test.ts +++ b/test/server/context.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'bun:test'; -import { createServerContext } from '../../src/server/server'; import type { Logger, Meter, Tracer } from '@opentelemetry/api'; +import { createServerContext } from '../../src/server/server'; import type { AgentConfig } from '../../src/types'; import '../setup'; // Import global test setup @@ -11,7 +11,7 @@ describe('Server Context', () => { const mockAgents: AgentConfig[] = []; describe('createServerContext', () => { - it('should create context with sessionId and set runId equal to sessionId', () => { + it('should create context with sessionId and set runId equal to sessionId', async () => { const req = { tracer: mockTracer, meter: mockMeter, @@ -21,14 +21,14 @@ describe('Server Context', () => { agents: mockAgents, }; - const context = createServerContext(req); + const context = await createServerContext(req); expect(context.sessionId).toBe('sess_test-session-123'); expect(context.runId).toBe('sess_test-session-123'); expect(context.runId).toBe(context.sessionId); }); - it('should add sess_ prefix if sessionId does not start with it', () => { + it('should add sess_ prefix if sessionId does not start with it', async () => { const req = { tracer: mockTracer, meter: mockMeter, @@ -38,13 +38,13 @@ describe('Server Context', () => { agents: mockAgents, }; - const context = createServerContext(req); + const context = await createServerContext(req); expect(context.sessionId).toBe('sess_no-prefix-session'); expect(context.runId).toBe('sess_no-prefix-session'); }); - it('should not add prefix if sessionId already starts with sess_', () => { + it('should not add prefix if sessionId already starts with sess_', async () => { const req = { tracer: mockTracer, meter: mockMeter, @@ -54,13 +54,13 @@ describe('Server Context', () => { agents: mockAgents, }; - const context = createServerContext(req); + const context = await createServerContext(req); expect(context.sessionId).toBe('sess_already-prefixed'); expect(context.runId).toBe('sess_already-prefixed'); }); - it('should fallback to runId if sessionId is not provided', () => { + it('should fallback to runId if sessionId is not provided', async () => { const req = { tracer: mockTracer, meter: mockMeter, @@ -70,13 +70,13 @@ describe('Server Context', () => { agents: mockAgents, }; - const context = createServerContext(req); + const context = await createServerContext(req); expect(context.sessionId).toBe('sess_legacy-run-id'); expect(context.runId).toBe('sess_legacy-run-id'); }); - it('should handle empty sessionId and runId', () => { + it('should handle empty sessionId and runId', async () => { const req = { tracer: mockTracer, meter: mockMeter, @@ -85,13 +85,13 @@ describe('Server Context', () => { agents: mockAgents, }; - const context = createServerContext(req); + const context = await createServerContext(req); expect(context.sessionId).toBe('sess_'); expect(context.runId).toBe('sess_'); }); - it('should prefer sessionId over runId when both are provided', () => { + it('should prefer sessionId over runId when both are provided', async () => { const req = { tracer: mockTracer, meter: mockMeter, @@ -102,7 +102,7 @@ describe('Server Context', () => { agents: mockAgents, }; - const context = createServerContext(req); + const context = await createServerContext(req); expect(context.sessionId).toBe('sess_new-session'); expect(context.runId).toBe('sess_new-session'); From 31e6d57f4ec18847c8b0bdbacf492bb53c6bc30e Mon Sep 17 00:00:00 2001 From: Bobby Christopher Date: Tue, 30 Sep 2025 12:15:49 -0400 Subject: [PATCH 2/8] checkpoint --- package-lock.json | 38 ++++++++++++++++++++++++++-- package.json | 4 +-- src/apis/prompt.ts | 10 -------- src/apis/prompt/generated/_index.js | 2 ++ src/apis/prompt/generated/index.d.ts | 6 +++++ src/apis/prompt/generated/index.ts | 6 +++++ src/apis/prompt/index.ts | 24 ++++++++++++++++++ src/server/server.ts | 6 ++--- src/types.ts | 8 ++---- 9 files changed, 81 insertions(+), 23 deletions(-) delete mode 100644 src/apis/prompt.ts create mode 100644 src/apis/prompt/generated/_index.js create mode 100644 src/apis/prompt/generated/index.d.ts create mode 100644 src/apis/prompt/generated/index.ts create mode 100644 src/apis/prompt/index.ts diff --git a/package-lock.json b/package-lock.json index bdbd2c60..c8ba189e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { "name": "@agentuity/sdk", - "version": "0.0.146", + "version": "0.0.148", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@agentuity/sdk", - "version": "0.0.146", + "version": "0.0.148", "license": "Apache-2.0", "dependencies": { + "@agentuity/sdk": "file:agentuity-sdk-0.0.147.tgz", "@opentelemetry/api": "^1.9.0", "@opentelemetry/api-logs": "^0.57.2", "@opentelemetry/auto-instrumentations-node": "^0.56.1", @@ -43,6 +44,39 @@ "typescript": "^5" } }, + "node_modules/@agentuity/sdk": { + "version": "0.0.147", + "resolved": "file:agentuity-sdk-0.0.147.tgz", + "integrity": "sha512-9aLlDmSRCuukqf1fa+bxgMffxeoO21vPYec9I4L2OgM89zDeizdpCIkMR4SSHGIrz4xaMEVy89Khi4kLgo7u1A==", + "license": "Apache-2.0", + "dependencies": { + "@agentuity/sdk": "file:agentuity-sdk-0.0.147.tgz", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/api-logs": "^0.57.2", + "@opentelemetry/auto-instrumentations-node": "^0.56.1", + "@opentelemetry/core": "^1.21.0", + "@opentelemetry/exporter-logs-otlp-http": "^0.57.2", + "@opentelemetry/exporter-metrics-otlp-http": "^0.57.2", + "@opentelemetry/exporter-trace-otlp-http": "^0.57.2", + "@opentelemetry/host-metrics": "^0.35.5", + "@opentelemetry/resources": "^1.30.1", + "@opentelemetry/sdk-logs": "^0.57.2", + "@opentelemetry/sdk-metrics": "^1.30.1", + "@opentelemetry/sdk-node": "^0.57.2", + "@opentelemetry/semantic-conventions": "^1.30.0", + "@traceloop/node-server-sdk": "^0.18.1", + "js-yaml": "^4.1.0", + "mailparser": "^3.7.4", + "nodemailer": "^7.0.3" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "botbuilder": "^4.23.2", + "typescript": "^5" + } + }, "node_modules/@aws-crypto/sha256-browser": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", diff --git a/package.json b/package.json index 5679cbb3..984c8b87 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@agentuity/sdk", - "version": "0.0.146", + "version": "0.0.149", "description": "The Agentuity SDK for NodeJS and Bun", "license": "Apache-2.0", "public": true, @@ -96,4 +96,4 @@ "mailparser": "^3.7.4", "nodemailer": "^7.0.3" } -} +} \ No newline at end of file diff --git a/src/apis/prompt.ts b/src/apis/prompt.ts deleted file mode 100644 index 3b64ca4f..00000000 --- a/src/apis/prompt.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { PromptStorage } from '../types'; - -/** - * This class get patched in the CLI to get its prompt function. - */ -export default class PromptAPI implements PromptStorage { - test(): Promise { - throw new Error('Method not implemented.'); - } -} diff --git a/src/apis/prompt/generated/_index.js b/src/apis/prompt/generated/_index.js new file mode 100644 index 00000000..c95cbdf1 --- /dev/null +++ b/src/apis/prompt/generated/_index.js @@ -0,0 +1,2 @@ +export const prompts = { +}; diff --git a/src/apis/prompt/generated/index.d.ts b/src/apis/prompt/generated/index.d.ts new file mode 100644 index 00000000..5d6d3ef1 --- /dev/null +++ b/src/apis/prompt/generated/index.d.ts @@ -0,0 +1,6 @@ +// biome-ignore lint/suspicious/noEmptyInterface: +export interface PromptsCollection {} + +export declare const prompts: PromptsCollection; +export type PromptConfig = any; +export type PromptName = any; diff --git a/src/apis/prompt/generated/index.ts b/src/apis/prompt/generated/index.ts new file mode 100644 index 00000000..2f26221a --- /dev/null +++ b/src/apis/prompt/generated/index.ts @@ -0,0 +1,6 @@ +export type PromptConfig = any; +export type PromptName = any; + +export interface PromptsCollection { + [promptSlug: string]: any; +} diff --git a/src/apis/prompt/index.ts b/src/apis/prompt/index.ts new file mode 100644 index 00000000..80f7fe2f --- /dev/null +++ b/src/apis/prompt/index.ts @@ -0,0 +1,24 @@ +// Main entry point for prompts - following POC pattern exactly + +import { prompts } from './generated/_index.js'; +import { PromptConfig, PromptName } from './generated/index.ts'; + +export default class PromptAPI { + public prompts: typeof prompts; + + constructor() { + // Initialize with generated prompts (following POC pattern exactly) + this.prompts = prompts; + + // Warn if no prompts are available (clean state) + if (Object.keys(prompts).length === 0) { + console.warn( + '⚠️ No generated prompts found. Run `agentuity bundle` to generate prompts from src/prompts.yaml' + ); + } + } +} + +// Re-export generated types and prompts (following POC pattern) +export { prompts, PromptConfig, PromptName }; +export * from './generated/index.js'; diff --git a/src/server/server.ts b/src/server/server.ts index cb3acfc2..b3ebcf36 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -6,7 +6,7 @@ import EmailAPI from '../apis/email'; import KeyValueAPI from '../apis/keyvalue'; import ObjectStoreAPI from '../apis/objectstore'; import PatchPortal from '../apis/patchportal'; -import PromptAPI from '../apis/prompt'; +import PromptAPI from '../apis/prompt/index.js'; import StreamAPIImpl from '../apis/stream'; import VectorAPI from '../apis/vector'; import type { Logger } from '../logger'; @@ -180,7 +180,7 @@ const stream = new StreamAPIImpl(); const email = new EmailAPI(); const discord = new DiscordAPI(); const objectstore = new ObjectStoreAPI(); -const prompt = new PromptAPI(); +const promptAPI = new PromptAPI(); // PatchPortal will be initialized lazily since it's async let patchportal: PatchPortal | null = null; @@ -216,9 +216,9 @@ export async function createServerContext( vector, stream, email, + prompts: () => promptAPI.prompts, discord, objectstore, - prompt, patchportal, sdkVersion: req.sdkVersion, agents: req.agents, diff --git a/src/types.ts b/src/types.ts index f0235ed3..69a8cc35 100644 --- a/src/types.ts +++ b/src/types.ts @@ -634,10 +634,6 @@ export interface SlackService { ): Promise; } -export interface PromptStorage { - test(): Promise; -} - export interface ObjectStorePutParams { /** * the content type of the object @@ -909,9 +905,9 @@ export interface AgentContext { slack: SlackService; /** - * the prompt storage + * get the prompts collection for compiling dynamic prompts */ - prompt: PromptStorage; + prompts(): import('./apis/prompt/generated/index.js').PromptsCollection; } /** From dfee537c3d136984f83b53e7cb3e9cb5cbf2c384 Mon Sep 17 00:00:00 2001 From: Bobby Christopher Date: Tue, 30 Sep 2025 12:39:26 -0400 Subject: [PATCH 3/8] checkpoint --- package-lock.json | 38 +----------------- package.json | 2 +- src/apis/prompt/index.ts | 83 ++++++++++++++++++++++++++++++++++++---- src/io/email.ts | 4 +- src/server/server.ts | 1 + 5 files changed, 82 insertions(+), 46 deletions(-) diff --git a/package-lock.json b/package-lock.json index c8ba189e..ee61c0d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,14 @@ { "name": "@agentuity/sdk", - "version": "0.0.148", + "version": "0.0.150", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@agentuity/sdk", - "version": "0.0.148", + "version": "0.0.150", "license": "Apache-2.0", "dependencies": { - "@agentuity/sdk": "file:agentuity-sdk-0.0.147.tgz", "@opentelemetry/api": "^1.9.0", "@opentelemetry/api-logs": "^0.57.2", "@opentelemetry/auto-instrumentations-node": "^0.56.1", @@ -44,39 +43,6 @@ "typescript": "^5" } }, - "node_modules/@agentuity/sdk": { - "version": "0.0.147", - "resolved": "file:agentuity-sdk-0.0.147.tgz", - "integrity": "sha512-9aLlDmSRCuukqf1fa+bxgMffxeoO21vPYec9I4L2OgM89zDeizdpCIkMR4SSHGIrz4xaMEVy89Khi4kLgo7u1A==", - "license": "Apache-2.0", - "dependencies": { - "@agentuity/sdk": "file:agentuity-sdk-0.0.147.tgz", - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/api-logs": "^0.57.2", - "@opentelemetry/auto-instrumentations-node": "^0.56.1", - "@opentelemetry/core": "^1.21.0", - "@opentelemetry/exporter-logs-otlp-http": "^0.57.2", - "@opentelemetry/exporter-metrics-otlp-http": "^0.57.2", - "@opentelemetry/exporter-trace-otlp-http": "^0.57.2", - "@opentelemetry/host-metrics": "^0.35.5", - "@opentelemetry/resources": "^1.30.1", - "@opentelemetry/sdk-logs": "^0.57.2", - "@opentelemetry/sdk-metrics": "^1.30.1", - "@opentelemetry/sdk-node": "^0.57.2", - "@opentelemetry/semantic-conventions": "^1.30.0", - "@traceloop/node-server-sdk": "^0.18.1", - "js-yaml": "^4.1.0", - "mailparser": "^3.7.4", - "nodemailer": "^7.0.3" - }, - "engines": { - "node": ">=22" - }, - "peerDependencies": { - "botbuilder": "^4.23.2", - "typescript": "^5" - } - }, "node_modules/@aws-crypto/sha256-browser": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", diff --git a/package.json b/package.json index 984c8b87..22485cc9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@agentuity/sdk", - "version": "0.0.149", + "version": "0.0.150", "description": "The Agentuity SDK for NodeJS and Bun", "license": "Apache-2.0", "public": true, diff --git a/src/apis/prompt/index.ts b/src/apis/prompt/index.ts index 80f7fe2f..7f58ceb3 100644 --- a/src/apis/prompt/index.ts +++ b/src/apis/prompt/index.ts @@ -1,17 +1,86 @@ // Main entry point for prompts - following POC pattern exactly -import { prompts } from './generated/_index.js'; import { PromptConfig, PromptName } from './generated/index.ts'; +// Default empty prompts object +const defaultPrompts = {}; + export default class PromptAPI { - public prompts: typeof prompts; + public prompts: typeof defaultPrompts; constructor() { - // Initialize with generated prompts (following POC pattern exactly) - this.prompts = prompts; + // Initialize with empty prompts by default + this.prompts = defaultPrompts; + } + + // Method to load prompts dynamically (called by context) + public async loadPrompts(): Promise { + // console.log('loadPrompts() called'); + try { + // Try multiple possible paths for the generated prompts + let generatedModule: any; + + // Skip relative path - doesn't work in bundled environment + // Try absolute path from node_modules + const path = require('path'); + const fs = require('fs'); - // Warn if no prompts are available (clean state) - if (Object.keys(prompts).length === 0) { + // Look for the generated file in common locations + const possiblePaths = [ + path.join( + process.cwd(), + 'node_modules', + '@agentuity', + 'sdk', + 'dist', + 'apis', + 'prompt', + 'generated', + '_index.js' + ), + path.join( + process.cwd(), + 'node_modules', + '@agentuity', + 'sdk', + 'src', + 'apis', + 'prompt', + 'generated', + '_index.js' + ), + ]; + + // console.log('Trying absolute paths:'); + for (const possiblePath of possiblePaths) { + // console.log(' Checking:', possiblePath); + // console.log(' Exists:', fs.existsSync(possiblePath)); + if (fs.existsSync(possiblePath)) { + delete require.cache[possiblePath]; + generatedModule = require(possiblePath); + // console.log(' Successfully loaded from:', possiblePath); + break; + } + } + + if (!generatedModule) { + throw new Error('Generated prompts file not found'); + } + + // console.log('Generated module:', generatedModule); + // console.log( + // 'Prompts in module:', + // Object.keys(generatedModule.prompts || {}) + // ); + this.prompts = generatedModule.prompts || defaultPrompts; + // console.log('Final prompts:', Object.keys(this.prompts)); + } catch (error) { + // Fallback to empty prompts if generated file doesn't exist + console.log( + 'Error loading prompts:', + error instanceof Error ? error.message : String(error) + ); + this.prompts = defaultPrompts; console.warn( '⚠️ No generated prompts found. Run `agentuity bundle` to generate prompts from src/prompts.yaml' ); @@ -20,5 +89,5 @@ export default class PromptAPI { } // Re-export generated types and prompts (following POC pattern) -export { prompts, PromptConfig, PromptName }; +export { defaultPrompts as prompts, PromptConfig, PromptName }; export * from './generated/index.js'; diff --git a/src/io/email.ts b/src/io/email.ts index 1eb4cc4e..ad68ddeb 100644 --- a/src/io/email.ts +++ b/src/io/email.ts @@ -4,8 +4,8 @@ import type { ReadableStream } from 'node:stream/web'; import { inspect } from 'node:util'; import { context, SpanStatusCode, trace } from '@opentelemetry/api'; import { type Headers, type ParsedMail, simpleParser } from 'mailparser'; -import MailComposer from 'nodemailer/lib/mail-composer'; -import type { Address, Attachment } from 'nodemailer/lib/mailer'; +import MailComposer from 'nodemailer/lib/mail-composer/index.js'; +import type { Address, Attachment } from 'nodemailer/lib/mailer/index.js'; import { send } from '../apis/api'; import { DataHandler } from '../router/data'; import { getTracer, recordException } from '../router/router'; diff --git a/src/server/server.ts b/src/server/server.ts index b3ebcf36..fe1d4489 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -201,6 +201,7 @@ export async function createServerContext( if (!patchportal) { patchportal = await PatchPortal.getInstance(); } + await promptAPI.loadPrompts(); return { devmode: req.devmode, From 3dbe64fdea8d5630f41729678a7c46386ba2f5bd Mon Sep 17 00:00:00 2001 From: Bobby Christopher Date: Wed, 1 Oct 2025 10:33:16 -0400 Subject: [PATCH 4/8] Update code generation rules for optional field handling - Add rules requiring generated code to never need optional chaining - Always generate both system and prompt fields, even if empty - Empty fields should return empty strings, not undefined - Prevents partial objects that require optional chaining in agent code --- .cursor/rules/code-generation.mdc | 151 +++++++++++++++++++++++++++ package.json | 2 +- src/apis/prompt/generated/index.d.ts | 1 - src/server/server.ts | 2 +- src/types.ts | 2 +- 5 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 .cursor/rules/code-generation.mdc diff --git a/.cursor/rules/code-generation.mdc b/.cursor/rules/code-generation.mdc new file mode 100644 index 00000000..23d1b8c6 --- /dev/null +++ b/.cursor/rules/code-generation.mdc @@ -0,0 +1,151 @@ +--- +description: Code Generation Development Rules for SDK +globs: src/apis/**/*.ts, src/server/*.ts, src/types.ts +alwaysApply: true +--- + +# Code Generation Development Rules + +> **⚠️ IMPORTANT**: These rules work in conjunction with the CLI rules. When updating these SDK rules, also update `cli/.cursor/rules/code-generation.mdc` to keep them in sync. + +## Core Principles + +### Optional Field Handling +- ✅ Generated code should NEVER require optional chaining (`?.`) +- ✅ Always generate both `system` and `prompt` fields, even if empty +- ✅ Empty fields should return empty strings, not undefined +- ❌ Never generate partial objects that require optional chaining + +### Never Modify Generated Content in Source Files +- ❌ NEVER edit files in `src/` that contain generated content +- ❌ NEVER hardcode generated content like `copyWriter` prompts in source files +- ✅ ALWAYS load generated content dynamically at runtime +- ✅ Provide fallbacks for missing generated content + +### Dynamic Loading Pattern +Generated content doesn't exist at SDK build time, so use dynamic loading patterns: + +```typescript +// ✅ Good: Dynamic loading with fallbacks +public async loadGeneratedContent(): Promise { + try { + const path = require('path'); + const fs = require('fs'); + + const possiblePaths = [ + path.join(process.cwd(), 'node_modules', '@agentuity', 'sdk', 'dist', 'generated', 'content.js'), + path.join(process.cwd(), 'node_modules', '@agentuity', 'sdk', 'src', 'generated', 'content.js') + ]; + + for (const possiblePath of possiblePaths) { + if (fs.existsSync(possiblePath)) { + const generatedModule = require(possiblePath); + this.content = generatedModule.content || defaultContent; + break; + } + } + } catch (error) { + this.content = defaultContent; + console.warn('No generated content found'); + } +} +``` + +## SDK-Specific Rules + +### Path Resolution +- Use absolute paths only (relative paths don't work in bundled environments) +- Check `dist/` directory first, then `src/` directory +- Always provide fallbacks for missing generated content + +### Context Integration +```typescript +// ✅ Good: Load generated content in context creation +export async function createServerContext(req: ServerContextRequest): Promise { + // ... other initialization + + // Load generated content dynamically + await promptAPI.loadPrompts(); + + return { + // ... other context properties + prompts: () => promptAPI.prompts, + }; +} +``` + +### Type Safety +- Generate TypeScript definitions for generated content +- Use proper type annotations for dynamic imports +- Maintain type safety throughout the loading process + +## Common Patterns + +### Generated Content API Class +```typescript +export default class GeneratedContentAPI { + public content: typeof defaultContent; + + constructor() { + this.content = defaultContent; + } + + public async loadContent(): Promise { + // Dynamic loading logic here + } +} +``` + +### Error Handling +```typescript +try { + // Try to load generated content + const generatedModule = require(possiblePath); + this.content = generatedModule.content || defaultContent; +} catch (error) { + // Fallback to default content + this.content = defaultContent; + console.warn('No generated content found'); +} +``` + +## Common Pitfalls to Avoid + +### ❌ Don't Do This +```typescript +// Hardcoding generated content in source files +export const prompts = { + copyWriter: { /* hardcoded content */ } +}; + +// Using relative imports that don't work in bundles +const generatedModule = require('./generated/_index.js'); + +// Not providing fallbacks +const generatedModule = require(possiblePath); // Will crash if file doesn't exist +``` + +### ✅ Do This Instead +```typescript +// Dynamic loading with absolute paths +const possiblePaths = [ + path.join(process.cwd(), 'node_modules', '@agentuity', 'sdk', 'dist', 'generated', '_index.js') +]; + +// With proper error handling +try { + const generatedModule = require(possiblePath); + this.content = generatedModule.content || defaultContent; +} catch (error) { + this.content = defaultContent; +} +``` + +## Build Considerations + +- Generated content is loaded at runtime, not build time +- Use `require()` for CommonJS compatibility in bundled environments +- Avoid `import()` statements for generated content +- Ensure fallbacks work when generated content is missing + +Remember: The SDK's job is to load generated content dynamically, not contain hardcoded generated content. \ No newline at end of file diff --git a/package.json b/package.json index 22485cc9..dfd5c3d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@agentuity/sdk", - "version": "0.0.150", + "version": "0.0.151", "description": "The Agentuity SDK for NodeJS and Bun", "license": "Apache-2.0", "public": true, diff --git a/src/apis/prompt/generated/index.d.ts b/src/apis/prompt/generated/index.d.ts index 5d6d3ef1..41db119a 100644 --- a/src/apis/prompt/generated/index.d.ts +++ b/src/apis/prompt/generated/index.d.ts @@ -1,6 +1,5 @@ // biome-ignore lint/suspicious/noEmptyInterface: export interface PromptsCollection {} - export declare const prompts: PromptsCollection; export type PromptConfig = any; export type PromptName = any; diff --git a/src/server/server.ts b/src/server/server.ts index fe1d4489..17618ec5 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -217,7 +217,7 @@ export async function createServerContext( vector, stream, email, - prompts: () => promptAPI.prompts, + _experimental_prompts: () => promptAPI.prompts, discord, objectstore, patchportal, diff --git a/src/types.ts b/src/types.ts index 69a8cc35..9db47497 100644 --- a/src/types.ts +++ b/src/types.ts @@ -907,7 +907,7 @@ export interface AgentContext { /** * get the prompts collection for compiling dynamic prompts */ - prompts(): import('./apis/prompt/generated/index.js').PromptsCollection; + _experimental_prompts(): import('./apis/prompt/generated/index.js').PromptsCollection; } /** From 059d884710b91dfb786d3a3f4bf8a8105a729187 Mon Sep 17 00:00:00 2001 From: Bobby Christopher Date: Wed, 1 Oct 2025 13:00:09 -0400 Subject: [PATCH 5/8] clean up --- src/apis/prompt/generated/_index.js | 4 ++++ src/apis/prompt/index.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/apis/prompt/generated/_index.js b/src/apis/prompt/generated/_index.js index c95cbdf1..36138bc2 100644 --- a/src/apis/prompt/generated/_index.js +++ b/src/apis/prompt/generated/_index.js @@ -1,2 +1,6 @@ export const prompts = { }; + +// Export types for compatibility +export const PromptConfig = undefined; // Type-only export, value is not used +export const PromptName = undefined; // Type-only export, value is not used diff --git a/src/apis/prompt/index.ts b/src/apis/prompt/index.ts index 7f58ceb3..d2080d0c 100644 --- a/src/apis/prompt/index.ts +++ b/src/apis/prompt/index.ts @@ -1,6 +1,6 @@ // Main entry point for prompts - following POC pattern exactly -import { PromptConfig, PromptName } from './generated/index.ts'; +import { PromptConfig, PromptName } from './generated/_index.js'; // Default empty prompts object const defaultPrompts = {}; From 7ef7d85ab4fab2bbefb090e701d160a7dd8cff2f Mon Sep 17 00:00:00 2001 From: Bobby Christopher Date: Wed, 1 Oct 2025 17:48:25 -0400 Subject: [PATCH 6/8] fixed a bunch of stuff --- .cursor/rules/code-generation.mdc | 18 +++++ package.json | 2 +- src/apis/patchportal.ts | 26 ++----- src/apis/prompt/generated/_index.js | 1 + src/apis/prompt/generated/index.ts | 7 +- src/apis/prompt/index.ts | 101 +++++++++++++++++++++------- src/index.ts | 1 + src/utils/interpolate.ts | 75 +++++++++++++++++++++ 8 files changed, 187 insertions(+), 44 deletions(-) create mode 100644 src/utils/interpolate.ts diff --git a/.cursor/rules/code-generation.mdc b/.cursor/rules/code-generation.mdc index 23d1b8c6..a627d51f 100644 --- a/.cursor/rules/code-generation.mdc +++ b/.cursor/rules/code-generation.mdc @@ -148,4 +148,22 @@ try { - Avoid `import()` statements for generated content - Ensure fallbacks work when generated content is missing +## Development Workflow + +### When Making Changes to Generated Content Structure: +1. **Update CLI code generation** in `cli/internal/bundler/prompts/code_generator.go` +2. **Update SDK to handle new structure** in `src/apis/prompt/index.ts` +3. **Bump SDK version** in `package.json` +4. **Build SDK**: `npm run build` +5. **Install in test project**: `npm install /path/to/sdk-js` +6. **Run bundle command**: `agentuity-cli bundle` to regenerate content +7. **Copy dist to node_modules**: `cp -r dist/* node_modules/@agentuity/sdk/dist/` (if needed) + +**Note**: After the first setup, you only need to run step 7 (`cp -r dist/* node_modules/@agentuity/sdk/dist/`) for subsequent changes to avoid reinstalling the entire package. + +### Required Exports +- ✅ Always export `interpolateTemplate` from main SDK index +- ✅ Generated content must import from `@agentuity/sdk` (not relative paths) +- ✅ Ensure all dependencies are properly exported + Remember: The SDK's job is to load generated content dynamically, not contain hardcoded generated content. \ No newline at end of file diff --git a/package.json b/package.json index dfd5c3d4..29da1ee6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@agentuity/sdk", - "version": "0.0.151", + "version": "0.0.154", "description": "The Agentuity SDK for NodeJS and Bun", "license": "Apache-2.0", "public": true, diff --git a/src/apis/patchportal.ts b/src/apis/patchportal.ts index b7f550ae..bfa8c835 100644 --- a/src/apis/patchportal.ts +++ b/src/apis/patchportal.ts @@ -1,41 +1,29 @@ /** - * Thread-safe singleton class for PatchPortal + * Singleton class for PatchPortal */ export default class PatchPortal { private static instance: PatchPortal | null = null; - private static lock: Promise | null = null; - private static state: { [key: string]: any } = {}; + private state: Record = {}; private constructor() { // Private constructor to prevent direct instantiation } /** - * Get the singleton instance of PatchPortal in a thread-safe manner + * Get the singleton instance of PatchPortal */ public static async getInstance(): Promise { - if (PatchPortal.instance) { - return PatchPortal.instance; + if (!PatchPortal.instance) { + PatchPortal.instance = new PatchPortal(); } - - // Double-checked locking pattern for thread safety - if (!PatchPortal.lock) { - PatchPortal.lock = (async () => { - if (!PatchPortal.instance) { - PatchPortal.instance = new PatchPortal(); - } - return PatchPortal.instance; - })(); - } - - return PatchPortal.lock; + return PatchPortal.instance; } /** * Example method - you can add your specific functionality here */ public async process(key: string, data: unknown): Promise { - PatchPortal.state[key] = data; + this.state[key] = data; return data; } diff --git a/src/apis/prompt/generated/_index.js b/src/apis/prompt/generated/_index.js index 36138bc2..d134ce22 100644 --- a/src/apis/prompt/generated/_index.js +++ b/src/apis/prompt/generated/_index.js @@ -4,3 +4,4 @@ export const prompts = { // Export types for compatibility export const PromptConfig = undefined; // Type-only export, value is not used export const PromptName = undefined; // Type-only export, value is not used +export const PromptsCollection = undefined; // Type-only export, value is not used diff --git a/src/apis/prompt/generated/index.ts b/src/apis/prompt/generated/index.ts index 2f26221a..c7394aa4 100644 --- a/src/apis/prompt/generated/index.ts +++ b/src/apis/prompt/generated/index.ts @@ -1,6 +1,11 @@ export type PromptConfig = any; export type PromptName = any; +// This will be replaced by the CLI with the actual generated types export interface PromptsCollection { - [promptSlug: string]: any; + [promptSlug: string]: { + slug: string; + system: { compile: (variables?: Record) => string }; + prompt: { compile: (variables?: Record) => string }; + }; } diff --git a/src/apis/prompt/index.ts b/src/apis/prompt/index.ts index d2080d0c..7f200414 100644 --- a/src/apis/prompt/index.ts +++ b/src/apis/prompt/index.ts @@ -1,34 +1,50 @@ // Main entry point for prompts - following POC pattern exactly +import fs from 'fs/promises'; +import path from 'path'; +import { pathToFileURL } from 'url'; import { PromptConfig, PromptName } from './generated/_index.js'; +import type { PromptsCollection } from './generated/index.js'; // Default empty prompts object const defaultPrompts = {}; +// Expected shape of generated module +interface GeneratedModule { + prompts?: PromptsCollection; +} + export default class PromptAPI { - public prompts: typeof defaultPrompts; + public prompts: PromptsCollection; constructor() { // Initialize with empty prompts by default this.prompts = defaultPrompts; } - // Method to load prompts dynamically (called by context) - public async loadPrompts(): Promise { - // console.log('loadPrompts() called'); - try { - // Try multiple possible paths for the generated prompts - let generatedModule: any; + /** + * Resolve possible paths for generated prompts using dynamic module resolution + */ + private async resolveGeneratedPaths(): Promise { + const paths: string[] = []; - // Skip relative path - doesn't work in bundled environment - // Try absolute path from node_modules - const path = require('path'); - const fs = require('fs'); + try { + // Try to resolve the @agentuity/sdk package + const sdkPath = require.resolve('@agentuity/sdk/package.json'); + const sdkRoot = path.dirname(sdkPath); - // Look for the generated file in common locations - const possiblePaths = [ + // Add both dist and src paths relative to resolved package + paths.push( + path.join(sdkRoot, 'dist', 'apis', 'prompt', 'generated', '_index.js'), + path.join(sdkRoot, 'src', 'apis', 'prompt', 'generated', '_index.js') + ); + } catch { + // Fallback to process.cwd() if package resolution fails + // (e.g., in bundled/serverless environments) + const fallbackRoot = process.cwd(); + paths.push( path.join( - process.cwd(), + fallbackRoot, 'node_modules', '@agentuity', 'sdk', @@ -39,7 +55,7 @@ export default class PromptAPI { '_index.js' ), path.join( - process.cwd(), + fallbackRoot, 'node_modules', '@agentuity', 'sdk', @@ -48,31 +64,70 @@ export default class PromptAPI { 'prompt', 'generated', '_index.js' - ), - ]; + ) + ); + } + + return paths; + } + + /** + * Type guard to validate generated module shape + */ + private isValidGeneratedModule(module: unknown): module is GeneratedModule { + return ( + typeof module === 'object' && + module !== null && + ((module as GeneratedModule).prompts === undefined || + typeof (module as GeneratedModule).prompts === 'object') + ); + } + + // Method to load prompts dynamically (called by context) + public async loadPrompts(): Promise { + // console.log('loadPrompts() called'); + try { + // Try multiple possible paths for the generated prompts + let generatedModule: unknown; + + // Dynamic module resolution strategy + const possiblePaths = await this.resolveGeneratedPaths(); // console.log('Trying absolute paths:'); for (const possiblePath of possiblePaths) { // console.log(' Checking:', possiblePath); - // console.log(' Exists:', fs.existsSync(possiblePath)); - if (fs.existsSync(possiblePath)) { - delete require.cache[possiblePath]; - generatedModule = require(possiblePath); + try { + await fs.access(possiblePath); + // Get file stats for cache-busting + const stats = await fs.stat(possiblePath); + const mtime = stats.mtime.getTime(); + + // Convert to file URL with cache-busting query param + const fileUrl = pathToFileURL(possiblePath).href + `?t=${mtime}`; + + // Use ESM dynamic import instead of require + generatedModule = await import(fileUrl); // console.log(' Successfully loaded from:', possiblePath); break; - } + } catch {} } if (!generatedModule) { throw new Error('Generated prompts file not found'); } + // Type guard to ensure generatedModule has expected shape + if (!this.isValidGeneratedModule(generatedModule)) { + throw new Error('Generated module has invalid shape'); + } + // console.log('Generated module:', generatedModule); // console.log( // 'Prompts in module:', // Object.keys(generatedModule.prompts || {}) // ); - this.prompts = generatedModule.prompts || defaultPrompts; + this.prompts = + generatedModule.prompts || (defaultPrompts as PromptsCollection); // console.log('Final prompts:', Object.keys(this.prompts)); } catch (error) { // Fallback to empty prompts if generated file doesn't exist diff --git a/src/index.ts b/src/index.ts index 3e15adee..ecc5ef08 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ export * from './logger'; export * from './server'; export * from './types'; +export * from './utils/interpolate'; import DiscordAPI from './apis/discord'; // Export APIs diff --git a/src/utils/interpolate.ts b/src/utils/interpolate.ts new file mode 100644 index 00000000..b0923e48 --- /dev/null +++ b/src/utils/interpolate.ts @@ -0,0 +1,75 @@ +/** + * Interpolates a template string with variables using go-common syntax + * Supports: + * - {variable} - optional variable (defaults to empty string) + * - {!variable} - required variable (throws error if missing) + * - {variable:default} - variable with default value + * - {!variable:default} - required variable with default value + */ +export function interpolateTemplate( + template: string, + variables: Record = {} +): string { + // Convert {{variable}} to {variable} for go-common compatibility + const goCommonTemplate = template.replace(/\{\{([^}]+)\}\}/g, '{$1}'); + + // Use go-common InterpolateString logic: {variable}, {!required}, {variable:-default} + // Support both {variable:default} and {variable:-default} syntax + return goCommonTemplate.replace( + /\{([!]?[^}:]+)(?::([^}]*))?\}/g, + (match, varName, defaultValue) => { + const isRequired = varName.startsWith('!'); + const cleanVarName = isRequired ? varName.substring(1) : varName; + const value = variables[cleanVarName]; + + if (value !== undefined && value !== '') { + return String(value); + } + + if (isRequired) { + throw new Error(`Required variable '${cleanVarName}' not provided`); + } + + return defaultValue !== undefined ? defaultValue : ''; + } + ); +} + +/** + * Extracts variable names from a template string + * Supports the same syntax as interpolateTemplate + */ +export function extractVariables(template: string): string[] { + // Convert {{variable}} to {variable} for go-common compatibility + const goCommonTemplate = template.replace(/\{\{([^}]+)\}\}/g, '{$1}'); + + const matches = goCommonTemplate.match(/\{([!]?[^}:]+)(?::([^}]*))?\}/g); + if (!matches) { + return []; + } + + const variables: string[] = []; + const seen = new Set(); + + for (const match of matches) { + const varMatch = match.match(/\{([!]?[^}:]+)(?::([^}]*))?\}/); + if (varMatch) { + let varName = varMatch[1].trim(); + // Remove ! prefix if present + if (varName.startsWith('!')) { + varName = varName.substring(1); + } + // Remove :default suffix if present + const colonIndex = varName.indexOf(':'); + if (colonIndex !== -1) { + varName = varName.substring(0, colonIndex); + } + if (!seen.has(varName)) { + variables.push(varName); + seen.add(varName); + } + } + } + + return variables; +} From adf770bfc0b5c5189770ed4b3685a07f56a91296 Mon Sep 17 00:00:00 2001 From: Bobby Christopher Date: Thu, 2 Oct 2025 08:19:51 -0400 Subject: [PATCH 7/8] fix tests --- src/utils/interpolate.ts | 10 ++- test/utils/interpolate.test.ts | 129 +++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 test/utils/interpolate.test.ts diff --git a/src/utils/interpolate.ts b/src/utils/interpolate.ts index b0923e48..c7631c0b 100644 --- a/src/utils/interpolate.ts +++ b/src/utils/interpolate.ts @@ -1,3 +1,5 @@ +// this is copied from https://github.com/agentuity/go-common/blob/main/string/interpolate_test.go +// but adapted to javascript /** * Interpolates a template string with variables using go-common syntax * Supports: @@ -8,7 +10,7 @@ */ export function interpolateTemplate( template: string, - variables: Record = {} + variables: Record = {} ): string { // Convert {{variable}} to {variable} for go-common compatibility const goCommonTemplate = template.replace(/\{\{([^}]+)\}\}/g, '{$1}'); @@ -17,12 +19,13 @@ export function interpolateTemplate( // Support both {variable:default} and {variable:-default} syntax return goCommonTemplate.replace( /\{([!]?[^}:]+)(?::([^}]*))?\}/g, - (match, varName, defaultValue) => { + (_match, varName, defaultValue) => { const isRequired = varName.startsWith('!'); const cleanVarName = isRequired ? varName.substring(1) : varName; const value = variables[cleanVarName]; - if (value !== undefined && value !== '') { + // Check if value exists and is not empty/null + if (value !== undefined && value !== null && value !== '') { return String(value); } @@ -30,6 +33,7 @@ export function interpolateTemplate( throw new Error(`Required variable '${cleanVarName}' not provided`); } + // Return default value if provided, otherwise empty string return defaultValue !== undefined ? defaultValue : ''; } ); diff --git a/test/utils/interpolate.test.ts b/test/utils/interpolate.test.ts new file mode 100644 index 00000000..4a4608c1 --- /dev/null +++ b/test/utils/interpolate.test.ts @@ -0,0 +1,129 @@ +import { describe, expect, it } from 'bun:test'; +import { + extractVariables, + interpolateTemplate, +} from '../../src/utils/interpolate'; + +describe('interpolateTemplate', () => { + interface TestCase { + input: string; + variables?: Record; + expectedVal: string; + expectedErr?: string; + } + + const testCases: TestCase[] = [ + { input: 'abc', variables: undefined, expectedVal: 'abc' }, + { input: '', variables: undefined, expectedVal: '' }, + { + input: 'this is a {test}', + variables: { test: 'TEST' }, + expectedVal: 'this is a TEST', + }, + { + input: 'this is a {test} {notfound}', + variables: { test: 'TEST' }, + expectedVal: 'this is a TEST ', + }, + { + input: 'this is a {test:-notfound}', + variables: { foo: 'TEST' }, + expectedVal: 'this is a -notfound', + }, + { + input: 'this is a {test:-fail}', + variables: { test: 'TEST' }, + expectedVal: 'this is a TEST', + }, + { + input: 'this is a {test}', + variables: { test: 123 }, + expectedVal: 'this is a 123', + }, + { + input: 'this is a {test}', + variables: { test: null }, + expectedVal: 'this is a ', + }, + { + input: 'this is a {test}', + variables: { test: '' }, + expectedVal: 'this is a ', + }, + { + input: 'this is a {!test}', + variables: { test: '' }, + expectedVal: '', + expectedErr: "Required variable 'test' not provided", + }, + { + input: 'this is a {!test}', + variables: { test: null }, + expectedVal: '', + expectedErr: "Required variable 'test' not provided", + }, + { + input: 'this is a ${test}', + variables: { test: null }, + expectedVal: 'this is a $', + }, + { + input: 'this is a ${test:-foo}', + variables: { test: 'foo' }, + expectedVal: 'this is a $foo', + }, + { + input: 'this is a {{test}}', + variables: { test: 'foo' }, + expectedVal: 'this is a foo', + }, + { + input: 'this is a {{test2:-foo}}', + variables: { test: 'foo' }, + expectedVal: 'this is a -foo', + }, + ]; + + for (const tc of testCases) { + it(`should handle "${tc.input}" with variables ${JSON.stringify(tc.variables)}`, () => { + if (tc.expectedErr) { + expect(() => interpolateTemplate(tc.input, tc.variables)).toThrow( + tc.expectedErr + ); + } else { + const result = interpolateTemplate(tc.input, tc.variables); + expect(result).toBe(tc.expectedVal); + } + }); + } +}); + +describe('extractVariables', () => { + interface TestCase { + input: string; + expected: string[]; + } + + const testCases: TestCase[] = [ + { input: 'abc', expected: [] }, + { input: '', expected: [] }, + { input: 'this is a {test}', expected: ['test'] }, + { input: 'this is a {test} {notfound}', expected: ['test', 'notfound'] }, + { input: 'this is a {test:-notfound}', expected: ['test'] }, + { input: 'this is a {!test}', expected: ['test'] }, + { input: 'this is a {!test:-default}', expected: ['test'] }, + { input: 'this is a {{test}}', expected: ['test'] }, + { input: 'this is a {{test2:-foo}}', expected: ['test2'] }, + { + input: 'multiple {var1} and {var2} and {var1} again', + expected: ['var1', 'var2'], + }, + ]; + + for (const tc of testCases) { + it(`should extract variables from "${tc.input}"`, () => { + const result = extractVariables(tc.input); + expect(result).toEqual(tc.expected); + }); + } +}); From 7708d3a3cb16234b25f1912f455c8cee4bcc293e Mon Sep 17 00:00:00 2001 From: Bobby Christopher Date: Thu, 2 Oct 2025 09:41:21 -0400 Subject: [PATCH 8/8] version bump --- CHANGELOG.md | 6 ++++++ README.md | 22 ++++++++++++++++++++++ package-lock.json | 6 +++--- package.json | 2 +- 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a57be29d..3853139f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # @agentuity/sdk Changelog +## 0.0.148 + +### Patch Changes + +- added experimental support for prompts + ## 0.0.147 ### Patch Changes diff --git a/README.md b/README.md index ca666511..b1734d53 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,28 @@ bun test npm test ``` +## Making Changes + +To make changes for this repo, do the following in your PR: + +```bash +npm run changeset +``` + +And if you plan to release in the same PR, run: + +```bash +npm run version +``` + +Also run (only if you ran `npm run version`): + +```bash +npm install +``` + +Then commit all files in the PR. When you merge the PR it will release the SDK. + ## License See the [LICENSE](LICENSE.md) file for details. diff --git a/package-lock.json b/package-lock.json index c86f2a94..d040ab0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@agentuity/sdk", - "version": "0.0.147", + "version": "0.0.148", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@agentuity/sdk", - "version": "0.0.147", + "version": "0.0.148", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.9.0", @@ -9611,4 +9611,4 @@ } } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 7b7968cf..ac50b3c4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@agentuity/sdk", - "version": "0.0.147", + "version": "0.0.148", "description": "The Agentuity SDK for NodeJS and Bun", "license": "Apache-2.0", "public": true,