Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- TypeScript sources in `packages/*/src`, tests in `packages/*/src/__tests__`.
- Always prefer `bun` over `npm`.
- When fixing unused-parameter errors, delete the parameter rather than prefixing with `_`.
- Landing page copy (`packages/landing`): never use em dashes (`—`) in user-facing text. Rephrase with commas, periods, or restructure the sentence.

### Submodules
`packages/owletto` is a submodule of `lobu-ai/owletto`. Push the submodule change to a reachable branch first, then bump the pointer in the parent — an unreachable SHA breaks production cloning.
Expand Down
11 changes: 9 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,40 +1,50 @@
/**
* Reaction for the `reconciliation-monitor` watcher.
*
* Persists any variance flagged during the daily 6am sweep as a durable
* `variance_flag` event tied to the affected account. Downstream agents
* (close-of-month rollup, audit prep) consume these events instead of
* re-extracting variances from the raw transaction stream.
* Persists variance events when unreconciled transactions or new anomalies
* are detected during the daily reconciliation pass.
*/
import type { ReactionContext } from "@lobu/connector-sdk";
import type { ReactionClient, ReactionContext } from "@lobu/connector-sdk";

interface ReconciliationData {
variances?: Array<{
account: string;
amount: number;
direction: "over" | "under";
reason: string;
}>;
unreconciled_count?: number;
unreconciled_count: number;
new_variances: string[];
approaching_deadlines: string[];
payment_risks?: string[];
}

export default async (ctx: ReactionContext, client: any): Promise<void> => {
export default async (
ctx: ReactionContext,
client: ReactionClient
): Promise<void> => {
const data = ctx.extracted_data as ReconciliationData;
const variances = data.variances ?? [];
if (variances.length === 0) return;

for (const v of variances) {
await client.knowledge.save({
entity_ids: ctx.entities.map((e) => e.id),
content: `Variance ${v.direction} on ${v.account}: ${v.amount} — ${v.reason}`,
semantic_type: "variance_flag",
metadata: {
account: v.account,
amount: v.amount,
direction: v.direction,
window_id: ctx.window.id,
unreconciled_count: data.unreconciled_count ?? null,
},
});
const hasIssues =
data.unreconciled_count > 0 ||
(data.new_variances?.length ?? 0) > 0 ||
(data.approaching_deadlines?.length ?? 0) > 0;
Comment on lines +22 to +25
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Include payment_risks in issue detection and alert payload.

payment_risks is part of ReconciliationData but is ignored in Line 22-Line 25 and in the saved alert body/metadata, so risk-only windows can be silently dropped.

Suggested fix
   const hasIssues =
     data.unreconciled_count > 0 ||
     (data.new_variances?.length ?? 0) > 0 ||
-    (data.approaching_deadlines?.length ?? 0) > 0;
+    (data.approaching_deadlines?.length ?? 0) > 0 ||
+    (data.payment_risks?.length ?? 0) > 0;
@@
   if (data.approaching_deadlines?.length) {
     parts.push(`Deadlines: ${data.approaching_deadlines.join("; ")}`);
   }
+  if (data.payment_risks?.length) {
+    parts.push(`Payment risks: ${data.payment_risks.join("; ")}`);
+  }
@@
       unreconciled_count: data.unreconciled_count,
       variance_count: data.new_variances?.length ?? 0,
+      payment_risk_count: data.payment_risks?.length ?? 0,
     },
   });

Also applies to: 36-38, 44-48

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/finance/models/reactions/reconciliation-monitor.reaction.ts` around
lines 22 - 25, The issue: windows with only payment_risks are ignored; update
the detection and alert payload to include payment_risks. Modify the boolean
computation in hasIssues (the expression that references
data.unreconciled_count, data.new_variances, data.approaching_deadlines) to also
check (data.payment_risks?.length ?? 0) > 0, and ensure the saved alert
body/metadata (the object constructed where the alert payload is built further
down in the reaction) includes payment_risks so risk-only windows are captured
and persisted; update the analogous checks at the other occurrences mentioned
(the blocks around lines where hasIssues is evaluated and where the alert
body/metadata are assembled).


if (!hasIssues) return;

const parts: string[] = [];
if (data.unreconciled_count > 0) {
parts.push(`${data.unreconciled_count} unreconciled transactions`);
}
if (data.new_variances?.length) {
parts.push(`Variances: ${data.new_variances.join("; ")}`);
}
if (data.approaching_deadlines?.length) {
parts.push(`Deadlines: ${data.approaching_deadlines.join("; ")}`);
}

await client.knowledge.save({
entity_ids: ctx.entities.map((e) => e.id),
content: parts.join("\n"),
semantic_type: "reconciliation_alert",
metadata: {
window_id: ctx.window.id,
unreconciled_count: data.unreconciled_count,
variance_count: data.new_variances?.length ?? 0,
},
});
};
37 changes: 37 additions & 0 deletions examples/lobu-crm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# lobu-crm — Reference example

A funnel CRM agent that tracks GitHub stars, X mentions, HN posts, and demo-form submissions.
Use this as a starting point for new projects — it shows every Lobu concept in one place.

## Structure

```
lobu-crm/
├── lobu.toml # Agent + memory config
├── connectors/
│ ├── github.yaml # Built-in connector (just config)
│ ├── x.yaml # Built-in connector
│ ├── hackernews.yaml # Built-in connector
│ ├── changelog-watch.yaml # Built-in connector (website)
│ ├── funnel-form.yaml # Custom connector manifest
│ └── funnel-form.connector.ts # Custom connector implementation
├── models/
│ ├── schema.yaml # Entities, relationships, watchers
│ └── reactions/
│ ├── inbound-triage.reaction.ts # Runs after watcher extraction
│ └── funnel-digest.reaction.ts # Runs after watcher extraction
└── agents/crm/
├── SOUL.md # Agent personality
├── IDENTITY.md # Agent identity
├── USER.md # User context
└── skills/crm-ops/SKILL.md # Agent skill
```
Comment on lines +8 to +28
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add language identifier to fenced code block.

The directory structure code block is missing a language specifier. Add text or plaintext after the opening backticks for proper rendering.

📝 Proposed fix
-```
+```text
 lobu-crm/
 ├── lobu.toml                              # Agent + memory config

Based on learnings: Static analysis flagged this as a markdown best practice violation.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```
lobu-crm/
├── lobu.toml # Agent + memory config
├── connectors/
│ ├── github.yaml # Built-in connector (just config)
│ ├── x.yaml # Built-in connector
│ ├── hackernews.yaml # Built-in connector
│ ├── changelog-watch.yaml # Built-in connector (website)
│ ├── funnel-form.yaml # Custom connector manifest
│ └── funnel-form.connector.ts # Custom connector implementation
├── models/
│ ├── schema.yaml # Entities, relationships, watchers
│ └── reactions/
│ ├── inbound-triage.reaction.ts # Runs after watcher extraction
│ └── funnel-digest.reaction.ts # Runs after watcher extraction
└── agents/crm/
├── SOUL.md # Agent personality
├── IDENTITY.md # Agent identity
├── USER.md # User context
└── skills/crm-ops/SKILL.md # Agent skill
```
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 8-8: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/lobu-crm/README.md` around lines 8 - 28, The fenced code block in
the README.md directory listing is missing a language specifier; update the
opening triple backticks for the block that begins with "lobu-crm/" to include a
language identifier such as text or plaintext (e.g., change ``` to ```text) so
the directory tree renders correctly; locate the block in
examples/lobu-crm/README.md and modify the opening fence accordingly.


## Key files to read

| File | What it shows |
|------|--------------|
| `lobu.toml` | Agent config, providers, network allowlist |
| `models/schema.yaml` | Entity definitions + watcher cron + extraction schema |
| `connectors/funnel-form.connector.ts` | Custom connector with typed checkpoint + config |
| `models/reactions/inbound-triage.reaction.ts` | Reaction script with typed `ReactionClient` |
39 changes: 31 additions & 8 deletions examples/lobu-crm/connectors/funnel-form.connector.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
// biome-ignore-all format: stays compact for the landing-page code panel
import { ConnectorRuntime, type SyncContext } from "@lobu/connector-sdk";
import {
ConnectorRuntime,
type SyncContext,
type SyncResult,
} from "@lobu/connector-sdk";

interface FunnelCheckpoint {
seen_ids: string[];
}

interface FunnelConfig {
endpoint: string;
}

/**
* Funnel-form connector — polls a small JSON API (`config.endpoint` returns
* `{ submissions: [...] }`) and emits one event per new submission. Dedup
* via a rolling checkpoint of seen IDs. Auto-discovered by `lobu apply`.
*
* Demonstrates: ConnectorRuntime<C, F> with typed checkpoint + config.
*/
export default class FunnelFormConnector extends ConnectorRuntime {
export default class FunnelFormConnector extends ConnectorRuntime<
FunnelCheckpoint,
FunnelConfig
> {
readonly definition = {
key: "funnel-form",
name: "Funnel form",
Expand All @@ -15,21 +31,28 @@ export default class FunnelFormConnector extends ConnectorRuntime {
feeds: { submissions: { key: "submissions", name: "Form submissions" } },
};

async sync(ctx: SyncContext) {
const seen = new Set<string>((ctx.checkpoint as any)?.seen_ids ?? []);
const subs: any[] = (await (await fetch(String(ctx.config.endpoint))).json()).submissions ?? [];
async sync(
ctx: SyncContext<FunnelCheckpoint, FunnelConfig>
): Promise<SyncResult<FunnelCheckpoint>> {
const seen = new Set<string>(ctx.checkpoint?.seen_ids ?? []);
const subs: any[] =
(await (await fetch(ctx.config.endpoint)).json()).submissions ?? [];
const fresh = subs.filter((s) => s?.id && !seen.has(s.id));
return {
events: fresh.map((s) => ({
origin_id: s.id,
origin_type: "form_submission",
title: s.company ? `Demo request — ${s.company}` : `Demo request — ${s.name ?? s.email ?? s.id}`,
title: s.company
? `Demo request — ${s.company}`
: `Demo request — ${s.name ?? s.email ?? s.id}`,
payload_text: s.message ?? "",
author_name: s.name,
occurred_at: s.submitted_at ? new Date(s.submitted_at) : new Date(),
metadata: { company: s.company, email: s.email },
})),
checkpoint: { seen_ids: [...seen, ...fresh.map((s) => s.id)].slice(-1000) },
checkpoint: {
seen_ids: [...seen, ...fresh.map((s) => s.id)].slice(-1000),
},
};
}

Expand Down
14 changes: 7 additions & 7 deletions examples/lobu-crm/models/reactions/inbound-triage.reaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@
* Reaction for the `inbound-triage` watcher.
*
* Fires every 2h after the watcher LLM extracts new and enriched leads from
* GitHub/X/HN signals. The script writes a `lead_interaction` event per
* recommended action so the next digest can count them — the watcher itself
* already creates the `lead` rows, so we don't duplicate that here.
* GitHub/X/HN signals. Persists a `lead_interaction` event per run so the
* next digest can count them.
*/
import type { ReactionContext } from "@lobu/connector-sdk";
import type { ReactionClient, ReactionContext } from "@lobu/connector-sdk";

interface TriageData {
new_leads?: Array<{
Expand All @@ -19,10 +18,11 @@ interface TriageData {
notable?: boolean;
}

export default async (ctx: ReactionContext, client: any): Promise<void> => {
export default async (
ctx: ReactionContext,
client: ReactionClient
): Promise<void> => {
const data = ctx.extracted_data as TriageData;
// Nothing notable → nothing to persist. The watcher's prompt is explicit
// about not manufacturing noise; we mirror that here.
if (!data.notable) return;

const actions = data.recommended_actions ?? [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@ network:
- playwright.azureedge.net
- cdn.playwright.dev
judge:
- deliveroo.co.uk
- .deliveroo.co.uk
- deliveroo.com
- .deliveroo.com
- domain: deliveroo.co.uk
judge: deliveroo
- domain: .deliveroo.co.uk
judge: deliveroo
- domain: deliveroo.com
judge: deliveroo
- domain: .deliveroo.com
judge: deliveroo
judges:
default: >
deliveroo: >
Allow GET requests that read restaurant listings, menus, item details, and
the current basket. Allow POST/PUT requests whose effect is limited to
building or modifying a basket / group order (add, remove, change quantity;
Expand Down
9 changes: 6 additions & 3 deletions examples/office-bot/lobu.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,16 @@ allowed = [
]
# Deliveroo goes through the egress judge — reading menus and assembling a
# basket is fine; touching payment, addresses, or the account profile is not.
# Each domain names the "deliveroo" policy below.
judge = [
"deliveroo.co.uk", ".deliveroo.co.uk",
"deliveroo.com", ".deliveroo.com",
{ domain = "deliveroo.co.uk", judge = "deliveroo" },
{ domain = ".deliveroo.co.uk", judge = "deliveroo" },
{ domain = "deliveroo.com", judge = "deliveroo" },
{ domain = ".deliveroo.com", judge = "deliveroo" },
]

[agents.food-ordering.network.judges]
default = """
deliveroo = """
Allow GET requests that read restaurant listings, menus, item details, and the
current basket. Allow POST/PUT requests whose effect is limited to building or
modifying a basket / group order (adding, removing, changing quantity of items;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* persist a `health_change` event so the renewal-risk view + weekly digest
* have a stable record without re-extracting from the CRM stream.
*/
import type { ReactionContext } from "@lobu/connector-sdk";
import type { ReactionClient, ReactionContext } from "@lobu/connector-sdk";

interface HealthData {
account_changes?: Array<{
Expand All @@ -18,11 +18,12 @@ interface HealthData {

const RISK_ORDER = { low: 0, medium: 1, high: 2 } as const;

export default async (ctx: ReactionContext, client: any): Promise<void> => {
export default async (
ctx: ReactionContext,
client: ReactionClient
): Promise<void> => {
const data = ctx.extracted_data as HealthData;
const changes = data.account_changes ?? [];
// Only persist *worsening* transitions — improvements are visible in the
// CRM stream and don't need a durable flag.
const escalations = changes.filter(
(c) => RISK_ORDER[c.current_risk] > RISK_ORDER[c.previous_risk]
);
Expand Down
Loading
Loading