Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export type {
NamedAgentDispatchRequest,
DispatchReceipt,
DirectAgentPayload,
DirectAgentToolDeclaration,
FluePublicError,
AgentWebSocketClientMessage,
WorkflowWebSocketClientMessage,
Expand Down
21 changes: 17 additions & 4 deletions packages/runtime/src/runtime/flue-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
validateWorkflowRequest,
} from '../errors.ts';
import type { AgentDispatchRequest, CreatedAgent, DispatchReceipt, NamedAgentDispatchRequest } from '../types.ts';
import { enqueueDispatch } from './dispatch.ts';
import type { DispatchQueue } from './dispatch-queue.ts';
import {
type AgentHandler,
type CreateContextFn,
Expand All @@ -27,8 +29,6 @@ import {
type StartWorkflowAdmissionFn,
type WorkflowHandler,
} from './handle-agent.ts';
import type { DispatchQueue } from './dispatch-queue.ts';
import { enqueueDispatch } from './dispatch.ts';
import { type HandleRunRouteOptions, handleRunRouteRequest } from './handle-run-routes.ts';
import { generateWorkflowRunId } from './ids.ts';
import type { RunPointer, RunRegistry } from './run-registry.ts';
Expand All @@ -37,15 +37,15 @@ import type { RunSubscriberRegistry } from './run-subscribers.ts';
import {
AgentInvocationResponseSchema,
AgentRouteParamSchema,
WorkflowInvocationQuerySchema,
WorkflowRouteParamSchema,
ErrorEnvelopeSchema,
RunEventListResponseSchema,
RunEventsQuerySchema,
RunIdParamSchema,
RunRecordSchema,
WorkflowAdmissionResponseSchema,
WorkflowInvocationQuerySchema,
WorkflowInvocationResponseSchema,
WorkflowRouteParamSchema,
} from './schemas.ts';

export interface FlueRuntime {
Expand Down Expand Up @@ -405,6 +405,19 @@ function agentRouteSpec() {
properties: {
message: { type: 'string' },
session: { type: 'string', minLength: 1, pattern: '.*\\S.*' },
tools: {
type: 'array',
items: {
type: 'object',
required: ['name', 'description', 'parameters'],
properties: {
name: { type: 'string', minLength: 1 },
description: { type: 'string', minLength: 1 },
parameters: { type: 'object', additionalProperties: true },
kind: { type: 'string', enum: ['client', 'deferred'] },
},
},
},
},
},
},
Expand Down
46 changes: 40 additions & 6 deletions packages/runtime/src/runtime/handle-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import type { FlueContextInternal } from '../client.ts';
import { InvalidRequestError, parseJsonBody, RunEventTooLargeError, RunStoreUnavailableError, toHttpResponse, toPublicError } from '../errors.ts';
import type { AttachedAgentEvent, AttachedAgentEventCallback, CreatedAgent, DirectAgentPayload, DispatchReceipt, FlueEvent, FlueEventCallback } from '../types.ts';
import type { AttachedAgentEvent, AttachedAgentEventCallback, CreatedAgent, DirectAgentPayload, DirectAgentToolDeclaration, DispatchReceipt, FlueEvent, FlueEventCallback } from '../types.ts';
import type { DispatchInput, DispatchProcessor } from './dispatch-queue.ts';
import { streamActiveRunEvents } from './handle-run-routes.ts';
import { generateWorkflowRunId } from './ids.ts';
Expand All @@ -16,7 +16,7 @@ export type CreatedAgentHandler = CreatedAgent;
export type WorkflowHandler = (ctx: FlueContextInternal) => unknown | Promise<unknown>;

interface DirectRequestSession {
processDirectInput(input: { message: string }): PromiseLike<unknown>;
processDirectInput(input: DirectAgentPayload): PromiseLike<unknown>;
}

interface DispatchSession {
Expand Down Expand Up @@ -105,7 +105,7 @@ export function createDirectAgentHandler(agent: CreatedAgentHandler): AgentHandl
if (!isDirectRequestSession(session)) {
throw new Error('[flue] Internal session does not support direct input processing.');
}
return session.processDirectInput({ message: payload.message });
return session.processDirectInput(payload);
};
}

Expand All @@ -114,18 +114,52 @@ function isDirectRequestSession(value: unknown): value is DirectRequestSession {
}

function parseDirectAgentPayload(payload: unknown): DirectAgentPayload {
const expected = 'Direct agent requests must use JSON object body { "message": string, "session"?: string }.';
const expected = 'Direct agent requests must use JSON object body { "message": string, "session"?: string, "tools"?: DirectAgentToolDeclaration[] }.';
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
throw new InvalidRequestError({ reason: expected });
}
const value = payload as { message?: unknown; session?: unknown };
const value = payload as { message?: unknown; session?: unknown; tools?: unknown };
if (typeof value.message !== 'string') {
throw new InvalidRequestError({ reason: expected });
}
if (value.session !== undefined && (typeof value.session !== 'string' || value.session.trim() === '')) {
throw new InvalidRequestError({ reason: 'Direct agent request "session" must be a non-empty string when provided.' });
}
return { message: value.message, session: value.session };
const tools = parseDirectAgentTools(value.tools);
return tools === undefined
? { message: value.message, session: value.session }
: { message: value.message, session: value.session, tools };
}

function parseDirectAgentTools(rawTools: unknown): DirectAgentToolDeclaration[] | undefined {
if (rawTools === undefined) return undefined;
if (!Array.isArray(rawTools)) {
throw new InvalidRequestError({ reason: 'Direct agent request "tools" must be an array when provided.' });
}
return rawTools.map((rawTool, index) => {
if (!rawTool || typeof rawTool !== 'object' || Array.isArray(rawTool)) {
throw new InvalidRequestError({ reason: `Direct agent request tools[${index}] must be a tool declaration object.` });
}
const tool = rawTool as Record<string, unknown>;
if (typeof tool.name !== 'string' || tool.name.trim() === '') {
throw new InvalidRequestError({ reason: `Direct agent request tools[${index}].name must be a non-empty string.` });
}
if (typeof tool.description !== 'string' || tool.description.trim() === '') {
throw new InvalidRequestError({ reason: `Direct agent request tools[${index}].description must be a non-empty string.` });
}
if (!tool.parameters || typeof tool.parameters !== 'object' || Array.isArray(tool.parameters)) {
throw new InvalidRequestError({ reason: `Direct agent request tools[${index}].parameters must be a JSON Schema object.` });
}
if (tool.kind !== undefined && tool.kind !== 'client' && tool.kind !== 'deferred') {
throw new InvalidRequestError({ reason: `Direct agent request tools[${index}].kind must be "client" or "deferred" when provided.` });
}
return {
name: tool.name,
description: tool.description,
parameters: tool.parameters as Record<string, unknown>,
kind: tool.kind as DirectAgentToolDeclaration['kind'],
};
});
}

/**
Expand Down
32 changes: 25 additions & 7 deletions packages/runtime/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import type { AgentMessage, AgentTool, AgentToolResult, StreamFn } from '@earendil-works/pi-agent-core';
import { Agent } from '@earendil-works/pi-agent-core';
import { streamSimple } from '@earendil-works/pi-ai';
import type {
AssistantMessage,
ImageContent,
Expand All @@ -11,6 +10,7 @@ import type {
ToolResultMessage,
UserMessage,
} from '@earendil-works/pi-ai';
import { streamSimple } from '@earendil-works/pi-ai';
import type * as v from 'valibot';
import { abortErrorFor, createCallHandle } from './abort.ts';
import {
Expand Down Expand Up @@ -42,22 +42,25 @@ import {
type ResultToolBundle,
ResultUnavailableError,
} from './result.ts';
import type { DispatchInput } from './runtime/dispatch-queue.ts';
import { generateOperationId, generateTurnId } from './runtime/ids.ts';
import { getProviderConfiguration, getRegisteredApiKey } from './runtime/providers.ts';
import { createFlueFs } from './sandbox.ts';

import type {
AgentConfig,
AgentProfile,
BranchSummaryEntry,
CallHandle,
CompactionEntry,
DirectAgentPayload,
DirectAgentToolDeclaration,
DispatchMessageMetadata,
FlueEvent,
FlueEventCallback,
FlueFs,
FlueSession,
MessageEntry,
PackagedSkillDirectory,
PromptModel,
PromptOptions,
PromptResponse,
Expand All @@ -70,14 +73,12 @@ import type {
SessionToolFactory,
ShellOptions,
ShellResult,
PackagedSkillDirectory,
SkillReference,
SkillOptions,
SkillReference,
TaskOptions,
ThinkingLevel,
ToolDefinition,
} from './types.ts';
import type { DispatchInput } from './runtime/dispatch-queue.ts';
import { addUsage, emptyUsage, fromProviderUsage } from './usage.ts';

const MAX_TASK_DEPTH = 4;
Expand Down Expand Up @@ -208,6 +209,23 @@ function resolveResultOption(
return options.schema;
}

function createDirectAgentTools(
declarations: DirectAgentToolDeclaration[] | undefined,
): ToolDefinition[] | undefined {
if (declarations === undefined) return undefined;
return declarations.map((tool) => ({
name: tool.name,
description: tool.description,
parameters: tool.parameters,
async execute() {
throw new Error(
`[flue] Direct agent tool "${tool.name}" is declaration-only. ` +
'Client/deferred tool execution and resume is not implemented for direct-agent payload tools yet.',
);
},
}));
}

interface InternalTaskOptions<S extends v.GenericSchema | undefined> extends TaskOptions<S> {
inheritedModel?: string;
inheritedThinkingLevel?: ThinkingLevel;
Expand Down Expand Up @@ -766,12 +784,12 @@ export class Session implements FlueSession {
);
}

processDirectInput(input: { message: string }): CallHandle<PromptResponse> {
processDirectInput(input: DirectAgentPayload): CallHandle<PromptResponse> {
return createCallHandle(undefined, (signal) =>
this.runOperation('prompt', signal, () => this.runPromptCall({
promptText: input.message,
schema: undefined,
tools: undefined,
tools: createDirectAgentTools(input.tools),
model: undefined,
thinkingLevel: undefined,
images: undefined,
Expand Down
14 changes: 14 additions & 0 deletions packages/runtime/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,20 @@ export interface DispatchReceipt {
export interface DirectAgentPayload {
message: string;
session?: string;
tools?: DirectAgentToolDeclaration[];
}

/**
* JSON-serializable tool declaration for a single direct-agent interaction.
*
* These declarations expose caller-scoped tools to the model for the prompt.
* They do not provide durable browser/client resume semantics by themselves.
*/
export interface DirectAgentToolDeclaration {
name: string;
description: string;
parameters: Record<string, unknown>;
kind?: 'client' | 'deferred';
}

export interface FluePublicError {
Expand Down
Loading
Loading