Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
bf82129
feat(telos): add @decocms/telos goal-pursuit agent core
pedrofrxncx Jun 13, 2026
5583246
feat(telos): anchored self-directed goals (GoalProposer + gating)
pedrofrxncx Jun 14, 2026
ccc0981
feat(telos): metaphysics subpaths — daimonion (veto), elenchus, demiurge
pedrofrxncx Jun 14, 2026
6303d4b
docs(telos): README — three-metaphysics architecture + Deco Store For…
pedrofrxncx Jun 14, 2026
8d44874
refactor(telos): semantic folders + DRY action application
pedrofrxncx Jun 14, 2026
77724fd
feat(telos): integrate telos goal ledger and onboarding process
pedrofrxncx Jun 14, 2026
fac8290
feat(telos): implement onboarding research and fact management
pedrofrxncx Jun 14, 2026
4370119
refactor(telos): streamline comments and documentation for clarity
pedrofrxncx Jun 14, 2026
892cfdc
feat(telos): integrate Kysely for goal ledger management and enhance …
pedrofrxncx Jun 14, 2026
d91d1cd
refactor(telos): remove deprecated telos goal and fact migrations, st…
pedrofrxncx Jun 14, 2026
95d2860
feat(telos): refactor telos integration and enhance onboarding capabi…
pedrofrxncx Jun 14, 2026
ab863a7
feat(telos): enhance onboarding and goal pursuit capabilities
pedrofrxncx Jun 14, 2026
40d4f93
feat(telos): refactor research subject handling and enhance goal purs…
pedrofrxncx Jun 14, 2026
290b1a8
feat(telos): enhance goal management and onboarding experience
pedrofrxncx Jun 14, 2026
ec9dbb8
feat(telos): introduce catalog management for app integration
pedrofrxncx Jun 14, 2026
1a8f391
feat(telos): enhance thought management and real-time feedback during…
pedrofrxncx Jun 14, 2026
a042d64
feat(telos): integrate telos purpose management for agents
pedrofrxncx Jun 14, 2026
8ad8b48
refactor(telos): update agent type imports and enhance documentation
pedrofrxncx Jun 14, 2026
5300808
feat(telos): implement fixed goal structure and enhance onboarding pr…
pedrofrxncx Jun 14, 2026
b9be9c1
refactor(telos): streamline goal pursuit and onboarding processes
pedrofrxncx Jun 14, 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
1 change: 1 addition & 0 deletions apps/mesh/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"@decocms/runtime": "workspace:*",
"@decocms/sandbox": "workspace:*",
"@decocms/std": "workspace:*",
"@decocms/telos": "workspace:*",
"@floating-ui/react": "^0.27.16",
"@happy-dom/global-registrator": "^20.9.0",
"@hookform/resolvers": "^5.2.2",
Expand Down
11 changes: 11 additions & 0 deletions apps/mesh/src/api/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
registerPublicSetsSyncWorkflow,
setPublicSetsSyncRuntime,
} from "../file-storage/dbos-public-sets-sync";
import { bootTelos, initTelosDbos } from "@/telos";
import { getPublicUrl } from "@/core/server-constants";
import { usesLocalObjectStorage } from "../tools/connection/dev-assets";
import { DECO_STORE_URL, isDecoHostedMcp } from "@/core/deco-constants";
Expand Down Expand Up @@ -1524,6 +1525,11 @@ export async function createApp(options: CreateAppOptions = {}) {
// workflow no-ops when ORGFS_PUBLIC_SETS is unset.
setPublicSetsSyncRuntime({ db: database.db, baseUrl: getPublicUrl() });

// Telos owns its `telos` schema and migrates it itself (DBOS-style), so mesh
// carries no telos tables or migration files. One call: migrate, build stores,
// wire deps, and register durable capabilities (before DBOS.launch()).
await bootTelos(database.db);

// ============================================================================
// Automation Runtime — wire storage + streaming into the DBOS workflow
// ============================================================================
Expand Down Expand Up @@ -2351,6 +2357,11 @@ export async function createApp(options: CreateAppOptions = {}) {
partitionQueue: true,
concurrency: THREAD_GATE_PARTITION_CONCURRENCY,
});

// Telos's shared work queue (onboarding research + goal pursuit). Registered
// here for the same reason as the gate queue: registerQueue needs launch.
await initTelosDbos();

await reconcileAutomationSchedules(automationsStorage);

// One-time cleanup of the retired per-automation/global gate queues.
Expand Down
2 changes: 2 additions & 0 deletions apps/mesh/src/api/routes/org-scoped.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { createSsoRoutes } from "./org-sso";
import { createProxyRoutes } from "./proxy";
import { createSelfRoutes } from "./self";
import { createHomeNextActionsRoutes } from "./home-next-actions";
import { createTelosGoalRoutes } from "./telos-goal";
import { createObjectStorageRoutes } from "./object-storage";
import { createThreadOutputsRoutes } from "./thread-outputs";
import { createTriggerCallbackRoutes } from "./trigger-callback";
Expand Down Expand Up @@ -98,6 +99,7 @@ export const createOrgScopedApi = (deps: OrgScopedDeps) => {
); // /api/:org/fs/:volume/...
app.route("/sandbox", createSandboxRoutes()); // /api/:org/sandbox/:virtualMcpId/:branch/*
app.route("/", createHomeNextActionsRoutes());
app.route("/", createTelosGoalRoutes()); // /api/:org/telos-goal
app.route("/deco-sites", createDecoSitesOrgRoutes()); // /api/:org/deco-sites
app.route("/sso", createSsoRoutes()); // /api/:org/sso/* (renamed from /api/org-sso)
app.route(
Expand Down
107 changes: 107 additions & 0 deletions apps/mesh/src/api/routes/telos-goal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// GET /api/:org/telos-goal → { goal, facts, status }
// POST /api/:org/telos-facts/:id → confirm/reject a fact
//
// No goal yet → publish a user.signup trigger (durable, OAOO), which also
// backfills orgs predating the signup hook; the client updates live via SSE.

import type { StudioContext } from "@/core/studio-context";
import { telos } from "@/telos";
import { connectTools, onboardingProgress } from "@/telos/domain";
import { telosBus } from "@/telos/durable/bus";
import { getLatestSuggestion, pullPursuit } from "@/telos/durable/pursuit";
import { getLatestThought } from "@/telos/durable/thought";
import { requireTelosRuntime } from "@/telos/durable/runtime";
import type { FactStatus } from "@decocms/telos/postgres";
import { researchSubject } from "@/telos/research";
import { Hono } from "hono";

type Variables = { meshContext: StudioContext };

export function createTelosGoalRoutes() {
const app = new Hono<{ Variables: Variables }>();

app.get("/telos-goal", async (c) => {
const mesh = c.get("meshContext");
const orgId = mesh.organization?.id;
if (!orgId) return c.json({ error: "Organization required" }, 400);

const { ledger, facts: factStore } = telos();

// The current working goal (latest of any source) — so an engine-authored
// progression goal shows, not just the original authority anchor.
const [current, facts] = await Promise.all([
Promise.resolve(ledger.latest(orgId)).catch(() => null),
factStore.list(orgId),
]);

// Project the Goal to the wire shape the card consumes: title + the connect-app
// steps' tools (the connect checklist). The full step model stays server-side.
const goal = current
? {
title: current.target.title,
tools: connectTools(current.target),
version: current.version,
source: current.source,
}
: null;
const suggestion = goal ? getLatestSuggestion(orgId) : null;
// Per-tool connected/not, so the card shows real progress (GitHub ✓, CMS ◯).
const progress = current
? await onboardingProgress(
requireTelosRuntime().db,
orgId,
current.target,
)
: null;

// No goal yet → kick onboarding (installs the fixed Goal + gathers facts),
// OAOO-deduped on the org.
if (!goal) {
const subject = researchSubject(
mesh.auth.user?.email,
mesh.auth.user?.name,
);
await telosBus.publish({
type: "user.signup",
organizationId: orgId,
userId: mesh.auth.user?.id ?? "",
email: subject.email,
name: subject.name,
});
}

return c.json({
goal,
facts,
suggestion,
thought: getLatestThought(orgId),
progress,
status: goal ? "ready" : "researching",
});
});

app.post("/telos-facts/:id", async (c) => {
const mesh = c.get("meshContext");
const orgId = mesh.organization?.id;
if (!orgId) return c.json({ error: "Organization required" }, 400);

const { status } = (await c.req.json().catch(() => ({}))) as {
status?: FactStatus;
};
if (status !== "confirmed" && status !== "rejected") {
return c.json({ error: "status must be confirmed or rejected" }, 400);
}

await telos().facts.setStatus(orgId, c.req.param("id"), status);
await telosBus.publish({ type: "facts.updated", organizationId: orgId });

// The user telling us who they are is signal. The agent observes confirmed
// facts, so pull a pursuit cycle: it re-thinks the next step with the new fact
// in view (a fast model produces the reasoning + picks the action). The Goal
// itself is fixed and never changes. Debounced; never blocks the edit.
void pullPursuit(orgId);
return c.json({ ok: true });
});

return app;
}
18 changes: 18 additions & 0 deletions apps/mesh/src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ import { createEmailSender, findEmailProvider } from "./email-providers";
import { emailButton, emailParagraph, emailTemplate } from "./email-template";
import { createMagicLinkConfig } from "./magic-link";
import { seedOrgDb } from "./org";
import { telosBus } from "@/telos/durable/bus";
import { researchSubject } from "@/telos/research";
import { hoistOrgLogo } from "./hoist-org-logo";
import { identifyAuthenticatedUser } from "./posthog-identify";
import { ADMIN_ROLES } from "./roles";
Expand Down Expand Up @@ -238,6 +240,22 @@ const plugins = [
organizationCreation: {
afterCreate: async (data) => {
await seedOrgDb(data.organization.id, data.member.userId);
// Telos onboarding: publish a durable trigger; the onboarding-research
// capability (Firecrawl + Perplexity → facts + first goal) runs in a DBOS
// workflow, so signup never waits on LLM latency. Best-effort — the
// home's GET also backfills, and OAOO collapses the two into one run.
try {
const subject = researchSubject(data.user.email, data.user.name);
await telosBus.publish({
type: "user.signup",
organizationId: data.organization.id,
userId: data.member.userId,
email: subject.email,
name: subject.name,
});
} catch (err) {
console.error("[telos] signup trigger failed", err);
}
},
},
organizationHooks: {
Expand Down
14 changes: 14 additions & 0 deletions apps/mesh/src/automations/automation-event-dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* the event bus hot path.
*/

import { orgActivity } from "@/core/activity";
import type { AutomationsStorage } from "@/storage/automations";
import type { Automation, AutomationTrigger } from "@/storage/types";
import type { ContextMessage } from "./dbos-workflow";
Expand Down Expand Up @@ -152,15 +153,28 @@ export class AutomationEventDispatcher {
),
);

let fired = 0;
for (const [i, result] of results.entries()) {
const trigger = triggersToFire[i]!;
if (result.status === "rejected") {
console.error(
`[AutomationDispatch] Trigger ${trigger.id} ("${trigger.automation.name}") REJECTED:`,
result.reason,
);
} else {
fired++;
}
}

// A fired automation moves the org's "automations run" metric — signal it so
// telos (and any other consumer) can react. Best-effort, never blocks dispatch.
if (fired > 0) {
orgActivity.emit({
organizationId: event.organizationId,
source: "automation",
name: event.type,
});
}
}

/**
Expand Down
39 changes: 39 additions & 0 deletions apps/mesh/src/core/activity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Org-activity signal: a decoupled, in-process pub/sub for "something meaningful
// changed in this org". Producers (a state-mutating tool call, an automation
// firing) emit; consumers (telos pursuit) subscribe. It is deliberately:
// - generic: producers carry no knowledge of who listens, consumers carry no
// knowledge of what produced — new producers/consumers compose without edits;
// - best-effort and lossy: a dropped signal only delays a reaction, never loses
// data — the consumer re-observes durable state as its source of truth, and a
// safety-net heartbeat covers anything missed.
// This is NOT the durable event bus (CloudEvents) — it's a lightweight nudge.

export interface OrgActivity {
organizationId: string;
/** What produced the signal — for logging/telemetry, never control flow. */
source: "tool" | "automation" | string;
/** The tool/automation/event name, when known. */
name?: string;
}

type Listener = (activity: OrgActivity) => void;

const listeners = new Set<Listener>();

export const orgActivity = {
// Fire-and-forget: a throwing listener can never break the producer's path.
emit(activity: OrgActivity): void {
if (!activity.organizationId) return;
for (const listener of listeners) {
try {
listener(activity);
} catch (err) {
console.warn("[activity] listener failed", err);
}
}
},
subscribe(listener: Listener): () => void {
listeners.add(listener);
return () => listeners.delete(listener);
},
};
8 changes: 8 additions & 0 deletions apps/mesh/src/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ export const CIRCUIT_BREAKER_COOLDOWN_MS = 30_000; // 30 seconds
/** Maximum number of circuit breaker entries to retain in memory */
export const CIRCUIT_BREAKER_MAX_ENTRIES = 1000;

/**
* Consecutive credential-decryption failures (per replica) before a connection
* is durably disabled (status="error"). A decrypt failure is deterministic — the
* same key never recovers — so a small buffer is enough; it only guards against a
* transient key-load race at boot. Mirrors CIRCUIT_BREAKER_FAILURE_THRESHOLD.
*/
export const CONNECTION_DECRYPT_DISABLE_THRESHOLD = 3;

/*
* Durable connection auto-disable (cross-replica, via the shared circuit store).
*
Expand Down
16 changes: 16 additions & 0 deletions apps/mesh/src/core/define-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import { SpanStatusCode } from "@opentelemetry/api";
import { z } from "zod";
import { orgActivity } from "./activity";
import type { StudioContext } from "./studio-context";

// ============================================================================
Expand Down Expand Up @@ -170,6 +171,21 @@ export function defineTool<
// Mark span as successful
span.setStatus({ code: SpanStatusCode.OK });

// A state-mutating tool that succeeded is org activity worth
// reacting to (e.g. telos re-observes its goal). readOnly tools
// are skipped; consumers debounce + re-observe so a stray emit is
// cheap. Best-effort — never let a listener affect the tool path.
if (
definition.annotations?.readOnlyHint !== true &&
ctx.organization?.id
) {
orgActivity.emit({
organizationId: ctx.organization.id,
source: "tool",
name: definition.name,
});
}

return output;
} catch (error) {
// Mark span as error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ export interface BuildAgentSystemPromptOptions {
*/
isDecopilot?: boolean;
agentInstructions?: string;
/** The agent's telos (purpose) rendered as a charter block — what this agent
* exists to serve. Resolved from the agent's installed goal; absent when the
* agent has no purpose installed. */
telosCharter?: string;
date?: Date;
/** Current thread id, excluded from the "history together" recall so the
* agent doesn't "remember" the conversation it's currently in. */
Expand Down Expand Up @@ -179,6 +183,10 @@ export async function buildAgentSystemPrompt(

add("agentInstructions", opts.agentInstructions);

// The agent's fixed purpose. Sits with its instructions — instructions say HOW
// to behave; the telos says WHAT the agent is ultimately for.
add("telos", opts.telosCharter);

if (opts.kind === "agent" && opts.user) {
add(
"userContext",
Expand Down
11 changes: 11 additions & 0 deletions apps/mesh/src/harnesses/decopilot/harness-deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import type {
RunEngineArgs,
} from "@decocms/harness/decopilot/engine";
import { runAgentLoop } from "./run-agent-loop";
import { resolveAgentTelos } from "./telos";
import type { DecopilotTelemetry } from "@decocms/harness/decopilot/run-stream";

/**
Expand All @@ -58,11 +59,21 @@ async function runClusterEngine(
organization: OrganizationScope,
args: RunEngineArgs,
): Promise<AssembledEngineHandle> {
// Carry the agent's telos (purpose) into this run: its charter joins the
// system prompt, its guard screens tool calls. Only top-level agents carry one
// — subagents inherit their parent's task scope. Best-effort: a resolution
// failure (no purpose, telos not booted) just means the agent runs untethered.
const telos =
args.kind === "agent"
? await resolveAgentTelos(args.virtualMcp.id).catch(() => null)
: null;
const handle = await runAgentLoop({
kind: args.kind,
ctx,
organization,
virtualMcp: args.virtualMcp,
telosCharter: telos?.charter,
telosGuard: telos?.guard,
mcpClient: args.mcpClient,
provider: args.provider,
models: args.models,
Expand Down
Loading
Loading