Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
753b2b7
🔧 update (secrets): extend SecretsManager interface and engine types
warengonzaga Apr 6, 2026
625ff50
🔧 update (core): extend agent loop with state tracking support
warengonzaga Apr 6, 2026
891a62d
🔧 update (discord): extend plugin with new channel handlers
warengonzaga Apr 6, 2026
c653699
🔧 update (cli): update setup and setup-web commands
warengonzaga Apr 6, 2026
28cf362
🔧 update (cli): extend start command and expand tests
warengonzaga Apr 6, 2026
1bf96e9
🔧 update (cli): extend purge command and expand tests
warengonzaga Apr 6, 2026
f357dd9
🔧 update: fix retry duplication, path quoting, auto-restart deduplica…
Copilot Apr 7, 2026
77261cb
🔧 update (core): extract RESTART_TOOL_NAME constant and fix auto-rest…
Copilot Apr 7, 2026
af4db11
☕ chore: Bump vite from 7.3.1 to 8.0.0 (#58)
dependabot[bot] Mar 19, 2026
51b17e9
☕ chore: Bump github/codeql-action from 4.32.5 to 4.33.0 (#57)
dependabot[bot] Mar 19, 2026
b804f73
☕ chore: Bump docker/setup-qemu-action from 3 to 4 (#53)
dependabot[bot] Mar 19, 2026
4813a81
☕ chore: Bump wgtechlabs/release-build-flow-action from 1.6.0 to 1.7.…
dependabot[bot] Mar 19, 2026
78a8fd1
☕ chore: Bump actions/setup-node from 6.2.0 to 6.3.0 (#51)
dependabot[bot] Mar 19, 2026
facbe35
☕ chore: Bump oven/bun from 1.3.10-slim to 1.3.11-slim (#59)
dependabot[bot] Apr 3, 2026
306b10b
☕ chore: Bump nick-fields/retry from 3 to 4 (#60)
dependabot[bot] Apr 3, 2026
b167574
☕ chore: Bump wgtechlabs/package-build-flow-action from 2.1.0 to 2.1.…
dependabot[bot] Apr 3, 2026
ed7c534
☕ chore: Bump typescript from 5.9.3 to 6.0.2 (#63)
dependabot[bot] Apr 3, 2026
4c51525
☕ chore: Bump github/codeql-action from 4.33.0 to 4.35.1 (#64)
dependabot[bot] Apr 3, 2026
1166a54
☕ chore: Bump actions/deploy-pages from 4.0.5 to 5.0.0 (#65)
dependabot[bot] Apr 3, 2026
18f517b
☕ chore: Bump wgtechlabs/container-build-flow-action from 1.7.0 to 1.…
dependabot[bot] Apr 7, 2026
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
100 changes: 97 additions & 3 deletions packages/core/src/loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type {
Message,
PendingApproval,
ShieldEvent,
StreamEvent,
Tool,
ToolCall,
} from '@tinyclaw/types';
import { isOwner, OWNER_ONLY_TOOLS } from '@tinyclaw/types';
Expand Down Expand Up @@ -207,6 +209,58 @@ function getWorkingMessage(toolName: string): string {
return '🤔 Working on that…\n\n';
}

function shouldAutoRestartAfterTool(toolName: string, result: string): boolean {
if (!/_(pair|unpair)$/.test(toolName)) {
return false;
}

if (result.startsWith('Error')) {
return false;
}

return result.includes('tinyclaw_restart');
}

async function maybeRunAutoRestart(
originalToolName: string,
toolResults: Array<{ id: string; result: string }>,
tools: Tool[],
onStream: ((event: StreamEvent) => void) | undefined,
): Promise<void> {
const needsRestart = toolResults.some((toolResult) =>
shouldAutoRestartAfterTool(originalToolName, toolResult.result),
);

if (!needsRestart) {
return;
}

const restartTool = tools.find((tool) => tool.name === 'tinyclaw_restart');
if (!restartTool) {
return;
}

if (onStream) {
onStream({ type: 'tool_start', tool: restartTool.name });
}

try {
const restartResult = await restartTool.execute({
reason: `Apply changes from ${originalToolName}`,
});
toolResults.push({ id: `${originalToolName}:auto-restart`, result: restartResult });
if (onStream) {
onStream({ type: 'tool_result', tool: restartTool.name, result: restartResult });
}
} catch (error) {
const errorMsg = `Error: ${error instanceof Error ? error.message : 'Unknown error'}`;
toolResults.push({ id: `${originalToolName}:auto-restart`, result: errorMsg });
if (onStream) {
onStream({ type: 'tool_result', tool: restartTool.name, result: errorMsg });
}
}
}

// ---------------------------------------------------------------------------
// Delegation stream event helpers
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -497,6 +551,18 @@ When the user asks to set up or change their primary provider:

Providers must be installed as plugins first (added to plugins.enabled in the config).

## Plugin Setup Guidance

When the user asks to set up, connect, install, enable, or pair a channel or provider plugin:
- First figure out whether they have already shared the required credential in the conversation, such as a bot token or API key.
- If they have not shared it yet, do not pretend the plugin is configured and do not claim it is online.
- Walk them through the setup step by step, briefly and concretely.
- For Discord, explain that they need to create an application in the Discord Developer Portal, add a bot, copy the bot token, and enable Message Content Intent.
- After they provide the required credential, call the appropriate pairing tool.
- After pairing succeeds, clearly tell them what changed and whether a restart is happening.
- Never say a plugin is active, connected, or ready unless the pairing tool succeeded and any required restart or activation step has been completed.
- If the user asks whether the Discord bot is online, offline, connected, or why it failed to start, use the discord_status tool before answering. Do not guess from config alone.

## How to Use Tools

When you need to use a tool, output ONLY a JSON object with the tool name and arguments. Examples:
Expand Down Expand Up @@ -961,6 +1027,8 @@ export async function agentLoop(
}
}

await maybeRunAutoRestart(toolCall.name, toolResults, tools, onStream);

// For read/search/recall operations, send result back to LLM for natural response
const isReadOperation =
toolCall.name.includes('read') ||
Expand Down Expand Up @@ -999,11 +1067,10 @@ export async function agentLoop(
// For write operations, feed the result back to the LLM so it
// can craft a natural, conversational response instead of the
// generic "Done!" that was causing a feedback loop in the history.
const writeResult = toolResults[0]?.result || 'completed';
const _writeSummary = summarizeToolResults([toolCall], toolResults);
const resultsText = toolResults.map((result) => result.result).join('\n\n');
messages.push({
role: 'assistant',
content: `I used ${toolCall.name} and the result was: ${writeResult}`,
content: `I used these tools and the results were:\n${resultsText}`,
});
messages.push({
role: 'user',
Expand Down Expand Up @@ -1143,6 +1210,17 @@ export async function agentLoop(
}
}

for (const toolCall of response.toolCalls) {
const matchingResults = toolResults.filter((result) => result.id === toolCall.id);
await maybeRunAutoRestart(toolCall.name, matchingResults, tools, onStream);
const autoRestartResults = matchingResults.filter(
(result) => result.id === `${toolCall.name}:auto-restart`,
);
if (autoRestartResults.length > 0) {
toolResults.push(...autoRestartResults);
}
}

// If pending approvals were queued during structured tool_calls, ask the user
// about the first one (subsequent ones will be handled on following turns).
const paQueue = pendingApprovals.get(userId);
Expand Down Expand Up @@ -1209,6 +1287,22 @@ export async function agentLoop(
continue;
}

if (toolResults.some((r) => !r.result.startsWith('Error'))) {
const resultsText = toolResults.map((r) => r.result).join('\n\n');
messages.push({
role: 'assistant',
content: `I used these tools and the results were:\n${resultsText}`,
});
messages.push({
role: 'user',
content:
'Now respond naturally to my original message. Briefly confirm the action you took and be conversational.',
});

// Continue the loop to get LLM's natural response
continue;
}

const responseText = summarizeToolResults(response.toolCalls, toolResults);

if (onStream) {
Expand Down
Loading
Loading