From a3d98afdfef8c9ce7135676bdaf703f89893e615 Mon Sep 17 00:00:00 2001 From: doria <93405247+dr-frmr@users.noreply.github.com> Date: Thu, 9 Oct 2025 15:44:21 -0400 Subject: [PATCH] Add updated get_context to SDKs (#231) * feat: add optional JWT and webhook secrets to honcho instance creation * chore: ignore spurious warnings * feat: add response format if using gpt-5 model family * feat: add response models to all apis except anthropic * fix: raise NotImplementedError for response models in AsyncAnthropic client * chore: address review * [WIP] representation structure + deriver cleanup * chore: add tests, cleanup * feat: [WIP: semi-working] representation object * fix: alignment * fix: make observations hashable for dedup * fix: datetime formatting, observation counting * fix: switch to int for message id, clean up representation * feat: remove need for metadata working rep * chore: cleanup * fix: use tenacity instead of custom fns * feat: add representation and card to context if desired * feat: add semantically relevant observations * fix: pass all params to streaming, nonblocking streaming * feat: consolidate document saving, make working representation fetching much smarter * chore: add 100% test coverage of representation util * feat: basic dream infra * feat: dream queue item first pass * chore: fixes & cleanup from coderabbit * fix: dreams scheduled when new document count reaches a certain threshold * feat: wip: timed dreams (not working) * fix: test * fix: remove useless pyright ignore * fix: executing dreams * feat: dreaming * feat: [WIP] longmemeval bench * feat: add USE_PEER_CARD setting, fix longmem test driver * feat: get full working rep for dialectic in one swoop -- fix representation_from_documents to use the proper timestamp! * fix: timestamps for real, handle assistant qs in longmem * fix: remove old client, add batching to longmem * perf: remove duplicate detection, will move to background task * feat: track perf metrics on evals * feat: adjust deriver prompt to use peer_id, add question date to question, clean up deriver * fix: label metrics by task for better perf trace * chore: code review * feat: add efficiency score to longmem bench * chore: tuning and cleaning up eval * chore: bring in the big prompts * feat: add support for vllm client * feat: perf: bundle db calls in deriver and dialectic, increase max conns in docker db * feat: [WIP] realtime context object note: must download custom stainless API for SDK * feat: add merge-sessions flag to longmemeval, add SUMMARY_ENABLED flag * fix: COLLECT_METRICS default false * chore: display start/end message ids, don't include in metrics * fix: break large messages apart for eval * fix: only get/create collection when needed * feat: properly attribute documents with message id ranges and add session name column to documents * fix: revert move of get_or_create_collection (need for fkey) * fix: always get collection with peer name even if it's none * chore: coderabbit * fix: bug in get context feat: get context updates in ts sdk * feat: viz * chore: update honcho-ai/core, remove WIPs * fix: consistent ordering, comment nits, removed excess dreamer init * fix: test int->str * fix: Add validation and update async python client * fix: add validation for last_user_message as well * fix: add deeper validation to getContext in typescript sdk * fix: let session context take a Message object for lastUserMessage to match python sdk behavior * fix: use PeerIdSchema * fix: allow peer object as argument * fix: lastUserMessage min length 1 --------- Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com> --- sdks/python/pyproject.toml | 2 +- .../python/src/honcho/async_client/session.py | 46 +++++++- sdks/python/src/honcho/session.py | 44 +++++++- sdks/python/src/honcho/session_context.py | 58 +++++++++- .../__tests__/session_context.test.ts | 2 +- sdks/typescript/bun.lock | 4 +- sdks/typescript/package.json | 2 +- sdks/typescript/src/session.ts | 105 ++++++++++++++++-- sdks/typescript/src/session_context.ts | 82 +++++++++++--- sdks/typescript/src/validation.ts | 55 +++++++-- tests/bench/harness.py | 7 +- uv.lock | 10 +- 12 files changed, 365 insertions(+), 52 deletions(-) diff --git a/sdks/python/pyproject.toml b/sdks/python/pyproject.toml index 669159b8..9d8c7544 100644 --- a/sdks/python/pyproject.toml +++ b/sdks/python/pyproject.toml @@ -8,7 +8,7 @@ authors = [ { name = "Plastic Labs", email = "hello@plasticlabs.ai" }, ] dependencies = [ - "honcho-core>=1.4.1-alpha.0", + "honcho-core>=1.5.0", "httpx>=0.28.0, <1", "pydantic>=2.0.0, <3", "typing-extensions>=4.12.0; python_version < \"3.12\"", diff --git a/sdks/python/src/honcho/async_client/session.py b/sdks/python/src/honcho/async_client/session.py index fb2bea00..3610367e 100644 --- a/sdks/python/src/honcho/async_client/session.py +++ b/sdks/python/src/honcho/async_client/session.py @@ -427,11 +427,23 @@ class AsyncSession(BaseModel): tokens: int | None = Field( None, gt=0, description="Maximum number of tokens to include in the context" ), + peer_target: str | None = Field( + None, + description="A peer ID to get context for. If given *without* `peer_perspective`, a representation and peer card will be included from the omniscient Honcho-level view of `peer_target`. If given *with* `peer_perspective`, will get the representation and card for `peer_target` *from the perspective of `peer_perspective`*.", + ), + last_user_message: str | Message | None = Field( + None, + description="The most recent message (string or Message object), used to fetch semantically relevant observations and returned as part of the context object. Use this alongside `peer_target` to get a more focused context -- does nothing if `peer_target` is not provided.", + ), + peer_perspective: str | None = Field( + None, + description="A peer ID to get context *from the perspective of*. If given, response will attempt to include representation and card from the perspective of `peer_perspective`. Must be provided with `peer_target`.", + ), ) -> SessionContext: """ Get optimized context for this session within a token limit. - Makes an async API call to retrieve a curated list of messages that provides + Makes an API call to retrieve a curated list of messages that provides optimal context for the conversation while staying within the specified token limit. Uses tiktoken for token counting, so results should be compatible with OpenAI models. @@ -440,6 +452,9 @@ class AsyncSession(BaseModel): summary: Whether to include summary information tokens: Maximum number of tokens to include in the context. Will default to Honcho server configuration if not provided. + peer_target: A peer ID to get context for. If given *without* `peer_perspective`, a representation and peer card will be included from the omniscient Honcho-level view of `peer_target`. If given *with* `peer_perspective`, will get the representation and card for `peer_target` *from the perspective of `peer_perspective`*. + last_user_message: The most recent message (string or Message object), used to fetch semantically relevant observations and returned as part of the context object. Use this alongside `peer_target` to get a more focused context -- does nothing if `peer_target` is not provided. + peer_perspective: A peer ID to get context *from the perspective of*. If given, response will attempt to include representation and card from the perspective of `peer_perspective`. Must be provided with `peer_target`. Returns: A SessionContext object containing the optimized message history and @@ -450,11 +465,32 @@ class AsyncSession(BaseModel): Token counting is performed using tiktoken. For models using different tokenizers, you may need to adjust the token limit accordingly. """ + + if peer_target is None and peer_perspective is not None: + raise ValueError( + "You must provide a `peer_target` when `peer_perspective` is provided" + ) + + if peer_target is None and last_user_message is not None: + raise ValueError( + "You must provide a `peer_target` when `last_user_message` is provided" + ) + + last_user_message_id = ( + last_user_message.id + if isinstance(last_user_message, Message) + else last_user_message + ) context = await self._client.workspaces.sessions.get_context( session_id=self.id, workspace_id=self.workspace_id, tokens=tokens if tokens is not None else omit, summary=summary, + last_message=last_user_message_id + if last_user_message_id is not None + else omit, + peer_target=peer_target if peer_target is not None else omit, + peer_perspective=peer_perspective if peer_perspective is not None else omit, ) # Convert the honcho_core summary to our Summary if it exists @@ -469,7 +505,13 @@ class AsyncSession(BaseModel): ) return SessionContext( - session_id=self.id, messages=context.messages, summary=session_summary + session_id=self.id, + messages=context.messages, + summary=session_summary, + peer_representation=str(context.peer_representation) + if context.peer_representation + else None, + peer_card=context.peer_card, ) async def get_summaries(self) -> SessionSummaries: diff --git a/sdks/python/src/honcho/session.py b/sdks/python/src/honcho/session.py index 193887b5..51a7f20c 100644 --- a/sdks/python/src/honcho/session.py +++ b/sdks/python/src/honcho/session.py @@ -407,6 +407,18 @@ class Session(BaseModel): tokens: int | None = Field( None, gt=0, description="Maximum number of tokens to include in the context" ), + peer_target: str | None = Field( + None, + description="A peer ID to get context for. If given *without* `peer_perspective`, a representation and peer card will be included from the omniscient Honcho-level view of `peer_target`. If given *with* `peer_perspective`, will get the representation and card for `peer_target` *from the perspective of `peer_perspective`*.", + ), + last_user_message: str | Message | None = Field( + None, + description="The most recent message (string or Message object), used to fetch semantically relevant observations and returned as part of the context object. Use this alongside `peer_target` to get a more focused context -- does nothing if `peer_target` is not provided.", + ), + peer_perspective: str | None = Field( + None, + description="A peer ID to get context *from the perspective of*. If given, response will attempt to include representation and card from the perspective of `peer_perspective`. Must be provided with `peer_target`.", + ), ) -> SessionContext: """ Get optimized context for this session within a token limit. @@ -420,6 +432,9 @@ class Session(BaseModel): summary: Whether to include summary information tokens: Maximum number of tokens to include in the context. Will default to Honcho server configuration if not provided. + peer_target: A peer ID to get context for. If given *without* `peer_perspective`, a representation and peer card will be included from the omniscient Honcho-level view of `peer_target`. If given *with* `peer_perspective`, will get the representation and card for `peer_target` *from the perspective of `peer_perspective`*. + last_user_message: The most recent message (string or Message object), used to fetch semantically relevant observations and returned as part of the context object. Use this alongside `peer_target` to get a more focused context -- does nothing if `peer_target` is not provided. + peer_perspective: A peer ID to get context *from the perspective of*. If given, response will attempt to include representation and card from the perspective of `peer_perspective`. Must be provided with `peer_target`. Returns: A SessionContext object containing the optimized message history and @@ -430,11 +445,32 @@ class Session(BaseModel): Token counting is performed using tiktoken. For models using different tokenizers, you may need to adjust the token limit accordingly. """ + + if peer_target is None and peer_perspective is not None: + raise ValueError( + "You must provide a `peer_target` when `peer_perspective` is provided" + ) + + if peer_target is None and last_user_message is not None: + raise ValueError( + "You must provide a `peer_target` when `last_user_message` is provided" + ) + + last_user_message_id = ( + last_user_message.id + if isinstance(last_user_message, Message) + else last_user_message + ) context = self._client.workspaces.sessions.get_context( session_id=self.id, workspace_id=self.workspace_id, tokens=tokens if tokens is not None else omit, summary=summary, + last_message=last_user_message_id + if last_user_message_id is not None + else omit, + peer_target=peer_target if peer_target is not None else omit, + peer_perspective=peer_perspective if peer_perspective is not None else omit, ) # Convert the honcho_core summary to our Summary if it exists @@ -449,7 +485,13 @@ class Session(BaseModel): ) return SessionContext( - session_id=self.id, messages=context.messages, summary=session_summary + session_id=self.id, + messages=context.messages, + summary=session_summary, + peer_representation=str(context.peer_representation) + if context.peer_representation + else None, + peer_card=context.peer_card, ) def get_summaries(self) -> SessionSummaries: diff --git a/sdks/python/src/honcho/session_context.py b/sdks/python/src/honcho/session_context.py index 45017075..0c657fe1 100644 --- a/sdks/python/src/honcho/session_context.py +++ b/sdks/python/src/honcho/session_context.py @@ -58,6 +58,14 @@ class SessionContext(BaseModel): summary: Summary | None = Field( None, description="Summary of the session history prior to the message cutoff" ) + peer_representation: str | None = Field( + None, + description="The peer representation, if context is requested from a specific perspective", + ) + peer_card: list[str] | None = Field( + None, + description="The peer card, if context is requested from a specific perspective", + ) @validate_call def __init__( @@ -72,6 +80,14 @@ class SessionContext(BaseModel): None, description="Summary of the session history prior to the message cutoff", ), + peer_representation: str | None = Field( + None, + description="The peer representation, if context is requested from a specific perspective", + ), + peer_card: list[str] | None = Field( + None, + description="The peer card, if context is requested from a specific perspective", + ), ) -> None: """ Initialize a new SessionContext. @@ -84,6 +100,8 @@ class SessionContext(BaseModel): session_id=session_id, messages=messages, summary=summary, + peer_representation=peer_representation, + peer_card=peer_card, ) def to_openai( @@ -117,14 +135,30 @@ class SessionContext(BaseModel): } for message in self.messages ] + system_messages: list[dict[str, str]] = [] + + if self.peer_representation: + peer_representation_message = { + "role": "system", + "content": f"{self.peer_representation}", + } + system_messages.append(peer_representation_message) + + if self.peer_card: + peer_card_message = { + "role": "system", + "content": f"{self.peer_card}", + } + system_messages.append(peer_card_message) if self.summary: summary_message = { "role": "system", "content": f"{self.summary.content}", } - return [summary_message, *messages] - return messages + system_messages.append(summary_message) + + return system_messages + messages def to_anthropic( self, @@ -164,14 +198,30 @@ class SessionContext(BaseModel): } for message in self.messages ] + system_messages: list[dict[str, str]] = [] + + if self.peer_representation: + peer_representation_message = { + "role": "user", + "content": f"{self.peer_representation}", + } + system_messages.append(peer_representation_message) + + if self.peer_card: + peer_card_message = { + "role": "user", + "content": f"{self.peer_card}", + } + system_messages.append(peer_card_message) if self.summary: summary_message = { "role": "user", "content": f"{self.summary.content}", } - return [summary_message, *messages] - return messages + system_messages.append(summary_message) + + return system_messages + messages def __len__(self) -> int: """ diff --git a/sdks/typescript/__tests__/session_context.test.ts b/sdks/typescript/__tests__/session_context.test.ts index 9c95dbce..5865c765 100644 --- a/sdks/typescript/__tests__/session_context.test.ts +++ b/sdks/typescript/__tests__/session_context.test.ts @@ -23,7 +23,7 @@ function createTestMessage(id: string, content: string, peer_id: string, additio function createTestSummary(content: string): Summary { return new Summary({ content, - message_id: 1, + message_id: "1", summary_type: 'short', created_at: new Date().toISOString(), token_count: content.length diff --git a/sdks/typescript/bun.lock b/sdks/typescript/bun.lock index 7ec73bd1..b4ad630e 100644 --- a/sdks/typescript/bun.lock +++ b/sdks/typescript/bun.lock @@ -4,7 +4,7 @@ "": { "name": "@honcho-ai/sdk", "dependencies": { - "@honcho-ai/core": "^1.4.1-alpha.0", + "@honcho-ai/core": "^1.5.0", "@types/node": "^24.0.1", "zod": "4.0.0", }, @@ -108,7 +108,7 @@ "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], - "@honcho-ai/core": ["@honcho-ai/core@1.4.1-alpha.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-cRp3ug8cBe9E7NZDgCXkI0EHIgm8x5Jq/nQB0xA5629GuniS99yq6M786I//FjOZhB2YEZoOxd4Zkg412uk5Yg=="], + "@honcho-ai/core": ["@honcho-ai/core@1.5.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-kjYhCO0S9Ll3DfVpK8MetjY1PlD9FG6QGtudlMnSwfTWGzXFjL/OafpoE3SZ3c9BVY48JIbW9IttFUZFekv/xg=="], "@istanbuljs/load-nyc-config": ["@istanbuljs/load-nyc-config@1.1.0", "", { "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", "get-package-type": "^0.1.0", "js-yaml": "^3.13.1", "resolve-from": "^5.0.0" } }, "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ=="], diff --git a/sdks/typescript/package.json b/sdks/typescript/package.json index 1e9750d3..803b9d23 100644 --- a/sdks/typescript/package.json +++ b/sdks/typescript/package.json @@ -20,7 +20,7 @@ "test:coverage": "jest --coverage" }, "dependencies": { - "@honcho-ai/core": "^1.4.1-alpha.0", + "@honcho-ai/core": "^1.5.0", "@types/node": "^24.0.1", "zod": "4.0.0" }, diff --git a/sdks/typescript/src/session.ts b/sdks/typescript/src/session.ts index 6bc3e16f..aae0e309 100644 --- a/sdks/typescript/src/session.ts +++ b/sdks/typescript/src/session.ts @@ -451,10 +451,21 @@ export class Session { * compatible with OpenAI models. The context optimization balances * recency and relevance to provide the best conversational context. * - * @param summary - Whether to include summary information in the context. - * When true, includes session summary if available. Defaults to true - * @param tokens - Maximum number of tokens to include in the context. If not provided, - * uses the server's default configuration + * @param options - Configuration options for context retrieval + * @param options.summary - Whether to include summary information in the context. + * When true, includes session summary if available. Defaults to true + * @param options.tokens - Maximum number of tokens to include in the context. If not provided, + * uses the server's default configuration + * @param options.peerTarget - The target of the perspective. If given without `peerPerspective`, + * will get the Honcho-level representation and peer card for this peer. + * If given with `peerPerspective`, will get the representation and card + * for this peer from the perspective of that peer. + * @param options.lastUserMessage - The most recent message, used to fetch semantically relevant + * observations and returned as part of the context object. + * Can be either a message ID string or a Message object. + * @param options.peerPerspective - A peer to get context for. If given, response will attempt to + * include representation and card from the perspective of that peer. + * Must be provided with `peerTarget`. * @returns Promise resolving to a SessionContext object containing the optimized * message history and summary (if available) that maximizes conversational * context while respecting the token limit @@ -462,25 +473,103 @@ export class Session { * @note Token counting is performed using tiktoken. For models using different * tokenizers, you may need to adjust the token limit accordingly. */ + async getContext( + summary?: boolean, + tokens?: number, + peerTarget?: string | Peer, + lastUserMessage?: string | Message, + peerPerspective?: string | Peer + ): Promise async getContext(options?: { summary?: boolean tokens?: number - }): Promise { + peerTarget?: string | Peer + lastUserMessage?: string | Message + peerPerspective?: string | Peer + }): Promise + async getContext( + summaryOrOptions?: + | boolean + | { + summary?: boolean + tokens?: number + peerTarget?: string | Peer + lastUserMessage?: string | Message + peerPerspective?: string | Peer + }, + tokens?: number, + peerTarget?: string | Peer, + lastUserMessage?: string | Message, + peerPerspective?: string | Peer + ): Promise { + // Normalize positional arguments into options object + let options: { + summary?: boolean + tokens?: number + peerTarget?: string + lastUserMessage?: string + peerPerspective?: string + } + + if ( + typeof summaryOrOptions === 'boolean' || + (summaryOrOptions === undefined && arguments.length > 1) + ) { + // Positional arguments pattern + options = { + summary: summaryOrOptions as boolean | undefined, + tokens, + peerTarget: typeof peerTarget === 'object' ? peerTarget.id : peerTarget, + lastUserMessage: + typeof lastUserMessage === 'string' + ? lastUserMessage + : lastUserMessage?.id, + peerPerspective: + typeof peerPerspective === 'object' + ? peerPerspective.id + : peerPerspective, + } + } else { + // Options object pattern + options = (summaryOrOptions as typeof options) || {} + } + const contextParams = ContextParamsSchema.parse({ - summary: options?.summary, - tokens: options?.tokens, + summary: options.summary, + tokens: options.tokens, + peerTarget: options.peerTarget, + lastUserMessage: options.lastUserMessage, + peerPerspective: options.peerPerspective, }) + + // Extract message ID if lastUserMessage is a Message object + const lastMessageId = + typeof contextParams.lastUserMessage === 'string' + ? contextParams.lastUserMessage + : contextParams.lastUserMessage?.id + const context = await this._client.workspaces.sessions.getContext( this.workspaceId, this.id, { tokens: contextParams.tokens, summary: contextParams.summary, + last_message: lastMessageId, + peer_target: contextParams.peerTarget, + peer_perspective: contextParams.peerPerspective, } ) // Convert the summary response to Summary object if present const summary = context.summary ? new Summary(context.summary) : null - return new SessionContext(this.id, context.messages, summary) + return new SessionContext( + this.id, + context.messages, + summary, + context.peer_representation + ? JSON.stringify(context.peer_representation) + : null, + context.peer_card ?? null + ) } /** diff --git a/sdks/typescript/src/session_context.ts b/sdks/typescript/src/session_context.ts index 538844c2..6eca21e0 100644 --- a/sdks/typescript/src/session_context.ts +++ b/sdks/typescript/src/session_context.ts @@ -102,21 +102,37 @@ export class SessionContext { */ readonly summary: Summary | null + /** + * The peer representation, if context is requested from a specific perspective. + */ + readonly peerRepresentation: string | null + + /** + * The peer card, if context is requested from a specific perspective. + */ + readonly peerCard: string[] | null + /** * Initialize a new SessionContext. * * @param sessionId ID of the session this context belongs to * @param messages List of Message objects to include in the context * @param summary Summary of the session history prior to the message cutoff + * @param peerRepresentation The peer representation, if context is requested from a specific perspective + * @param peerCard The peer card, if context is requested from a specific perspective */ constructor( sessionId: string, messages: Message[], - summary: Summary | null = null + summary: Summary | null = null, + peerRepresentation: string | null = null, + peerCard: string[] | null = null ) { this.sessionId = sessionId this.messages = messages this.summary = summary + this.peerRepresentation = peerRepresentation + this.peerCard = peerCard } /** @@ -136,18 +152,36 @@ export class SessionContext { assistant: string | Peer ): Array<{ role: string; content: string; name?: string }> { const assistantId = typeof assistant === 'string' ? assistant : assistant.id - const summaryMessage = this.summary - ? { - role: 'system', - content: `${this.summary.content}`, - } - : null const messages = this.messages.map((message) => ({ role: message.peer_id === assistantId ? 'assistant' : 'user', name: message.peer_id, content: message.content, })) - return summaryMessage ? [summaryMessage, ...messages] : messages + + const systemMessages: Array<{ role: string; content: string }> = [] + + if (this.peerRepresentation) { + systemMessages.push({ + role: 'system', + content: `${this.peerRepresentation}`, + }) + } + + if (this.peerCard) { + systemMessages.push({ + role: 'system', + content: `${this.peerCard}`, + }) + } + + if (this.summary) { + systemMessages.push({ + role: 'system', + content: `${this.summary.content}`, + }) + } + + return [...systemMessages, ...messages] } /** @@ -170,12 +204,6 @@ export class SessionContext { assistant: string | Peer ): Array<{ role: string; content: string }> { const assistantId = typeof assistant === 'string' ? assistant : assistant.id - const summaryMessage = this.summary - ? { - role: 'user', - content: `${this.summary.content}`, - } - : null const messages = this.messages.map((message) => message.peer_id === assistantId ? { @@ -187,7 +215,31 @@ export class SessionContext { content: `${message.peer_id}: ${message.content}`, } ) - return summaryMessage ? [summaryMessage, ...messages] : messages + + const systemMessages: Array<{ role: string; content: string }> = [] + + if (this.peerRepresentation) { + systemMessages.push({ + role: 'user', + content: `${this.peerRepresentation}`, + }) + } + + if (this.peerCard) { + systemMessages.push({ + role: 'user', + content: `${this.peerCard}`, + }) + } + + if (this.summary) { + systemMessages.push({ + role: 'user', + content: `${this.summary.content}`, + }) + } + + return [...systemMessages, ...messages] } /** diff --git a/sdks/typescript/src/validation.ts b/sdks/typescript/src/validation.ts index 3c30943b..e5e405c8 100644 --- a/sdks/typescript/src/validation.ts +++ b/sdks/typescript/src/validation.ts @@ -1,3 +1,4 @@ +import type { Message } from '@honcho-ai/core/resources/workspaces/sessions/messages' import { z } from 'zod' /** @@ -123,16 +124,56 @@ export const ChatQuerySchema = z.object({ sessionId: z.string().optional(), }) +/** + * Schema for validating Message objects from the core SDK. + */ +const MessageSchema: z.ZodType = z.object({ + id: z.string(), + content: z.string(), + created_at: z.string(), + peer_id: z.string(), + session_id: z.string(), + token_count: z.number(), + workspace_id: z.string(), + metadata: z.record(z.string(), z.unknown()).optional(), +}) as z.ZodType + /** * Schema for context retrieval parameters. */ -export const ContextParamsSchema = z.object({ - summary: z.boolean().optional(), - tokens: z - .number() - .positive('Token limit must be a positive number') - .optional(), -}) +export const ContextParamsSchema = z + .object({ + summary: z.boolean().optional(), + tokens: z + .number() + .positive('Token limit must be a positive number') + .optional(), + lastUserMessage: z + .union([ + z.string().min(1, 'Last user message must be a non-empty string'), + MessageSchema, + ]) + .optional(), + peerTarget: PeerIdSchema.optional(), + peerPerspective: PeerIdSchema.optional(), + }) + .superRefine((data, ctx) => { + if (data.lastUserMessage && !data.peerTarget) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'peerTarget is required when lastUserMessage is provided', + path: ['lastUserMessage'], + }) + } + + if (data.peerPerspective && !data.peerTarget) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'peerTarget is required when peerPerspective is provided', + path: ['peerPerspective'], + }) + } + }) /** * Schema for deriver status options. diff --git a/tests/bench/harness.py b/tests/bench/harness.py index f7f235a0..321c79a3 100755 --- a/tests/bench/harness.py +++ b/tests/bench/harness.py @@ -436,11 +436,8 @@ try: print_settings(value, full_key, max_depth, current_depth + 1) else: # Mask sensitive information - if isinstance(value, str) and any(sensitive in value.lower() for sensitive in ['password', 'secret', 'key', 'token']): - if 'testpwd' in value: - masked_value = value.replace('testpwd', '***') - else: - masked_value = '***' + if isinstance(full_key, str) and any(sensitive in full_key.lower() for sensitive in ['password', 'secret', 'key', 'uri']): + masked_value = '*' * len(value) if value else 'None' else: masked_value = value print(f" {{key}}: {{masked_value}}") diff --git a/uv.lock b/uv.lock index a8615c45..95765dbb 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.13'", @@ -780,7 +780,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "honcho-core", specifier = ">=1.4.1a0" }, + { name = "honcho-core", specifier = ">=1.5.0" }, { name = "httpx", specifier = ">=0.28.0,<1" }, { name = "pydantic", specifier = ">=2.0.0,<3" }, { name = "typing-extensions", marker = "python_full_version < '3.12'", specifier = ">=4.12.0" }, @@ -791,7 +791,7 @@ dev = [{ name = "ruff", specifier = ">=0.11.13" }] [[package]] name = "honcho-core" -version = "1.4.1a0" +version = "1.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -801,9 +801,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2d/2f/fbf3ca017ffcdaef92e26fe84b77268c1c0b26f95e282d8684f50932d81a/honcho_core-1.4.1a0.tar.gz", hash = "sha256:e0023f46c65500d727e3fb1275781ae1e0b162c226a01352afd45c421c5f350b", size = 130826, upload-time = "2025-10-01T03:23:27.143Z" } +sdist = { url = "https://files.pythonhosted.org/packages/75/cd/05d2a21afd037673637e390de411cd66ca56b07e524cd7ead65c655a2b49/honcho_core-1.5.0.tar.gz", hash = "sha256:4876195dad16db437117d40a1d5e34ff88974e8eca6d093f3653b4ac2bda3c6d", size = 132236, upload-time = "2025-10-08T18:30:15.777Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/5f/2aa746b7fd92690ef782dcacc56a5c47a19a999b9729597c04c7cc509e52/honcho_core-1.4.1a0-py3-none-any.whl", hash = "sha256:8b4c86e3ac3964ff56fe6a449b9fa23be49530609c34bdf35c9ecde7cd871bba", size = 121704, upload-time = "2025-10-01T03:23:25.66Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4e/9ef193bba00c521be1152791d2ba37a98bdb58ece8f8ce7863b710a4f511/honcho_core-1.5.0-py3-none-any.whl", hash = "sha256:01db345371d6e80230b202c797a73f9f086dc244936522dbc39efb05f4cee306", size = 123210, upload-time = "2025-10-08T18:30:14.277Z" }, ] [[package]]