@lota-sdk/core 0.1.5
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/infrastructure/schema/00_workstream.surql +55 -0
- package/infrastructure/schema/01_memory.surql +47 -0
- package/infrastructure/schema/02_execution_plan.surql +62 -0
- package/infrastructure/schema/03_learned_skill.surql +32 -0
- package/infrastructure/schema/04_runtime_bootstrap.surql +8 -0
- package/package.json +128 -0
- package/src/ai/definitions.ts +308 -0
- package/src/bifrost/bifrost.ts +256 -0
- package/src/config/agent-defaults.ts +99 -0
- package/src/config/constants.ts +33 -0
- package/src/config/env-shapes.ts +122 -0
- package/src/config/logger.ts +29 -0
- package/src/config/model-constants.ts +31 -0
- package/src/config/search.ts +17 -0
- package/src/config/workstream-defaults.ts +68 -0
- package/src/db/base.service.ts +55 -0
- package/src/db/cursor-pagination.ts +73 -0
- package/src/db/memory-query-builder.ts +207 -0
- package/src/db/memory-store.helpers.ts +118 -0
- package/src/db/memory-store.rows.ts +29 -0
- package/src/db/memory-store.ts +974 -0
- package/src/db/memory-types.ts +193 -0
- package/src/db/memory.ts +505 -0
- package/src/db/record-id.ts +78 -0
- package/src/db/service.ts +932 -0
- package/src/db/startup.ts +152 -0
- package/src/db/tables.ts +20 -0
- package/src/document/org-document-chunking.ts +224 -0
- package/src/document/parsing.ts +40 -0
- package/src/embeddings/provider.ts +76 -0
- package/src/index.ts +302 -0
- package/src/queues/context-compaction.queue.ts +82 -0
- package/src/queues/document-processor.queue.ts +118 -0
- package/src/queues/memory-consolidation.queue.ts +65 -0
- package/src/queues/post-chat-memory.queue.ts +128 -0
- package/src/queues/recent-activity-title-refinement.queue.ts +69 -0
- package/src/queues/regular-chat-memory-digest.config.ts +12 -0
- package/src/queues/regular-chat-memory-digest.queue.ts +73 -0
- package/src/queues/skill-extraction.config.ts +9 -0
- package/src/queues/skill-extraction.queue.ts +62 -0
- package/src/redis/connection.ts +176 -0
- package/src/redis/index.ts +30 -0
- package/src/redis/org-memory-lock.ts +43 -0
- package/src/redis/redis-lease-lock.ts +158 -0
- package/src/runtime/agent-contract.ts +1 -0
- package/src/runtime/agent-prompt-context.ts +119 -0
- package/src/runtime/agent-runtime-policy.ts +192 -0
- package/src/runtime/agent-stream-helpers.ts +117 -0
- package/src/runtime/agent-types.ts +22 -0
- package/src/runtime/approval-continuation.ts +16 -0
- package/src/runtime/chat-attachments.ts +46 -0
- package/src/runtime/chat-message.ts +10 -0
- package/src/runtime/chat-request-routing.ts +21 -0
- package/src/runtime/chat-run-orchestration.ts +25 -0
- package/src/runtime/chat-run-registry.ts +20 -0
- package/src/runtime/chat-types.ts +18 -0
- package/src/runtime/context-compaction-constants.ts +11 -0
- package/src/runtime/context-compaction-runtime.ts +86 -0
- package/src/runtime/context-compaction.ts +909 -0
- package/src/runtime/execution-plan.ts +59 -0
- package/src/runtime/helper-model.ts +405 -0
- package/src/runtime/indexed-repositories-policy.ts +28 -0
- package/src/runtime/instruction-sections.ts +8 -0
- package/src/runtime/llm-content.ts +71 -0
- package/src/runtime/memory-block.ts +264 -0
- package/src/runtime/memory-digest-policy.ts +14 -0
- package/src/runtime/memory-format.ts +8 -0
- package/src/runtime/memory-pipeline.ts +570 -0
- package/src/runtime/memory-prompts-fact.ts +47 -0
- package/src/runtime/memory-prompts-parse.ts +3 -0
- package/src/runtime/memory-prompts-update.ts +37 -0
- package/src/runtime/memory-scope.ts +43 -0
- package/src/runtime/plugin-types.ts +10 -0
- package/src/runtime/retrieval-adapters.ts +25 -0
- package/src/runtime/retrieval-pipeline.ts +3 -0
- package/src/runtime/runtime-extensions.ts +154 -0
- package/src/runtime/skill-extraction-policy.ts +3 -0
- package/src/runtime/team-consultation-orchestrator.ts +245 -0
- package/src/runtime/team-consultation-prompts.ts +32 -0
- package/src/runtime/title-helpers.ts +12 -0
- package/src/runtime/turn-lifecycle.ts +28 -0
- package/src/runtime/workstream-chat-helpers.ts +187 -0
- package/src/runtime/workstream-routing-policy.ts +301 -0
- package/src/runtime/workstream-state.ts +261 -0
- package/src/services/attachment.service.ts +159 -0
- package/src/services/chat-attachments.service.ts +17 -0
- package/src/services/chat-run-registry.service.ts +3 -0
- package/src/services/context-compaction-runtime.ts +13 -0
- package/src/services/context-compaction.service.ts +115 -0
- package/src/services/document-chunk.service.ts +141 -0
- package/src/services/execution-plan.service.ts +890 -0
- package/src/services/learned-skill.service.ts +328 -0
- package/src/services/memory-assessment.service.ts +43 -0
- package/src/services/memory.service.ts +807 -0
- package/src/services/memory.utils.ts +84 -0
- package/src/services/mutating-approval.service.ts +110 -0
- package/src/services/recent-activity-title.service.ts +74 -0
- package/src/services/recent-activity.service.ts +397 -0
- package/src/services/workstream-change-tracker.service.ts +313 -0
- package/src/services/workstream-message.service.ts +283 -0
- package/src/services/workstream-title.service.ts +58 -0
- package/src/services/workstream-turn-preparation.ts +1340 -0
- package/src/services/workstream-turn.ts +37 -0
- package/src/services/workstream.service.ts +854 -0
- package/src/services/workstream.types.ts +118 -0
- package/src/storage/attachment-parser.ts +101 -0
- package/src/storage/attachment-storage.service.ts +391 -0
- package/src/storage/attachments.types.ts +11 -0
- package/src/storage/attachments.utils.ts +58 -0
- package/src/storage/generated-document-storage.service.ts +55 -0
- package/src/system-agents/agent-result.ts +27 -0
- package/src/system-agents/context-compacter.agent.ts +46 -0
- package/src/system-agents/delegated-agent-factory.ts +177 -0
- package/src/system-agents/helper-agent-options.ts +20 -0
- package/src/system-agents/memory-reranker.agent.ts +38 -0
- package/src/system-agents/memory.agent.ts +58 -0
- package/src/system-agents/recent-activity-title-refiner.agent.ts +53 -0
- package/src/system-agents/regular-chat-memory-digest.agent.ts +75 -0
- package/src/system-agents/researcher.agent.ts +34 -0
- package/src/system-agents/skill-extractor.agent.ts +88 -0
- package/src/system-agents/skill-manager.agent.ts +80 -0
- package/src/system-agents/title-generator.agent.ts +42 -0
- package/src/system-agents/workstream-tracker.agent.ts +58 -0
- package/src/tools/execution-plan.tool.ts +163 -0
- package/src/tools/fetch-webpage.tool.ts +132 -0
- package/src/tools/firecrawl-client.ts +12 -0
- package/src/tools/memory-block.tool.ts +55 -0
- package/src/tools/read-file-parts.tool.ts +80 -0
- package/src/tools/remember-memory.tool.ts +85 -0
- package/src/tools/research-topic.tool.ts +15 -0
- package/src/tools/search-tools.ts +55 -0
- package/src/tools/search-web.tool.ts +175 -0
- package/src/tools/team-think.tool.ts +125 -0
- package/src/tools/tool-contract.ts +21 -0
- package/src/tools/user-questions.tool.ts +18 -0
- package/src/utils/async.ts +50 -0
- package/src/utils/date-time.ts +34 -0
- package/src/utils/error.ts +10 -0
- package/src/utils/errors.ts +28 -0
- package/src/utils/hono-error-handler.ts +71 -0
- package/src/utils/string.ts +51 -0
- package/src/workers/bootstrap.ts +44 -0
- package/src/workers/memory-consolidation.worker.ts +318 -0
- package/src/workers/regular-chat-memory-digest.helpers.ts +100 -0
- package/src/workers/regular-chat-memory-digest.runner.ts +363 -0
- package/src/workers/regular-chat-memory-digest.worker.ts +22 -0
- package/src/workers/skill-extraction.runner.ts +331 -0
- package/src/workers/skill-extraction.worker.ts +22 -0
- package/src/workers/utils/repo-indexer-chunker.ts +331 -0
- package/src/workers/utils/repo-structure-extractor.ts +645 -0
- package/src/workers/utils/repomix-process-concurrency.ts +65 -0
- package/src/workers/utils/sandbox-error.ts +5 -0
- package/src/workers/worker-utils.ts +182 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { ToolLoopAgent } from 'ai'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
import { bifrostOpenRouterResponseHealingModel } from '../bifrost/bifrost'
|
|
5
|
+
import {
|
|
6
|
+
OPENROUTER_STRUCTURED_HELPER_MODEL_ID,
|
|
7
|
+
OPENROUTER_HIGH_REASONING_PROVIDER_OPTIONS,
|
|
8
|
+
} from '../config/model-constants'
|
|
9
|
+
import type { CreateHelperToolLoopAgentOptions } from '../runtime/agent-types'
|
|
10
|
+
import { resolveHelperAgentOptions } from './helper-agent-options'
|
|
11
|
+
|
|
12
|
+
const SKILL_MANAGER_MAX_TOKENS = 4_096
|
|
13
|
+
|
|
14
|
+
export const skillManagerPrompt = `<agent-instructions>
|
|
15
|
+
You are the skill manager.
|
|
16
|
+
|
|
17
|
+
<goal>
|
|
18
|
+
Given a candidate skill and optionally the most similar existing skill, decide whether to add, merge, or discard.
|
|
19
|
+
</goal>
|
|
20
|
+
|
|
21
|
+
<decision-axes>
|
|
22
|
+
Compare on four axes:
|
|
23
|
+
1. Job-to-be-done overlap — do both skills serve the same user intent?
|
|
24
|
+
2. Deliverable type similarity — do both produce the same kind of output?
|
|
25
|
+
3. Hard constraints/success criteria match — do they share quality gates or validation rules?
|
|
26
|
+
4. Required workflow overlap — do they follow similar procedural steps?
|
|
27
|
+
</decision-axes>
|
|
28
|
+
|
|
29
|
+
<decisions>
|
|
30
|
+
- add: the candidate is a genuinely new capability not covered by the existing skill
|
|
31
|
+
- merge: the candidate refines or extends the existing skill — produce merged instructions that combine both
|
|
32
|
+
- discard: the candidate is too noisy, already fully covered, or non-portable
|
|
33
|
+
</decisions>
|
|
34
|
+
|
|
35
|
+
<merge-rules>
|
|
36
|
+
When merging:
|
|
37
|
+
- Combine instructions from both, keeping the best parts of each
|
|
38
|
+
- Increment version number
|
|
39
|
+
- Update description to reflect the merged capability
|
|
40
|
+
- Keep triggers and examples from both, deduplicated
|
|
41
|
+
- Use the higher confidence of the two
|
|
42
|
+
</merge-rules>
|
|
43
|
+
|
|
44
|
+
<output-contract>
|
|
45
|
+
Return a JSON object with:
|
|
46
|
+
- decision: 'add' | 'merge' | 'discard'
|
|
47
|
+
- reason: 1-2 sentence explanation
|
|
48
|
+
- mergedSkill: (only when decision is 'merge') object with merged name, description, instructions, triggers, tags, examples, confidence
|
|
49
|
+
</output-contract>
|
|
50
|
+
</agent-instructions>`
|
|
51
|
+
|
|
52
|
+
export const MergedSkillSchema = z.object({
|
|
53
|
+
name: z.string(),
|
|
54
|
+
description: z.string(),
|
|
55
|
+
instructions: z.string(),
|
|
56
|
+
triggers: z.array(z.string()),
|
|
57
|
+
tags: z.array(z.string()),
|
|
58
|
+
examples: z.array(z.string()),
|
|
59
|
+
confidence: z.number().min(0).max(1),
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
export const SkillManagerOutputSchema = z.object({
|
|
63
|
+
decision: z.enum(['add', 'merge', 'discard']),
|
|
64
|
+
reason: z.string(),
|
|
65
|
+
mergedSkill: MergedSkillSchema.optional(),
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
export type SkillManagerOutput = z.infer<typeof SkillManagerOutputSchema>
|
|
69
|
+
|
|
70
|
+
export function createSkillManagerAgent(options: CreateHelperToolLoopAgentOptions) {
|
|
71
|
+
return new ToolLoopAgent({
|
|
72
|
+
id: 'skill-manager',
|
|
73
|
+
model: bifrostOpenRouterResponseHealingModel(OPENROUTER_STRUCTURED_HELPER_MODEL_ID),
|
|
74
|
+
providerOptions: OPENROUTER_HIGH_REASONING_PROVIDER_OPTIONS,
|
|
75
|
+
...resolveHelperAgentOptions(options, {
|
|
76
|
+
instructions: skillManagerPrompt,
|
|
77
|
+
maxOutputTokens: SKILL_MANAGER_MAX_TOKENS,
|
|
78
|
+
}),
|
|
79
|
+
})
|
|
80
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ToolLoopAgent } from 'ai'
|
|
2
|
+
|
|
3
|
+
import { bifrostModel } from '../bifrost/bifrost'
|
|
4
|
+
import {
|
|
5
|
+
OPENROUTER_FAST_REASONING_MODEL_ID,
|
|
6
|
+
OPENROUTER_MINIMAL_REASONING_PROVIDER_OPTIONS,
|
|
7
|
+
} from '../config/model-constants'
|
|
8
|
+
import type { CreateHelperToolLoopAgentOptions } from '../runtime/agent-types'
|
|
9
|
+
import { resolveHelperAgentOptions } from './helper-agent-options'
|
|
10
|
+
|
|
11
|
+
const TITLE_MAX_TOKENS = 500
|
|
12
|
+
|
|
13
|
+
const WORKSTREAM_TITLE_GENERATOR_PROMPT = `<agent-instructions>
|
|
14
|
+
You are a **Title Generator** that creates concise chat titles.
|
|
15
|
+
|
|
16
|
+
<task>
|
|
17
|
+
Generate a chat title based only on the user's message.
|
|
18
|
+
</task>
|
|
19
|
+
|
|
20
|
+
<constraints>
|
|
21
|
+
- Maximum 4-5 words
|
|
22
|
+
- Capture the core workstream or intent
|
|
23
|
+
- Use natural, readable language
|
|
24
|
+
- No punctuation at the end
|
|
25
|
+
</constraints>
|
|
26
|
+
|
|
27
|
+
<output-format>
|
|
28
|
+
Return only the title text. No quotes, no labels, no explanation.
|
|
29
|
+
</output-format>
|
|
30
|
+
</agent-instructions>`
|
|
31
|
+
|
|
32
|
+
export function createTitleGeneratorAgent(options: CreateHelperToolLoopAgentOptions) {
|
|
33
|
+
return new ToolLoopAgent({
|
|
34
|
+
id: 'title-generator',
|
|
35
|
+
model: bifrostModel(OPENROUTER_FAST_REASONING_MODEL_ID),
|
|
36
|
+
providerOptions: OPENROUTER_MINIMAL_REASONING_PROVIDER_OPTIONS,
|
|
37
|
+
...resolveHelperAgentOptions(options, {
|
|
38
|
+
instructions: WORKSTREAM_TITLE_GENERATOR_PROMPT,
|
|
39
|
+
maxOutputTokens: TITLE_MAX_TOKENS,
|
|
40
|
+
}),
|
|
41
|
+
})
|
|
42
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ToolLoopAgent } from 'ai'
|
|
2
|
+
|
|
3
|
+
import { bifrostOpenRouterResponseHealingModel } from '../bifrost/bifrost'
|
|
4
|
+
import {
|
|
5
|
+
OPENROUTER_LOW_REASONING_PROVIDER_OPTIONS,
|
|
6
|
+
OPENROUTER_STRUCTURED_REASONING_MODEL_ID,
|
|
7
|
+
} from '../config/model-constants'
|
|
8
|
+
import type { CreateHelperToolLoopAgentOptions } from '../runtime/agent-types'
|
|
9
|
+
import { resolveHelperAgentOptions } from './helper-agent-options'
|
|
10
|
+
|
|
11
|
+
const WORKSTREAM_TRACKER_PROMPT = `<agent-instructions>
|
|
12
|
+
You are the **Workstream Tracker**.
|
|
13
|
+
|
|
14
|
+
<task>
|
|
15
|
+
Convert one completed workstream turn into:
|
|
16
|
+
- a concise change-tracker summary for the right sidebar
|
|
17
|
+
- a structured workstream state delta containing only the updates implied by this turn
|
|
18
|
+
</task>
|
|
19
|
+
|
|
20
|
+
<rules>
|
|
21
|
+
- Use only the provided turn evidence and prior state.
|
|
22
|
+
- Keep the summary short, concrete, and focused on what changed or what is blocked now.
|
|
23
|
+
- Prefer durable tracker items over prose: tasks, decisions, questions, risks, artifacts, and agent notes.
|
|
24
|
+
- Do not duplicate existing state unless this turn meaningfully changed it.
|
|
25
|
+
- Never invent analytics, repository facts, customer signals, or business decisions that were not stated.
|
|
26
|
+
</rules>
|
|
27
|
+
|
|
28
|
+
<state-guidance>
|
|
29
|
+
- Use taskUpdates for concrete next steps, ongoing work, completed work, or blocked work.
|
|
30
|
+
- When the turn includes concrete tool work or operational execution, capture that work as taskUpdates even if it was
|
|
31
|
+
completed in the same turn.
|
|
32
|
+
- For website inspection or plugin refresh flows, prefer task titles that describe the executed step clearly, such as
|
|
33
|
+
refreshing website intelligence, overwriting artifacts, or reviewing evidence gaps.
|
|
34
|
+
- If an owner is not explicit, default the task owner to the visible lead agent for the workstream.
|
|
35
|
+
- Use newDecisions only when a clear decision or tradeoff was made.
|
|
36
|
+
- Use newQuestions only for unresolved questions that matter for progress.
|
|
37
|
+
- Use newRisks only for concrete execution or business risks surfaced in this turn.
|
|
38
|
+
- Use agentNote for the main contribution from the lead agent when it is worth retaining.
|
|
39
|
+
- Keep the delta compact. A noisy tracker is worse than a sparse tracker.
|
|
40
|
+
</state-guidance>
|
|
41
|
+
|
|
42
|
+
<output>
|
|
43
|
+
The caller enforces a structured schema.
|
|
44
|
+
- Return every stateDelta field.
|
|
45
|
+
- Use empty arrays for unchanged list fields.
|
|
46
|
+
- Use null for unchanged nullable fields.
|
|
47
|
+
- For currentPlan, use \`{"action":"unchanged","text":null}\` when nothing changed, \`{"action":"clear","text":null}\` to clear it, and \`{"action":"set","text":"..."}\` to replace it.
|
|
48
|
+
</output>
|
|
49
|
+
</agent-instructions>`
|
|
50
|
+
|
|
51
|
+
export function createWorkstreamTrackerAgent(options: CreateHelperToolLoopAgentOptions) {
|
|
52
|
+
return new ToolLoopAgent({
|
|
53
|
+
id: 'workstream-tracker',
|
|
54
|
+
model: bifrostOpenRouterResponseHealingModel(OPENROUTER_STRUCTURED_REASONING_MODEL_ID),
|
|
55
|
+
providerOptions: OPENROUTER_LOW_REASONING_PROVIDER_OPTIONS,
|
|
56
|
+
...resolveHelperAgentOptions(options, { instructions: WORKSTREAM_TRACKER_PROMPT }),
|
|
57
|
+
})
|
|
58
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CreateExecutionPlanArgsSchema,
|
|
3
|
+
GetActiveExecutionPlanArgsSchema,
|
|
4
|
+
RestartExecutionTaskArgsSchema,
|
|
5
|
+
ReplaceExecutionPlanArgsSchema,
|
|
6
|
+
SetExecutionTaskStatusArgsSchema,
|
|
7
|
+
} from '@lota-sdk/shared/schemas/tools'
|
|
8
|
+
import type { ExecutionPlanToolResultData } from '@lota-sdk/shared/schemas/tools'
|
|
9
|
+
import { tool } from 'ai'
|
|
10
|
+
|
|
11
|
+
import type { RecordIdRef } from '../db/record-id'
|
|
12
|
+
import { executionPlanService } from '../services/execution-plan.service'
|
|
13
|
+
|
|
14
|
+
function getLatestExecutionPlanToolResult(output: unknown): ExecutionPlanToolResultData | undefined {
|
|
15
|
+
if (output && typeof output === 'object' && 'hasPlan' in output) {
|
|
16
|
+
return output as ExecutionPlanToolResultData
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (Array.isArray(output)) {
|
|
20
|
+
for (let index = output.length - 1; index >= 0; index -= 1) {
|
|
21
|
+
const candidate = getLatestExecutionPlanToolResult(output[index])
|
|
22
|
+
if (candidate) return candidate
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createCreateExecutionPlanTool(params: {
|
|
30
|
+
orgId: RecordIdRef
|
|
31
|
+
workstreamId: RecordIdRef
|
|
32
|
+
agentId: string
|
|
33
|
+
onPlanChanged?: () => void
|
|
34
|
+
}) {
|
|
35
|
+
return tool({
|
|
36
|
+
description:
|
|
37
|
+
'Create a structured execution plan for multi-step work in this workstream. Use this before starting multi-step execution.',
|
|
38
|
+
inputSchema: CreateExecutionPlanArgsSchema,
|
|
39
|
+
execute: async (input) => {
|
|
40
|
+
const result = await executionPlanService.createPlan({
|
|
41
|
+
organizationId: params.orgId,
|
|
42
|
+
workstreamId: params.workstreamId,
|
|
43
|
+
leadAgentId: params.agentId,
|
|
44
|
+
input,
|
|
45
|
+
})
|
|
46
|
+
params.onPlanChanged?.()
|
|
47
|
+
return result
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createReplaceExecutionPlanTool(params: {
|
|
53
|
+
orgId: RecordIdRef
|
|
54
|
+
workstreamId: RecordIdRef
|
|
55
|
+
agentId: string
|
|
56
|
+
onPlanChanged?: () => void
|
|
57
|
+
}) {
|
|
58
|
+
return tool({
|
|
59
|
+
description: 'Replace the active execution plan when the ordered steps or approach have materially changed.',
|
|
60
|
+
inputSchema: ReplaceExecutionPlanArgsSchema,
|
|
61
|
+
execute: async (input) => {
|
|
62
|
+
const result = await executionPlanService.replacePlan({
|
|
63
|
+
organizationId: params.orgId,
|
|
64
|
+
workstreamId: params.workstreamId,
|
|
65
|
+
leadAgentId: params.agentId,
|
|
66
|
+
input,
|
|
67
|
+
})
|
|
68
|
+
params.onPlanChanged?.()
|
|
69
|
+
return result
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createSetExecutionTaskStatusTool(params: {
|
|
75
|
+
workstreamId: RecordIdRef
|
|
76
|
+
agentId: string
|
|
77
|
+
onPlanChanged?: () => void
|
|
78
|
+
}) {
|
|
79
|
+
return tool({
|
|
80
|
+
description:
|
|
81
|
+
'Update the active execution-plan task state, including in-progress, completed, blocked, failed, or skipped outcomes.',
|
|
82
|
+
inputSchema: SetExecutionTaskStatusArgsSchema,
|
|
83
|
+
execute: async function* (input) {
|
|
84
|
+
const activePlan = await executionPlanService.getActivePlanToolResult({
|
|
85
|
+
workstreamId: params.workstreamId,
|
|
86
|
+
includeEvents: true,
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
if (activePlan.plan && activePlan.plan.planId === input.planId) {
|
|
90
|
+
const optimisticPlan = structuredClone(activePlan.plan)
|
|
91
|
+
const targetTask = optimisticPlan.tasks.find((task) => task.id === input.taskId)
|
|
92
|
+
|
|
93
|
+
if (targetTask) {
|
|
94
|
+
targetTask.status = input.status
|
|
95
|
+
if (input.resultStatus !== undefined) targetTask.resultStatus = input.resultStatus
|
|
96
|
+
if (input.outputSummary !== undefined) targetTask.outputSummary = input.outputSummary || undefined
|
|
97
|
+
if (input.blockedReason !== undefined) targetTask.blockedReason = input.blockedReason || undefined
|
|
98
|
+
if (input.externalRef !== undefined) targetTask.externalRef = input.externalRef || undefined
|
|
99
|
+
if (input.status === 'in-progress') {
|
|
100
|
+
optimisticPlan.status = 'executing'
|
|
101
|
+
optimisticPlan.currentTaskId = targetTask.id
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
yield {
|
|
105
|
+
action: 'task-status-updated',
|
|
106
|
+
message: `Updating task "${targetTask.title}".`,
|
|
107
|
+
changedTaskId: targetTask.id,
|
|
108
|
+
plan: optimisticPlan,
|
|
109
|
+
hasPlan: true,
|
|
110
|
+
status: optimisticPlan.status,
|
|
111
|
+
} satisfies ExecutionPlanToolResultData
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const result = await executionPlanService.setTaskStatus({
|
|
116
|
+
workstreamId: params.workstreamId,
|
|
117
|
+
emittedBy: params.agentId,
|
|
118
|
+
input,
|
|
119
|
+
})
|
|
120
|
+
params.onPlanChanged?.()
|
|
121
|
+
return result
|
|
122
|
+
},
|
|
123
|
+
toModelOutput: ({ output }) => {
|
|
124
|
+
const result = getLatestExecutionPlanToolResult(output)
|
|
125
|
+
const summary = result?.message?.trim()
|
|
126
|
+
return { type: 'text', value: summary && summary.length > 0 ? summary : 'Execution task status updated.' }
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function createGetActiveExecutionPlanTool(params: { workstreamId: RecordIdRef }) {
|
|
132
|
+
return tool({
|
|
133
|
+
description:
|
|
134
|
+
'Load the active execution plan for this workstream, including task state and recent events when needed.',
|
|
135
|
+
inputSchema: GetActiveExecutionPlanArgsSchema,
|
|
136
|
+
execute: async (input) =>
|
|
137
|
+
await executionPlanService.getActivePlanToolResult({
|
|
138
|
+
workstreamId: params.workstreamId,
|
|
139
|
+
includeEvents: input.includeEvents,
|
|
140
|
+
}),
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function createRestartExecutionTaskTool(params: {
|
|
145
|
+
workstreamId: RecordIdRef
|
|
146
|
+
agentId: string
|
|
147
|
+
onPlanChanged?: () => void
|
|
148
|
+
}) {
|
|
149
|
+
return tool({
|
|
150
|
+
description:
|
|
151
|
+
'Restart a failed or blocked execution-plan task and optionally reset downstream tasks back to pending.',
|
|
152
|
+
inputSchema: RestartExecutionTaskArgsSchema,
|
|
153
|
+
execute: async (input) => {
|
|
154
|
+
const result = await executionPlanService.restartTask({
|
|
155
|
+
workstreamId: params.workstreamId,
|
|
156
|
+
emittedBy: params.agentId,
|
|
157
|
+
input,
|
|
158
|
+
})
|
|
159
|
+
params.onPlanChanged?.()
|
|
160
|
+
return result
|
|
161
|
+
},
|
|
162
|
+
})
|
|
163
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { tool } from 'ai'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
import type { ToolDefinition } from '../ai/definitions'
|
|
5
|
+
import type { Citation } from '../services/workstream.types'
|
|
6
|
+
import { withTimeout } from '../utils/async'
|
|
7
|
+
import { readStringField, truncateOptionalText } from '../utils/string'
|
|
8
|
+
import { getFirecrawlClient } from './firecrawl-client'
|
|
9
|
+
|
|
10
|
+
const TOOL_TIMEOUT_MS = 30_000
|
|
11
|
+
const FormatSchema = z.enum(['markdown', 'html', 'rawHtml', 'links', 'images', 'screenshot', 'summary'])
|
|
12
|
+
const MAX_MARKDOWN_CHARS = 6_000
|
|
13
|
+
const MAX_SUMMARY_CHARS = 1_200
|
|
14
|
+
const MAX_LINKS = 25
|
|
15
|
+
const MAX_IMAGES = 10
|
|
16
|
+
|
|
17
|
+
function readStringList(record: Record<string, unknown>, key: string, maxItems: number): string[] {
|
|
18
|
+
const value = record[key]
|
|
19
|
+
if (!Array.isArray(value)) return []
|
|
20
|
+
|
|
21
|
+
return value
|
|
22
|
+
.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0)
|
|
23
|
+
.slice(0, maxItems)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function summarizeDocument(url: string, document: unknown): Record<string, unknown> {
|
|
27
|
+
if (!document || typeof document !== 'object' || Array.isArray(document)) {
|
|
28
|
+
return { url }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const record = document as Record<string, unknown>
|
|
32
|
+
const metadata =
|
|
33
|
+
record.metadata && typeof record.metadata === 'object' && !Array.isArray(record.metadata)
|
|
34
|
+
? (record.metadata as Record<string, unknown>)
|
|
35
|
+
: {}
|
|
36
|
+
|
|
37
|
+
const canonicalUrl = truncateOptionalText(
|
|
38
|
+
readStringField(metadata, 'url') ?? readStringField(metadata, 'sourceURL') ?? readStringField(record, 'url') ?? url,
|
|
39
|
+
500,
|
|
40
|
+
)
|
|
41
|
+
const title = truncateOptionalText(readStringField(metadata, 'title') ?? readStringField(record, 'title'), 160)
|
|
42
|
+
const description = truncateOptionalText(readStringField(metadata, 'description'), 320)
|
|
43
|
+
const summary = truncateOptionalText(readStringField(record, 'summary'), MAX_SUMMARY_CHARS)
|
|
44
|
+
const markdownExcerpt = truncateOptionalText(readStringField(record, 'markdown'), MAX_MARKDOWN_CHARS)
|
|
45
|
+
const links = readStringList(record, 'links', MAX_LINKS)
|
|
46
|
+
|
|
47
|
+
const images = Array.isArray(record.images)
|
|
48
|
+
? record.images
|
|
49
|
+
.map((image) => {
|
|
50
|
+
if (typeof image === 'string') {
|
|
51
|
+
return truncateOptionalText(image, 500)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (image && typeof image === 'object' && !Array.isArray(image)) {
|
|
55
|
+
const imageRecord = image as Record<string, unknown>
|
|
56
|
+
return truncateOptionalText(readStringField(imageRecord, 'src') ?? readStringField(imageRecord, 'url'), 500)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return undefined
|
|
60
|
+
})
|
|
61
|
+
.filter((image): image is string => typeof image === 'string' && image.length > 0)
|
|
62
|
+
.slice(0, MAX_IMAGES)
|
|
63
|
+
: []
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
url: canonicalUrl ?? url,
|
|
67
|
+
...(title ? { title } : {}),
|
|
68
|
+
...(description ? { description } : {}),
|
|
69
|
+
...(summary ? { summary } : {}),
|
|
70
|
+
...(markdownExcerpt ? { markdownExcerpt } : {}),
|
|
71
|
+
...(links.length > 0 ? { links } : {}),
|
|
72
|
+
...(images.length > 0 ? { images } : {}),
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function buildFetchCitations(url: string, document: unknown): Citation[] {
|
|
77
|
+
const fallbackUrl = url.trim()
|
|
78
|
+
let sourceId = fallbackUrl
|
|
79
|
+
|
|
80
|
+
if (document && typeof document === 'object') {
|
|
81
|
+
const metadata = (document as Record<string, unknown>).metadata
|
|
82
|
+
if (metadata && typeof metadata === 'object') {
|
|
83
|
+
const record = metadata as Record<string, unknown>
|
|
84
|
+
if (typeof record.url === 'string' && record.url.trim().length > 0) {
|
|
85
|
+
sourceId = record.url.trim()
|
|
86
|
+
} else if (typeof record.sourceURL === 'string' && record.sourceURL.trim().length > 0) {
|
|
87
|
+
sourceId = record.sourceURL.trim()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return [{ source: 'web', sourceId, retrievedAt: new Date().toISOString() }]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const fetchWebpageTool = {
|
|
96
|
+
name: 'fetchWebpage',
|
|
97
|
+
create: () =>
|
|
98
|
+
tool({
|
|
99
|
+
description: 'Retrieve and parse a single webpage.',
|
|
100
|
+
inputSchema: z
|
|
101
|
+
.object({
|
|
102
|
+
url: z.url('Valid URL is required'),
|
|
103
|
+
formats: z.array(FormatSchema).optional(),
|
|
104
|
+
onlyMainContent: z.boolean().optional(),
|
|
105
|
+
maxAge: z.number().int().min(1).optional(),
|
|
106
|
+
})
|
|
107
|
+
.strict(),
|
|
108
|
+
execute: async ({
|
|
109
|
+
url,
|
|
110
|
+
formats,
|
|
111
|
+
onlyMainContent,
|
|
112
|
+
maxAge,
|
|
113
|
+
}: {
|
|
114
|
+
url: string
|
|
115
|
+
formats?: z.infer<typeof FormatSchema>[]
|
|
116
|
+
onlyMainContent?: boolean
|
|
117
|
+
maxAge?: number
|
|
118
|
+
}) => {
|
|
119
|
+
const result = await withTimeout(
|
|
120
|
+
getFirecrawlClient().scrape(url, {
|
|
121
|
+
formats: formats?.length ? formats : ['markdown'],
|
|
122
|
+
onlyMainContent: onlyMainContent ?? true,
|
|
123
|
+
maxAge,
|
|
124
|
+
}),
|
|
125
|
+
TOOL_TIMEOUT_MS,
|
|
126
|
+
'Webpage fetch',
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return { url, document: summarizeDocument(url, result), citations: buildFetchCitations(url, result) }
|
|
130
|
+
},
|
|
131
|
+
}),
|
|
132
|
+
} as const satisfies ToolDefinition<void>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import Firecrawl from '@mendable/firecrawl-js'
|
|
2
|
+
|
|
3
|
+
import { env } from '../config/env-shapes'
|
|
4
|
+
|
|
5
|
+
let _firecrawlClient: Firecrawl | undefined
|
|
6
|
+
|
|
7
|
+
export function getFirecrawlClient(): Firecrawl {
|
|
8
|
+
if (!_firecrawlClient) {
|
|
9
|
+
_firecrawlClient = new Firecrawl({ apiKey: env.FIRECRAWL_API_KEY })
|
|
10
|
+
}
|
|
11
|
+
return _firecrawlClient
|
|
12
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { tool } from 'ai'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
import type { RecordIdRef } from '../db/record-id'
|
|
5
|
+
import {
|
|
6
|
+
hasMemoryBlockEntry,
|
|
7
|
+
normalizeMemoryBlockEntry,
|
|
8
|
+
prepareMemoryBlockAppend,
|
|
9
|
+
validateMemoryBlockEntry,
|
|
10
|
+
} from '../runtime/memory-block'
|
|
11
|
+
import { workstreamService } from '../services/workstream.service'
|
|
12
|
+
import { safeEnqueue } from '../utils/async'
|
|
13
|
+
|
|
14
|
+
export function createMemoryBlockTool({
|
|
15
|
+
workstreamId,
|
|
16
|
+
agentLabel,
|
|
17
|
+
getCurrentBlock,
|
|
18
|
+
onAppend,
|
|
19
|
+
}: {
|
|
20
|
+
workstreamId: RecordIdRef
|
|
21
|
+
agentLabel: string
|
|
22
|
+
getCurrentBlock?: () => string
|
|
23
|
+
onAppend?: (value: string) => void
|
|
24
|
+
}) {
|
|
25
|
+
return tool({
|
|
26
|
+
description:
|
|
27
|
+
'Append a critical 2-3 sentence note to the conversation memory block. Include only essential information.',
|
|
28
|
+
inputSchema: z.object({ entry: z.string().min(1) }).strict(),
|
|
29
|
+
execute: ({ entry }: { entry: string }) => {
|
|
30
|
+
const prepared = prepareMemoryBlockAppend({
|
|
31
|
+
entry,
|
|
32
|
+
agentLabel,
|
|
33
|
+
currentBlock: getCurrentBlock?.() ?? '',
|
|
34
|
+
normalizeEntry: normalizeMemoryBlockEntry,
|
|
35
|
+
validateEntry: validateMemoryBlockEntry,
|
|
36
|
+
hasEntry: hasMemoryBlockEntry,
|
|
37
|
+
})
|
|
38
|
+
if (prepared.skipped) {
|
|
39
|
+
return { skipped: true, reason: prepared.reason }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
onAppend?.(prepared.optimisticBlock)
|
|
43
|
+
|
|
44
|
+
void safeEnqueue(
|
|
45
|
+
async () => {
|
|
46
|
+
const updated = await workstreamService.appendMemoryBlock(workstreamId, prepared.formatted)
|
|
47
|
+
onAppend?.(updated)
|
|
48
|
+
},
|
|
49
|
+
{ operationName: 'append memory block entry', logPrefix: 'Background memoryBlockAppend task failed' },
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return { success: true, status: 'scheduled', appendedBy: prepared.appendedBy }
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { tool } from 'ai'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
import type { ToolDefinition } from '../ai/definitions'
|
|
5
|
+
import { attachmentStorageService } from '../storage/attachment-storage.service'
|
|
6
|
+
import type { ReadableUploadMetadata } from '../storage/attachments.types'
|
|
7
|
+
|
|
8
|
+
const PAGES_PER_PART = 25
|
|
9
|
+
|
|
10
|
+
export interface ReadFilePartsToolContext {
|
|
11
|
+
orgId: string
|
|
12
|
+
userId: string
|
|
13
|
+
uploads: ReadableUploadMetadata[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function toUploadSummary(upload: ReadableUploadMetadata) {
|
|
17
|
+
return {
|
|
18
|
+
storageKey: upload.storageKey,
|
|
19
|
+
filename: upload.filename,
|
|
20
|
+
mediaType: upload.mediaType,
|
|
21
|
+
sizeBytes: upload.sizeBytes,
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolveUploadTarget(params: {
|
|
26
|
+
uploads: ReadableUploadMetadata[]
|
|
27
|
+
storageKey?: string
|
|
28
|
+
}): ReadableUploadMetadata | null {
|
|
29
|
+
const normalizedStorageKey = params.storageKey?.trim()
|
|
30
|
+
const byStorageKey = normalizedStorageKey
|
|
31
|
+
? params.uploads.find((upload) => upload.storageKey === normalizedStorageKey)
|
|
32
|
+
: null
|
|
33
|
+
if (normalizedStorageKey !== undefined && !byStorageKey) {
|
|
34
|
+
throw new Error(`No upload found for storageKey "${normalizedStorageKey}".`)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return byStorageKey ?? null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const readFilePartsTool = {
|
|
41
|
+
name: 'readFileParts',
|
|
42
|
+
create: ({ orgId, userId, uploads }: ReadFilePartsToolContext) =>
|
|
43
|
+
tool({
|
|
44
|
+
description:
|
|
45
|
+
'Read uploaded file content by part. Call with no args to list uploads metadata, then call with storageKey and part (25 pages per part).',
|
|
46
|
+
inputSchema: z
|
|
47
|
+
.object({ storageKey: z.string().trim().min(1).optional(), part: z.number().int().positive().default(1) })
|
|
48
|
+
.strict(),
|
|
49
|
+
execute: async ({ storageKey, part }: { storageKey?: string; part?: number }) => {
|
|
50
|
+
const availableUploads = uploads.map(toUploadSummary)
|
|
51
|
+
const selected = resolveUploadTarget({ uploads, storageKey })
|
|
52
|
+
|
|
53
|
+
if (!selected) {
|
|
54
|
+
return {
|
|
55
|
+
pagesPerPart: PAGES_PER_PART,
|
|
56
|
+
currentPart: 1,
|
|
57
|
+
hasNextPart: false,
|
|
58
|
+
totalParts: 0,
|
|
59
|
+
data: [],
|
|
60
|
+
availableUploads,
|
|
61
|
+
selectedUpload: null,
|
|
62
|
+
message:
|
|
63
|
+
uploads.length === 0
|
|
64
|
+
? 'No uploaded files are available in this conversation.'
|
|
65
|
+
: 'Select an upload via storageKey, then provide part (25 pages per part).',
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const parts = await attachmentStorageService.readFilePartsFromUpload({
|
|
70
|
+
upload: selected,
|
|
71
|
+
orgId,
|
|
72
|
+
userId,
|
|
73
|
+
part,
|
|
74
|
+
pagesPerPart: PAGES_PER_PART,
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
return { availableUploads, selectedUpload: toUploadSummary(selected), ...parts }
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
} as const satisfies ToolDefinition<ReadFilePartsToolContext>
|