@owlmeans/agent 0.1.18-rc.7
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 +93 -0
- package/agent-meta/manifest.json +16 -0
- package/agent-meta/skills/agent/SKILL.md +122 -0
- package/build/consts.d.ts +16 -0
- package/build/consts.d.ts.map +1 -0
- package/build/consts.js +16 -0
- package/build/consts.js.map +1 -0
- package/build/errors.d.ts +16 -0
- package/build/errors.d.ts.map +1 -0
- package/build/errors.js +27 -0
- package/build/errors.js.map +1 -0
- package/build/helpers/compaction.d.ts +46 -0
- package/build/helpers/compaction.d.ts.map +1 -0
- package/build/helpers/compaction.js +119 -0
- package/build/helpers/compaction.js.map +1 -0
- package/build/helpers/index.d.ts +4 -0
- package/build/helpers/index.d.ts.map +1 -0
- package/build/helpers/index.js +4 -0
- package/build/helpers/index.js.map +1 -0
- package/build/helpers/rolling.d.ts +25 -0
- package/build/helpers/rolling.d.ts.map +1 -0
- package/build/helpers/rolling.js +45 -0
- package/build/helpers/rolling.js.map +1 -0
- package/build/helpers/tools.d.ts +29 -0
- package/build/helpers/tools.d.ts.map +1 -0
- package/build/helpers/tools.js +36 -0
- package/build/helpers/tools.js.map +1 -0
- package/build/index.d.ts +12 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +11 -0
- package/build/index.js.map +1 -0
- package/build/model.d.ts +15 -0
- package/build/model.d.ts.map +1 -0
- package/build/model.js +208 -0
- package/build/model.js.map +1 -0
- package/build/plugins/export.d.ts +8 -0
- package/build/plugins/export.d.ts.map +1 -0
- package/build/plugins/export.js +4 -0
- package/build/plugins/export.js.map +1 -0
- package/build/plugins/memory-events.d.ts +36 -0
- package/build/plugins/memory-events.d.ts.map +1 -0
- package/build/plugins/memory-events.js +83 -0
- package/build/plugins/memory-events.js.map +1 -0
- package/build/plugins/memory-graph.d.ts +52 -0
- package/build/plugins/memory-graph.d.ts.map +1 -0
- package/build/plugins/memory-graph.js +155 -0
- package/build/plugins/memory-graph.js.map +1 -0
- package/build/plugins/summarize.d.ts +44 -0
- package/build/plugins/summarize.d.ts.map +1 -0
- package/build/plugins/summarize.js +77 -0
- package/build/plugins/summarize.js.map +1 -0
- package/build/runtime/checkpoint.d.ts +37 -0
- package/build/runtime/checkpoint.d.ts.map +1 -0
- package/build/runtime/checkpoint.js +52 -0
- package/build/runtime/checkpoint.js.map +1 -0
- package/build/runtime/provider.d.ts +18 -0
- package/build/runtime/provider.d.ts.map +1 -0
- package/build/runtime/provider.js +30 -0
- package/build/runtime/provider.js.map +1 -0
- package/build/runtime/transport.d.ts +27 -0
- package/build/runtime/transport.d.ts.map +1 -0
- package/build/runtime/transport.js +29 -0
- package/build/runtime/transport.js.map +1 -0
- package/build/service.d.ts +14 -0
- package/build/service.d.ts.map +1 -0
- package/build/service.js +61 -0
- package/build/service.js.map +1 -0
- package/build/stores/index.d.ts +3 -0
- package/build/stores/index.d.ts.map +1 -0
- package/build/stores/index.js +2 -0
- package/build/stores/index.js.map +1 -0
- package/build/stores/memory.d.ts +6 -0
- package/build/stores/memory.d.ts.map +1 -0
- package/build/stores/memory.js +0 -0
- package/build/stores/memory.js.map +1 -0
- package/build/stores/types.d.ts +38 -0
- package/build/stores/types.d.ts.map +1 -0
- package/build/stores/types.js +2 -0
- package/build/stores/types.js.map +1 -0
- package/build/types.d.ts +130 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/package.json +72 -0
- package/src/consts.ts +19 -0
- package/src/errors.ts +33 -0
- package/src/helpers/compaction.ts +172 -0
- package/src/helpers/index.ts +3 -0
- package/src/helpers/rolling.ts +68 -0
- package/src/helpers/tools.ts +46 -0
- package/src/index.ts +11 -0
- package/src/model.ts +269 -0
- package/src/plugins/export.ts +12 -0
- package/src/plugins/memory-events.ts +129 -0
- package/src/plugins/memory-graph.ts +217 -0
- package/src/plugins/summarize.ts +129 -0
- package/src/runtime/checkpoint.ts +89 -0
- package/src/runtime/provider.ts +35 -0
- package/src/runtime/transport.ts +50 -0
- package/src/service.ts +97 -0
- package/src/stores/index.ts +2 -0
- package/src/stores/memory.ts +0 -0
- package/src/stores/types.ts +45 -0
- package/src/types.ts +144 -0
- package/tests/_tools/model.ts +50 -0
- package/tests/agent.spec.ts +218 -0
- package/tests/plugins.spec.ts +257 -0
- package/tests/runtime.spec.ts +140 -0
- package/tests/summary.spec.ts +143 -0
- package/tests/tools.spec.ts +68 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { AIMessage, HumanMessage, ToolMessage } from '@langchain/core/messages'
|
|
3
|
+
import type { LlmModel } from '@owlmeans/llm'
|
|
4
|
+
import { AgentRunStatus } from '@owlmeans/agent-common'
|
|
5
|
+
import { composeCompaction, composeRollingSummary, renderTranscript } from '../src/index.js'
|
|
6
|
+
|
|
7
|
+
/** A model double: the helpers only ever call `invoke` (structured) or `ask` (text). */
|
|
8
|
+
const answering = (answer: unknown): LlmModel => ({
|
|
9
|
+
invoke: async () => answer,
|
|
10
|
+
ask: async () => answer as string,
|
|
11
|
+
} as unknown as LlmModel)
|
|
12
|
+
|
|
13
|
+
const failing = (): LlmModel => ({
|
|
14
|
+
invoke: async () => { throw new Error('budget exhausted') },
|
|
15
|
+
ask: async () => { throw new Error('budget exhausted') },
|
|
16
|
+
} as unknown as LlmModel)
|
|
17
|
+
|
|
18
|
+
const conversation = [
|
|
19
|
+
new HumanMessage({ content: 'rename the header' }),
|
|
20
|
+
new AIMessage({ content: '', tool_calls: [{ name: 'write_file', args: {}, id: 'c1', type: 'tool_call' }] }),
|
|
21
|
+
new ToolMessage({ tool_call_id: 'c1', name: 'write_file', content: 'written' }),
|
|
22
|
+
new AIMessage({ content: 'Renamed the dashboard header.' }),
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
describe('agent — conversation compaction', () => {
|
|
26
|
+
test('returns both parts, each inside its own cap', async () => {
|
|
27
|
+
const result = await composeCompaction({
|
|
28
|
+
model: answering({ summary: 'S'.repeat(5_000), advice: 'A'.repeat(5_000) }),
|
|
29
|
+
prompt: 'rename the header',
|
|
30
|
+
messages: conversation,
|
|
31
|
+
status: AgentRunStatus.Ok,
|
|
32
|
+
maxSummaryChars: 100,
|
|
33
|
+
maxAdviceChars: 40,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// A cap in a prompt is a request; a cap in code is a cap.
|
|
37
|
+
expect(result.summary.length).toBeLessThanOrEqual(100)
|
|
38
|
+
expect(result.advice!.length).toBeLessThanOrEqual(40)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('falls back to a usable event when the model fails', async () => {
|
|
42
|
+
// An exhausted budget is the common case, and asking again would fail the same way — so the
|
|
43
|
+
// fallback has to carry the facts the caller already holds rather than an apology.
|
|
44
|
+
const result = await composeCompaction({
|
|
45
|
+
model: failing(),
|
|
46
|
+
prompt: 'rename the header',
|
|
47
|
+
messages: conversation,
|
|
48
|
+
status: AgentRunStatus.Failed,
|
|
49
|
+
note: 'the fixer gave up',
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
expect(result.summary).toContain('rename the header')
|
|
53
|
+
expect(result.summary).toContain('the fixer gave up')
|
|
54
|
+
expect(result.advice).toBeUndefined()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('treats an empty summary as a non-answer, not a short one', async () => {
|
|
58
|
+
const result = await composeCompaction({
|
|
59
|
+
model: answering({ summary: ' ', advice: 'do something' }),
|
|
60
|
+
prompt: 'rename the header',
|
|
61
|
+
messages: conversation,
|
|
62
|
+
status: AgentRunStatus.Ok,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
expect(result.summary).toContain('rename the header')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('works with no model at all', async () => {
|
|
69
|
+
const result = await composeCompaction({
|
|
70
|
+
prompt: 'rename the header', messages: conversation, status: AgentRunStatus.Ok,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
expect(result.summary).toContain('rename the header')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('drops an empty advice rather than storing a blank field', async () => {
|
|
77
|
+
const result = await composeCompaction({
|
|
78
|
+
model: answering({ summary: 'did the thing', advice: '' }),
|
|
79
|
+
prompt: 'p', messages: conversation, status: AgentRunStatus.Ok,
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
expect(result).toEqual({ summary: 'did the thing' })
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe('agent — transcript rendering', () => {
|
|
87
|
+
test('names the tools a message called when it carried no text', async () => {
|
|
88
|
+
expect(renderTranscript(conversation)).toContain('called write_file')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('keeps the tail when the budget binds', () => {
|
|
92
|
+
// How a run ENDED is what decides what to do next, so the head is what goes.
|
|
93
|
+
const long = [
|
|
94
|
+
new HumanMessage({ content: 'A'.repeat(400) }),
|
|
95
|
+
new AIMessage({ content: 'the final answer' }),
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
const rendered = renderTranscript(long, 100)
|
|
99
|
+
|
|
100
|
+
expect(rendered).toContain('the final answer')
|
|
101
|
+
expect(rendered).not.toContain('AAAA')
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe('agent — rolling summary', () => {
|
|
106
|
+
test('caps the folded account', async () => {
|
|
107
|
+
const result = await composeRollingSummary({
|
|
108
|
+
model: answering('X'.repeat(9_000)),
|
|
109
|
+
previous: 'the story so far',
|
|
110
|
+
event: 'a story completed',
|
|
111
|
+
maxChars: 120,
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
expect(result.length).toBeLessThanOrEqual(120)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('keeps the previous account when the fold fails', async () => {
|
|
118
|
+
// A failed fold costs detail, never the fact — the caller's own verbatim record of the event
|
|
119
|
+
// is what preserves that, which is why history can be recorded unconditionally.
|
|
120
|
+
const result = await composeRollingSummary({
|
|
121
|
+
model: failing(),
|
|
122
|
+
previous: 'the story so far',
|
|
123
|
+
event: 'a story completed',
|
|
124
|
+
maxChars: 3_000,
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
expect(result).toBe('the story so far')
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('starts from the event itself when nothing was recorded yet', async () => {
|
|
131
|
+
const result = await composeRollingSummary({
|
|
132
|
+
model: failing(), previous: '', event: 'project created', maxChars: 3_000,
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
expect(result).toBe('project created')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('takes the deterministic path with no model', async () => {
|
|
139
|
+
expect(await composeRollingSummary({
|
|
140
|
+
previous: 'kept', event: 'ignored', maxChars: 3_000,
|
|
141
|
+
})).toBe('kept')
|
|
142
|
+
})
|
|
143
|
+
})
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { tool } from '@langchain/core/tools'
|
|
3
|
+
import * as z from 'zod'
|
|
4
|
+
import { isToolError, safeInvokeTool } from '../src/index.js'
|
|
5
|
+
import type { AgentToolSet } from '../src/index.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* No model involved: these pin the containment contract the tool loop depends on, which is what
|
|
9
|
+
* keeps one bad argument from taking down a whole run. A rejected LangGraph task aborts the entire
|
|
10
|
+
* superstep, so every sibling call in the same batch dies with it.
|
|
11
|
+
*/
|
|
12
|
+
const tools = {
|
|
13
|
+
get_structured_list: tool(
|
|
14
|
+
async ({ type }: { type: string }) => `listed:${type}`,
|
|
15
|
+
{
|
|
16
|
+
name: 'get_structured_list',
|
|
17
|
+
description: 'Test double mirroring a real enum-argument tool.',
|
|
18
|
+
schema: z.object({ type: z.enum(['ui-screens', 'ui-layout']) }),
|
|
19
|
+
},
|
|
20
|
+
),
|
|
21
|
+
explodes: tool(
|
|
22
|
+
async () => { throw new Error('the tool itself failed') },
|
|
23
|
+
{
|
|
24
|
+
name: 'explodes',
|
|
25
|
+
description: 'A tool that throws.',
|
|
26
|
+
schema: z.object({}),
|
|
27
|
+
},
|
|
28
|
+
),
|
|
29
|
+
} as unknown as AgentToolSet
|
|
30
|
+
|
|
31
|
+
const callOf = (name: string, args: Record<string, unknown>) =>
|
|
32
|
+
({ name, args, id: 'call_test', type: 'tool_call' as const })
|
|
33
|
+
|
|
34
|
+
describe('agent — tool invocation is contained', () => {
|
|
35
|
+
test('passes a valid call through to the tool', async () => {
|
|
36
|
+
expect(await safeInvokeTool(tools, callOf('get_structured_list', { type: 'ui-layout' })))
|
|
37
|
+
.toBe('listed:ui-layout')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('an out-of-enum argument resolves to an error naming the valid options', async () => {
|
|
41
|
+
const result = await safeInvokeTool(tools, callOf('get_structured_list', { type: 'ui-navigation' }))
|
|
42
|
+
|
|
43
|
+
expect(isToolError(result)).toBe(true)
|
|
44
|
+
expect((result as { error: string }).error).toContain('ui-screens')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('a throwing tool resolves to an error instead of rejecting', async () => {
|
|
48
|
+
const result = await safeInvokeTool(tools, callOf('explodes', {}))
|
|
49
|
+
|
|
50
|
+
expect(isToolError(result)).toBe(true)
|
|
51
|
+
expect((result as { error: string }).error).toContain('the tool itself failed')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('resolves a tool by its own name when the map key differs', async () => {
|
|
55
|
+
// `bindTools` advertises `tool.name`, so that is what the model calls — a map keyed by a local
|
|
56
|
+
// variable would otherwise lose the tool permanently.
|
|
57
|
+
const mismatched = { some_local_name: tools.get_structured_list } as unknown as AgentToolSet
|
|
58
|
+
|
|
59
|
+
expect(await safeInvokeTool(mismatched, callOf('get_structured_list', { type: 'ui-layout' })))
|
|
60
|
+
.toBe('listed:ui-layout')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('an unknown tool name resolves to an error instead of throwing', async () => {
|
|
64
|
+
const result = await safeInvokeTool(tools, callOf('hallucinated_tool', {}))
|
|
65
|
+
|
|
66
|
+
expect((result as { error: string }).error).toContain('not found')
|
|
67
|
+
})
|
|
68
|
+
})
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": [
|
|
3
|
+
"@owlmeans/dep-config/tsconfig.base.json",
|
|
4
|
+
"@owlmeans/dep-config/tsconfig.node.json"
|
|
5
|
+
],
|
|
6
|
+
"compilerOptions": {
|
|
7
|
+
"rootDir": "./src/",
|
|
8
|
+
"outDir": "./build/"
|
|
9
|
+
},
|
|
10
|
+
"include": [
|
|
11
|
+
"src/**/*"
|
|
12
|
+
],
|
|
13
|
+
"exclude": [
|
|
14
|
+
"./dist/**/*",
|
|
15
|
+
"./build/**/*",
|
|
16
|
+
"./tests/**/*",
|
|
17
|
+
"./*.ts"
|
|
18
|
+
]
|
|
19
|
+
}
|