Skip to main content
Wildo.ai Coming soon

In-app assistant

Keep a conversation going

Stream answers as they arrive and retain the conversation in the application.

A conversation passes through an agent and returns an answer in successive pieces.

Keep a conversation going

An in-app assistant needs more than a text box. Wildo streams its answer as it arrives, keeps previous turns in the application and lets the person continue the same discussion.

You define the assistant’s purpose, model and available tools. The conversation runtime handles the exchange and retrieves its history from the server, so a browser does not have to reconstruct the model’s memory.

Example — Continue a discussion about current work

A person asks which tasks need attention, then asks about one of the results. The second turn belongs to the same conversation and can use its earlier messages. Resource tools supply current records when the answer needs fresh data.

For engineers

This connected example adapts Wonder Todos to one narrow workflow: read a task, retrieve policy passages and request a reviewed rename. It deliberately omits the reference assistant’s list and create tools. First complete the backend provider setup; this agent needs an enabled Anthropic connection and credentials in that runtime.

The agent, system and registry below share the same references. The policy search is the engine’s retrievalTool, scoped to the knowledge documents and explained below. Every import uses the package roots:

import { z } from 'zod';
import {
  resourceReadTool, resourceWriteTool, retrievalTool, RETRIEVAL_FUNCTION_REF,
  type FlowActorSystem_InitializationFactoryMap, type FunctionTool,
} from '@wildo-ai/saas-backend-lib';
import {
  Actor_ExecutionMode, Actor_Role_Category, CoreResourceOperation,
  FlowActorSystem_Kind, FlowActorSystem_Status, FlowActorSystem_Visibility,
  FlowsActors_Agent_Skill_Type, FlowsActors_Agent_Status,
  ResourceOperationRiskLevel, ResourcePrimaryScope,
  type ActorSystem, type FlowsActors_Agent,
} from '@wildo-ai/saas-models';
import {
  LLMProvider_ChatCompletionInputSchema, LLMProvider_ChatCompletionOutputSchema,
  LLMProvider_OperationContext_BaseSchema, LLMProvider_OperationKey, LLM_Model,
  type LLMProvider_OperationDefinitionBase,
} from '@wildo-ai/external-connectors-models';
import { TasksManager_ResourceType } from '@wonder-todos/shared-lib';

const TODO_ASSISTANT_CHAT_AGENT_REF = 'todo-assistant.chat.agent';
const TODO_ASSISTANT_SYSTEM_REF = 'todo-assistant';
const readTask = resourceReadTool(TasksManager_ResourceType.TODOS);
const searchPolicies = retrievalTool({ resourceTypes: [TasksManager_ResourceType.KNOWLEDGE_DOCUMENTS] });
const renameTask = resourceWriteTool(TasksManager_ResourceType.TODOS, CoreResourceOperation.UPDATE, {
  requiresHumanApproval: true,
  inputSchema: z.object({
    id: z.string().min(1).describe('Task id returned by the read tool.'),
    title: z.string().min(1).max(200).describe('The new title explicitly requested by the user.'),
  }),
});

The rename tool exposes only the record ID and new title. Its approval flag pauses that particular call; approval does not replace the operation’s access checks. Reusing the tools’ functionRef values below prevents a separate list of hand-typed names drifting away from the registry.

const todoAssistantChatAgent: FlowsActors_Agent = {
  ref: TODO_ASSISTANT_CHAT_AGENT_REF,
  displayName: 'Todo Assistant',
  status: FlowsActors_Agent_Status.ACTIVE,
  config: {
    providerRef: 'anthropic',
    model: LLM_Model.CLAUDE_SONNET_5,
    operationKey: LLMProvider_OperationKey.CHAT_COMPLETION,
    accessControl: [{ primaryScope: ResourcePrimaryScope.APPLICATION, requiresApproval: false }],
    riskLevel: ResourceOperationRiskLevel.LOW,
  },
  operationDefinition: {
    operationKey: LLMProvider_OperationKey.CHAT_COMPLETION,
    providerRef: 'anthropic',
    inputSchema: LLMProvider_ChatCompletionInputSchema,
    outputSchema: LLMProvider_ChatCompletionOutputSchema,
    operationContextSchema: LLMProvider_OperationContext_BaseSchema,
  } satisfies LLMProvider_OperationDefinitionBase,
  promptSpec: {
    role: 'Help the user understand policy documents and rename their tasks when asked.',
    skillType: FlowsActors_Agent_Skill_Type.CHAT,
    instructions: [
      `Use ${readTask.functionRef} to resolve an existing task before proposing a rename.`,
      `Use ${RETRIEVAL_FUNCTION_REF} for policy questions. Read outcome and notes before answering.`,
      'For matched results, answer only from the returned passages and name their documents.',
      'For nothing-matched, say no relevant passages were found. For refused, explain access was refused; do not claim the information is absent.',
      'If notes report withheld or truncated material, explain that the answer covers only the returned material.',
      `Call ${renameTask.functionRef} only for an explicitly requested rename. Its approval request is the review step.`,
      'Report a rename as completed only after the tool returns a successful result.',
    ],
  },
  capabilities: {
    tools: { functionRefs: [readTask.functionRef, RETRIEVAL_FUNCTION_REF, renameTask.functionRef], mcpRefs: [] },
  },
  createdAt: '2026-07-12T00:00:00.000Z',
};

The prompt gives the tools their conversational purpose. The tool schemas, execution context and operation checks enforce the application contract; instructions alone do not establish access control. A refused search and an empty search require different answers even though both can return no sources.

Connect an agent, a system and a view

A FlowsActors_Agent defines the model invocation and structured prompt. An ActorSystem gives that agent a frontend-visible identity. A frontend view points to the system; it does not duplicate the prompt or tool definitions.

This actor-system declaration follows Wonder Todos and points to the agent just declared:

const todoAssistantActorSystem: ActorSystem = {
  ref: TODO_ASSISTANT_SYSTEM_REF,
  displayName: 'Todo Assistant',
  description: 'Read tasks, explain policy passages and request reviewed task renames.',
  kind: FlowActorSystem_Kind.ACTOR,
  status: FlowActorSystem_Status.ACTIVE,
  visibility: FlowActorSystem_Visibility.FRONTEND,
  config: {
    timeout: 60000,
    accessControl: [{
      primaryScope: ResourcePrimaryScope.APPLICATION,
      requiresApproval: false,
    }],
    riskLevel: ResourceOperationRiskLevel.LOW,
  },
  actors: [{
    ref: 'assistant',
    displayName: 'Assistant',
    roleCategory: Actor_Role_Category.OPERATOR,
    executionMode: Actor_ExecutionMode.AGENTIC,
    agentRef: TODO_ASSISTANT_CHAT_AGENT_REF,
  }],
  createdAt: '2026-07-12T00:00:00.000Z',
};

FRONTEND makes the system discoverable by the view. AGENTIC and agentRef select the conversational agent; the system is not itself the prompt. Register the system factory, agent and tools on the backend module’s flowsActors contribution:

export const moduleBackend_FlowsActorsRegistry: {
  flowActorSystemsFactoryMap: FlowActorSystem_InitializationFactoryMap;
  agents: FlowsActors_Agent[];
  functionTools: FunctionTool[];
} = {
  flowActorSystemsFactoryMap: { [TODO_ASSISTANT_SYSTEM_REF]: () => todoAssistantActorSystem },
  agents: [todoAssistantChatAgent],
  functionTools: [readTask, searchPolicies, renameTask],
};

Mount the registry on the owning backend module. This is the relevant composition in backend-api/src/modules/tasks-manager/index.ts; retain the module’s other contributions:

import type { BackendDomainModule } from '@wildo-ai/saas-backend-lib';
import resourcesModule from './resources';
import { moduleBackend_FlowsActorsRegistry } from './addons/flows-actors';

const backendModule: BackendDomainModule = {
  ...resourcesModule,
  flowsActors: moduleBackend_FlowsActorsRegistry,
};

That module must remain in the application’s backend module registry. Exporting an agent file alone does not make it available. The frontend view names todo-assistant, the system names todo-assistant.chat.agent, and that agent names exactly the three registered function tools.

Answer policy questions from the knowledge documents

searchPolicies is the engine’s retrieval tool, scoped to the resource whose fields declare .ragSource(). The application writes no search code: the tool takes a question and an optional passage limit, and its identity comes from the conversation, never from the model.

const searchPolicies = retrievalTool({ resourceTypes: [TasksManager_ResourceType.KNOWLEDGE_DOCUMENTS] });

It searches as the person chatting. The caller’s context is rebuilt from the verified authentication on the source’s READ operation, and retrieval admits a source only when the person’s roles admit that READ. It re-reads every matched record on the person’s authority, so the model can never quote a document its person could not open. Startup refuses a source that declares no .ragSource() field or no READ operation, rather than letting the tool search nothing.

Preserve what the search actually established

The tool answers { outcome, results, notSearched, notes, searchStatus, embeddingStatus }. Each result is a record (recordId, title) with the passages that matched, and each passage names the field it came from: that pair is what the agent cites.

outcomeWhat the agent may say
matchedAnswer only from the passages, naming the document each fact came from.
nothing-matchedA healthy search found nothing; say so, without answering from general knowledge.
refusedA source could not be searched for this person; nothing is known about whether the policy exists.
unavailable, incompleteThe search failed, did not run, or lost rows it could not read; never report the policy as absent.

notes says, in words the agent can repeat, what made the answer smaller than the index could support: withheld records, a degraded search, a context budget reached. The agent’s instructions above consume outcome and notes instead of treating every empty result as proof that no policy exists. A later rename follows the separate approval-gated resource tool.

What happens during a turn

StageEngine behavior
StartResolve the system and the conversation’s owner scope.
ContinueRead the stored transcript using the conversation identifier and its isolation fields.
AnswerCompile the prompt, invoke the agent and emit growing text to the client.
Tool callResolve only the tools declared by the agent and run them in the caller’s context.
FinishPersist the messages and execution status for the next turn.

The browser continues a discussion by reusing its executionId. It sends the new message; the server loads previous turns. Owner, credential and agent-instance fields remain part of the persistence lookup rather than trusting an identifier by itself.

Use the streaming conversational path for tool-using chat. The buffered one-shot invocation is a different contract: declared function tools do not execute there. Coding sessions also have their own progress events rather than this token-delta channel.

Building a B2B product or an internal tool?

Wildo is not self-service yet. Tell us what you have in mind and we will say plainly whether it fits, and what happens next.