@loopstack/agent-examples 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +150 -0
- package/dist/agent-examples.module.d.ts +3 -0
- package/dist/agent-examples.module.d.ts.map +1 -0
- package/dist/agent-examples.module.js +52 -0
- package/dist/agent-examples.module.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/calculator.tool.d.ts +15 -0
- package/dist/tools/calculator.tool.d.ts.map +1 -0
- package/dist/tools/calculator.tool.js +51 -0
- package/dist/tools/calculator.tool.js.map +1 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +19 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/weather-lookup.tool.d.ts +10 -0
- package/dist/tools/weather-lookup.tool.d.ts.map +1 -0
- package/dist/tools/weather-lookup.tool.js +36 -0
- package/dist/tools/weather-lookup.tool.js.map +1 -0
- package/dist/workflows/agent/agent-example.workflow.d.ts +9 -0
- package/dist/workflows/agent/agent-example.workflow.d.ts.map +1 -0
- package/dist/workflows/agent/agent-example.workflow.js +62 -0
- package/dist/workflows/agent/agent-example.workflow.js.map +1 -0
- package/dist/workflows/agent/templates/system.md +4 -0
- package/dist/workflows/code-agent/code-agent-example.workflow.d.ts +12 -0
- package/dist/workflows/code-agent/code-agent-example.workflow.d.ts.map +1 -0
- package/dist/workflows/code-agent/code-agent-example.workflow.js +75 -0
- package/dist/workflows/code-agent/code-agent-example.workflow.js.map +1 -0
- package/dist/workflows/custom-agent/custom-agent-example.workflow.d.ts +30 -0
- package/dist/workflows/custom-agent/custom-agent-example.workflow.d.ts.map +1 -0
- package/dist/workflows/custom-agent/custom-agent-example.workflow.js +165 -0
- package/dist/workflows/custom-agent/custom-agent-example.workflow.js.map +1 -0
- package/dist/workflows/custom-agent/templates/system.md +14 -0
- package/dist/workflows/custom-agent/templates/wrap-up.md +3 -0
- package/dist/workflows/mcp-linear/mcp-linear-example.workflow.d.ts +18 -0
- package/dist/workflows/mcp-linear/mcp-linear-example.workflow.d.ts.map +1 -0
- package/dist/workflows/mcp-linear/mcp-linear-example.workflow.js +65 -0
- package/dist/workflows/mcp-linear/mcp-linear-example.workflow.js.map +1 -0
- package/package.json +54 -0
- package/src/agent-examples.module.ts +40 -0
- package/src/index.ts +5 -0
- package/src/tools/calculator.tool.ts +47 -0
- package/src/tools/index.ts +2 -0
- package/src/tools/weather-lookup.tool.ts +29 -0
- package/src/workflows/agent/agent-example.workflow.ts +40 -0
- package/src/workflows/agent/templates/system.md +4 -0
- package/src/workflows/code-agent/code-agent-example.workflow.ts +56 -0
- package/src/workflows/custom-agent/custom-agent-example.workflow.ts +202 -0
- package/src/workflows/custom-agent/templates/system.md +14 -0
- package/src/workflows/custom-agent/templates/wrap-up.md +3 -0
- package/src/workflows/mcp-linear/mcp-linear-example.workflow.ts +52 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { BaseWorkflow, Guard, MarkdownDocument, Transition, Workflow } from '@loopstack/common';
|
|
3
|
+
import type { TransitionInput } from '@loopstack/common';
|
|
4
|
+
import type { LlmDelegateResult, LlmGenerateTextResult } from '@loopstack/llm-provider-module';
|
|
5
|
+
import {
|
|
6
|
+
LlmDelegateToolCallsTool,
|
|
7
|
+
LlmGenerateTextTool,
|
|
8
|
+
LlmMessageDocument,
|
|
9
|
+
LlmUpdateToolResultTool,
|
|
10
|
+
} from '@loopstack/llm-provider-module';
|
|
11
|
+
|
|
12
|
+
const MAX_TURNS = 2;
|
|
13
|
+
|
|
14
|
+
interface CustomAgentState {
|
|
15
|
+
llmResult?: LlmGenerateTextResult;
|
|
16
|
+
delegateResult?: LlmDelegateResult;
|
|
17
|
+
turnCount: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Custom agent loop with a deterministic turn budget and a forced wrap-up phase.
|
|
22
|
+
*
|
|
23
|
+
* The generic `AgentWorkflow` only exits when the LLM emits `end_turn`. This example
|
|
24
|
+
* shows what a hand-rolled loop unlocks: after `MAX_TURNS` tool-using turns, the
|
|
25
|
+
* workflow stops the loop itself and runs one final LLM call with a different
|
|
26
|
+
* system prompt and no tools, asking the model to summarize whatever it has so far.
|
|
27
|
+
*
|
|
28
|
+
* Two distinct LLM phases (loop with tools → wrap-up without tools) cannot be
|
|
29
|
+
* expressed by a single `AgentWorkflow.run(...)` call, which is the motivation for
|
|
30
|
+
* building a custom agent.
|
|
31
|
+
*/
|
|
32
|
+
@Workflow({
|
|
33
|
+
title: 'Agent - Custom Agent Example',
|
|
34
|
+
description:
|
|
35
|
+
'A from-scratch agent loop with a deterministic turn budget. After N tool-using turns the workflow forces a wrap-up LLM call with a different system prompt and no tools — illustrating a control-flow shape the generic AgentWorkflow cannot express.',
|
|
36
|
+
})
|
|
37
|
+
export class CustomAgentExampleWorkflow extends BaseWorkflow {
|
|
38
|
+
constructor(
|
|
39
|
+
private readonly llmGenerateText: LlmGenerateTextTool,
|
|
40
|
+
private readonly llmDelegateToolCalls: LlmDelegateToolCallsTool,
|
|
41
|
+
private readonly llmUpdateToolResult: LlmUpdateToolResultTool,
|
|
42
|
+
) {
|
|
43
|
+
super();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Initial transition. Renders an introductory note for the user, seeds the
|
|
48
|
+
* conversation with the user's request, and initialises the turn counter.
|
|
49
|
+
*/
|
|
50
|
+
@Transition({ to: 'ready' })
|
|
51
|
+
async setup(_state: CustomAgentState) {
|
|
52
|
+
await this.documentStore.save(MarkdownDocument, {
|
|
53
|
+
markdown:
|
|
54
|
+
'# Custom Agent Example\n\n' +
|
|
55
|
+
`This agent has a budget of **${MAX_TURNS} tool-using turns**. ` +
|
|
56
|
+
'Once the budget is hit, the workflow forces a final summary turn with no tools available.',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
await this.documentStore.save(LlmMessageDocument, {
|
|
60
|
+
role: 'user',
|
|
61
|
+
text:
|
|
62
|
+
'I want to travel somewhere warm. Find me the first city of at least 25°C from this list, ' +
|
|
63
|
+
'trying them one at a time in order: London, New York, Paris, Berlin, Tokyo. ' +
|
|
64
|
+
'After each lookup, decide based on the temperature whether to keep searching.',
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
this.assignState({ turnCount: 0 });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Main loop turn. While the budget allows it, runs a normal LLM call with the
|
|
72
|
+
* full tool set and increments the turn counter. The next state is decided
|
|
73
|
+
* by the guards on transitions out of `prompt_executed`.
|
|
74
|
+
*/
|
|
75
|
+
@Transition({ from: 'ready', to: 'prompt_executed', priority: 10 })
|
|
76
|
+
@Guard('hasBudget')
|
|
77
|
+
async llmTurn(state: CustomAgentState) {
|
|
78
|
+
const result = await this.llmGenerateText.call(
|
|
79
|
+
{},
|
|
80
|
+
{
|
|
81
|
+
config: {
|
|
82
|
+
provider: 'claude',
|
|
83
|
+
system: this.render(join(__dirname, 'templates', 'system.md')),
|
|
84
|
+
tools: ['weather_lookup', 'calculator'],
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
this.assignState({
|
|
90
|
+
turnCount: state.turnCount + 1,
|
|
91
|
+
llmResult: result.data,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Fires the moment the budget is exhausted. Appends a user-role
|
|
97
|
+
* `LlmMessageDocument` telling the model the budget is gone (so the model
|
|
98
|
+
* itself sees the constraint, not just the system prompt). Kept as its own
|
|
99
|
+
* short transition so the message renders in the UI immediately, without
|
|
100
|
+
* waiting for the slower wrap-up LLM call that follows.
|
|
101
|
+
*/
|
|
102
|
+
@Transition({ from: 'ready', to: 'over_budget' })
|
|
103
|
+
@Guard('isOverBudget')
|
|
104
|
+
async notifyOverBudget(_state: CustomAgentState) {
|
|
105
|
+
await this.documentStore.save(LlmMessageDocument, {
|
|
106
|
+
role: 'user',
|
|
107
|
+
text:
|
|
108
|
+
`You have used your full turn budget of ${MAX_TURNS} tool-using turns and can no longer call tools. ` +
|
|
109
|
+
'Summarise what you found so far in plain text and answer based on that.',
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Forced final turn after the budget notice has rendered. Runs one last LLM
|
|
115
|
+
* call with the wrap-up system prompt and no tools, asking for a plain-text
|
|
116
|
+
* summary based on whatever results were gathered before the budget hit.
|
|
117
|
+
*/
|
|
118
|
+
@Transition({ from: 'over_budget', to: 'end' })
|
|
119
|
+
async wrapUp(_state: CustomAgentState) {
|
|
120
|
+
await this.llmGenerateText.call(
|
|
121
|
+
{},
|
|
122
|
+
{
|
|
123
|
+
config: {
|
|
124
|
+
provider: 'claude',
|
|
125
|
+
system: this.render(join(__dirname, 'templates', 'wrap-up.md')),
|
|
126
|
+
tools: [],
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The LLM asked to call one or more tools. Hands the assistant message to
|
|
134
|
+
* `LlmDelegateToolCalls`, which dispatches each tool concurrently and reports
|
|
135
|
+
* back via the `toolResultReceived` callback.
|
|
136
|
+
*/
|
|
137
|
+
@Transition({ from: 'prompt_executed', to: 'awaiting_tools', priority: 10 })
|
|
138
|
+
@Guard('hasToolCalls')
|
|
139
|
+
async executeToolCalls(state: CustomAgentState) {
|
|
140
|
+
const result = await this.llmDelegateToolCalls.call({
|
|
141
|
+
message: state.llmResult!.message,
|
|
142
|
+
callback: { transition: 'toolResultReceived' },
|
|
143
|
+
});
|
|
144
|
+
this.assignState({ delegateResult: result.data });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Wait-transition fired once per completed tool. Folds the result back into
|
|
149
|
+
* the `delegateResult` aggregate so we know which tool_use ids are still
|
|
150
|
+
* outstanding and when every call has finished.
|
|
151
|
+
*/
|
|
152
|
+
@Transition({ from: 'awaiting_tools', to: 'awaiting_tools', wait: true })
|
|
153
|
+
async toolResultReceived(state: CustomAgentState, input: TransitionInput) {
|
|
154
|
+
const result = await this.llmUpdateToolResult.call({
|
|
155
|
+
delegateResult: state.delegateResult!,
|
|
156
|
+
completedTool: input,
|
|
157
|
+
});
|
|
158
|
+
this.assignState({ delegateResult: result.data });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Every tool dispatched in this turn has reported back. Hands control back
|
|
163
|
+
* to `ready` so the loop can decide whether to keep going or wrap up.
|
|
164
|
+
*/
|
|
165
|
+
@Transition({ from: 'awaiting_tools', to: 'ready' })
|
|
166
|
+
@Guard('allToolsComplete')
|
|
167
|
+
toolsComplete(_state: CustomAgentState) {}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The LLM emitted `end_turn` instead of more tool calls — it considers the
|
|
171
|
+
* task finished. The text response was already persisted by `llmTurn`, so we
|
|
172
|
+
* just close the workflow.
|
|
173
|
+
*/
|
|
174
|
+
@Transition({ from: 'prompt_executed', to: 'end' })
|
|
175
|
+
@Guard('isEndTurn')
|
|
176
|
+
respond(_state: CustomAgentState) {}
|
|
177
|
+
|
|
178
|
+
/** True while the model is still allowed to take another tool-using turn. */
|
|
179
|
+
private hasBudget(state: CustomAgentState): boolean {
|
|
180
|
+
return state.turnCount < MAX_TURNS;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Inverse of {@link hasBudget} — gates the forced `wrapUp` transition. */
|
|
184
|
+
private isOverBudget(state: CustomAgentState): boolean {
|
|
185
|
+
return state.turnCount >= MAX_TURNS;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** True when the last LLM response asked to call one or more tools. */
|
|
189
|
+
private hasToolCalls(state: CustomAgentState): boolean {
|
|
190
|
+
return state.llmResult?.message.stopReason === 'tool_use';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** True when every tool dispatched in the current turn has reported back. */
|
|
194
|
+
private allToolsComplete(state: CustomAgentState): boolean {
|
|
195
|
+
return !!state.delegateResult?.allCompleted;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** True when the last LLM response was a final plain-text answer (no tools). */
|
|
199
|
+
private isEndTurn(state: CustomAgentState): boolean {
|
|
200
|
+
return state.llmResult?.message.stopReason === 'end_turn';
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
You are a helpful assistant searching for a city that matches the user's criteria.
|
|
2
|
+
|
|
3
|
+
Available tools:
|
|
4
|
+
|
|
5
|
+
- `weather_lookup` — look up the current weather for a city
|
|
6
|
+
- `calculator` — perform a basic arithmetic calculation
|
|
7
|
+
|
|
8
|
+
How to work:
|
|
9
|
+
|
|
10
|
+
- Look up exactly ONE city per turn — never call `weather_lookup` for multiple cities in parallel.
|
|
11
|
+
- After each lookup, reason about the result before deciding whether to keep searching.
|
|
12
|
+
- Stop calling tools as soon as you have enough information to answer.
|
|
13
|
+
|
|
14
|
+
When you are done, answer in plain text without calling any more tools.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ChatAgentWorkflow } from '@loopstack/agent';
|
|
3
|
+
import { BaseWorkflow, Transition, Workflow } from '@loopstack/common';
|
|
4
|
+
import type { RunContext } from '@loopstack/common';
|
|
5
|
+
import { McpCallTool, McpListToolsTool } from '@loopstack/mcp-module';
|
|
6
|
+
|
|
7
|
+
const LINEAR_MCP_URL = 'https://mcp.linear.app/mcp';
|
|
8
|
+
|
|
9
|
+
const McpLinearExampleArgsSchema = z.object({
|
|
10
|
+
initialMessage: z
|
|
11
|
+
.string()
|
|
12
|
+
.optional()
|
|
13
|
+
.default(
|
|
14
|
+
'List the available Linear MCP tools, then fetch my active issues and summarize the top 5 by priority with assignee and status.',
|
|
15
|
+
)
|
|
16
|
+
.describe('Initial message shown to the agent.'),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
type McpLinearExampleArgs = z.infer<typeof McpLinearExampleArgsSchema>;
|
|
20
|
+
|
|
21
|
+
@Workflow({
|
|
22
|
+
title: 'Agent - MCP Linear Example',
|
|
23
|
+
description: "Chat agent connected to Linear's hosted MCP server. Demonstrates MCP tool integration.",
|
|
24
|
+
schema: McpLinearExampleArgsSchema,
|
|
25
|
+
})
|
|
26
|
+
export class McpLinearExampleWorkflow extends BaseWorkflow<McpLinearExampleArgs> {
|
|
27
|
+
constructor(
|
|
28
|
+
private readonly chatAgentWorkflow: ChatAgentWorkflow,
|
|
29
|
+
private readonly mcpListTools: McpListToolsTool,
|
|
30
|
+
private readonly mcpCallTool: McpCallTool,
|
|
31
|
+
) {
|
|
32
|
+
super();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@Transition({ to: 'chatting' })
|
|
36
|
+
async startChat(state: Record<string, unknown>, ctx: RunContext<McpLinearExampleArgs>) {
|
|
37
|
+
const systemPrompt = [
|
|
38
|
+
`You are a Linear assistant connected via MCP at ${LINEAR_MCP_URL} (transport: streamableHttp).`,
|
|
39
|
+
'Use `mcpListTools` to discover the available Linear tools, then `mcpCallTool` to invoke them.',
|
|
40
|
+
`Always pass serverUrl="${LINEAR_MCP_URL}" and transport="streamableHttp".`,
|
|
41
|
+
].join('\n');
|
|
42
|
+
|
|
43
|
+
await this.chatAgentWorkflow.run(
|
|
44
|
+
{
|
|
45
|
+
system: systemPrompt,
|
|
46
|
+
tools: ['mcp_list_tools', 'mcp_call'],
|
|
47
|
+
userMessage: ctx.args.initialMessage,
|
|
48
|
+
},
|
|
49
|
+
{ show: 'inline', label: 'Linear Agent Chat' },
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|