Agent Works
SkillFiles & storageLets your agent register and dedupe its outputs like files, documents, and tasks as work items.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Agent Works skill
About this capability
Agent Work registry (works / work_versions): how agent outputs — github/linear entities, files, documents, tasks — get registered, deduped, and rendered as cards, and how to extend it with a new skill provider, a new shell CLI scanner, or a new Work type.
What this skill tells your AI
The instructions your AI receives, as published by lobehub/lobehub in .agents/skills/agent-work/SKILL.md and read by ahel’s review.
A Work is a durable record of something an agent produced or touched — a GitHub PR/issue, a Linear issue, an entity file (pptx/xlsx/docx/pdf), a document, a task. Works render as cards under the assistant message that produced them and accumulate versions across operations, so the same PR edited twice shows one card with history.
Two tables (packages/database/src/schemas/work.ts):
works— one row per resource. Identity/dedup key:(resourceType, resourceId)within the user/workspace scope.currentVersionIdsoft-references the latest version.work_versions— one row per registration event. Dedup: unique(workId, toolCallId), so a retried registration with the same real tool call id is a no-op. Versions carry provenance (source message, producing tool) and operation-levelcumulativeCost/cumulativeUsage.
Type registry
packages/database/src/models/work/registry.ts is the single registry of Work types (document / external / file / task): WORK_TYPE_ADAPTERS is satisfies Record<WorkType, WorkTypeAdapter>, so a type added to @lobechat/types without an adapter is a compile error.
Read-path compatibility gate: OPT_IN_WORK_TYPES (currently {'file'}) hides newer types from clients that did not opt in (includeFileWorks). Released Electron clients lag by weeks and crash on unknown type descriptors (descriptor.getIcon on undefined), so a request without the opt-in receives exactly the pre-file set. Any NEW Work type must ship behind the same kind of opt-in.
Registration write paths
Four write paths, two timing classes:
| Path | When | Where |
|---|---|---|
Skill structured tools + sandbox runCommand (github / linear) | at tool execution | WorkModel.handleSkillToolResult, called from server toolExecution and the client executor (registerClientWorkFromIntent.ts on the legacy non-gateway runtime) |
Shell Work scan (hetero codex/claude-code + device lobe-local-system) | at operation completion | apps/server/src/services/workRegistration/shellWorkRegistration.ts via registerWorksForOperation |
| File Work scan (sandbox entity files) | at operation completion | apps/server/src/services/workRegistration/registerWorksForOperation.ts |
| Task / document works | at creation by their owning feature | WorkModel.registerTask / registerDocument |
Execution-time: skill providers
SKILL_TOOL_RESULT_NORMALIZERS in packages/database/src/models/work/index.ts maps WorkSkillProvider (github, linear; vocabulary in packages/types/src/work.ts) to a normalizer. Both satisfies a Record<WorkSkillProvider, …>, so provider list and normalizer map cannot drift.
A normalizer turns one tool result into ExternalToolWorkOperation | null — null means "not Work-worthy", which is the common case and never an error.
Completion-time: the shell Work scan framework
Heterogeneous CLI agents (codex, claude-code) and the device lobe-local-system tool run CLIs like gh through their own shell surfaces, which never pass the skill-tool hook. registerWorksForOperation recovers their Works at completion from the persisted command text + stdout. Layering:
registerWorksForOperation (registerWorksForOperation.ts)
├─ collectOperationRecords one pass over the operation tree, shared by both scans
├─ registerShellWorks (shellWorkRegistration.ts — the ENGINE, command-agnostic)
│ ├─ SHELL_COMMAND_SOURCES identifier→apiName scoping of shell surfaces
│ ├─ success gate plugin error / state.success===false / state.error → skip
│ ├─ extract {command, exitCode, output} from arguments / state / message content
│ └─ shellWorkScanners/ one file per CLI family
│ ├─ types.ts ShellWorkScanner = { matches, name, register }
│ ├─ github.ts matches: includes('gh ') → workModel.registerShellGithubResult
│ └─ index.ts SHELL_WORK_SCANNERS registry
└─ file Work scan aggregate per-path fold + sandbox export pipeline (NOT a scanner)
Command parsing is split the same way: packages/database/src/models/work/shellCommandParsing.ts holds the command-agnostic layer (POSIX-ish tokenizer, control-operator segmenting, codex /bin/zsh -lc wrapper expansion — parseShellCommandSegments), while githubToolResult.ts holds only the gh-specific parseGhSegment + field mapping.
Cost granularity: execution-time vs completion-time
cumulativeCost / cumulativeUsage are per-version SNAPSHOTS ("spend up to this version"), never deltas — summing them across versions double-counts on every path.
- Execution-time registrations (skill providers) snapshot right after
UsageCounter.accumulateToolfor that call, so create + edit + edit within one run carries an INCREASING series (e.g. $0.30 → $0.70 → $1.20) and step deltas are recoverable. - Completion-time registrations (shell scan, file scan) attach one OPERATION-LEVEL figure — the completing run's terminal total plus terminal child-op totals — to EVERY version registered that round: the scan only has persisted command text + stdout, no per-call intermediate snapshots (hetero CLI usage is aggregated per step by the trace recorder, not per tool call). The same create + edit + edit therefore yields three versions all carrying $1.20, indistinguishable per step.
- Cross-operation is independent: a later run editing the same resource appends a version with THAT run's total only.
Why file Works are not a scanner: their unit is a file path folded across ALL records (multi-edit merge, renames, last-edit provenance), the registration is a heavy IO pipeline (idempotency probe → sandbox export → upload → version → redeploy, bounded concurrency), hetero records are deliberately EXCLUDED (the file lives on the executing device, not the exportable sandbox), and the version key is the synthetic op:${operationId} instead of a real toolCallId. Shoehorning that into ShellWorkScanner would break the interface. If a second aggregate-style scan ever appears, extract an aggregate layer then.
Display anchor & completion marker
A Works card renders only under an assistant message stamped with metadata.work.rootOperationId (the "anchor"). Who stamps it:
- In-process runs:
callLlmFinalizerstamps the round's final assistant message. - Hetero runs never pass that executor, so
registerWorksForOperationstamps the anchor itself when the shell scan registered anything — using the completion'sassistantMessageId, falling back to theparentIdof the last registered tool message (hetero single-step runs persist no final-assistant pointer at all; seeheteroFinish).
The whole scan returns {attempted, failed} and the completion backstop (CompletionLifecycle) writes its idempotency marker only when failed === 0. Anything that must be retried — a thrown registration, a {success: false} anchor stamp, an unresolvable anchor — counts into failed. Registration is retry-safe end to end (probe short-circuits, (workId, toolCallId) guard backs it up), so counting a failure is always the right move.
Key decisions (from the Work PRs)
- Only successful create/edit results become Works (LOBE-10967). Read-only queries, comments, merges/closes, and branch/repo operations are excluded —
gh pr viewprinting an entity URL must NOT register. owner/repo#numberis the canonical github identity, not node_id: the gh CLI surface never returns node_id, and the same entity touched via REST tools and CLI must land on one Work row.- stdout is the source of truth for identity on the CLI path (
gh … create/editprints the entity URL); the command's edit target is the fallback. The LAST gh create/edit segment of a chained command owns the trailing URL. - Persisted URLs are http(s)-allowlisted (
sanitizeExternalUrl): gh stdout / tool results are member-controlled and the URL reachesshell.openExternalon desktop. - The tokenizer is deliberately hand-rolled (no
shell-quotedep): a real parser would expand what must stay literal, and the worst failure mode is skipping a bookkeeping registration. - Error-terminated runs skip the completion scan (same success gate as file Works): a
ghcreate that succeeds before a later terminal failure registers no Work that round. Accepted — bounded cost, missing card only. - Hetero Works register after the terminal snapshot (
agent_runtime_endpublishes beforecompleteOperation), so a hetero-created card appears on the next refetch, not in the terminal snapshot. Accepted for now. - PRs pushed via codex's own GitHub integration are not covered — no shell record exists; text-mention parsing was rejected as too false-positive-prone.
Extension recipes
New skill provider (structured tools), e.g. Notion:
packages/types/src/work.ts— extendWORK_SKILL_PROVIDERS+WORK_PROVIDER_RESOURCE_TYPES(+ resource-type unions).packages/database/src/models/work/<provider>ToolResult.ts— write the normalizer; mirrorlinearToolResult.ts.packages/database/src/models/work/index.ts— add it toSKILL_TOOL_RESULT_NORMALIZERS(thesatisfiesforces this).- Pick a stable cross-surface
resourceId(the github lesson: choose the identity every surface can produce).
New shell CLI scanner, e.g. linear CLI or vercel deploy:
- Add a normalizer in
packages/database/src/models/work/reusingparseShellCommandSegments, plus aWorkModel.registerShell<X>Resultfacade method. - Add
apps/server/src/services/workRegistration/shellWorkScanners/<name>.tswith{ matches, name, register }—matchesstays a cheap substring check. - Append it to
SHELL_WORK_SCANNERSinshellWorkScanners/index.ts. The engine needs zero changes. - Pin the parsing edge cases in
packages/database/src/models/work/__tests__/(wrapper unwrap, chained segments, failed/read-only commands must not register).
New Work type (new card kind):
@lobechat/types— extendWorkTypeand related unions.packages/database/src/models/work/— new adapter module +WORK_TYPE_ADAPTERSentry (compile-enforced).- Client — add the type descriptor for the card UI.
- Add the type to the read-path opt-in gate (
OPT_IN_WORK_TYPESpattern) — released clients throw on unknown types; never ship a new type into the legacy result set.
File map
| Concern | Path |
|---|---|
| Schema | packages/database/src/schemas/work.ts |
| Type registry + read gate | packages/database/src/models/work/registry.ts |
| WorkModel facade + provider normalizer map | packages/database/src/models/work/index.ts |
| gh CLI + structured github normalizer | packages/database/src/models/work/githubToolResult.ts |
| Command-agnostic shell parsing | packages/database/src/models/work/shellCommandParsing.ts |
| Completion scan orchestrator (file + shell + anchor) | apps/server/src/services/workRegistration/registerWorksForOperation.ts |
| Shell scan engine | apps/server/src/services/workRegistration/shellWorkRegistration.ts |
| Desktop-local run scan (client-reported) | apps/server/src/services/workRegistration/localRunWorkRegistration.ts |
| Per-CLI scanners | apps/server/src/services/workRegistration/shellWorkScanners/ |
| Client (non-gateway) registration | src/store/chat/agents/registerClientWorkFromIntent.ts |
Signals
- GitHub stars
- 82k
- Forks
- 16k
- Last commit
- Sep 2026
ahel recommends instead
Advanced
- Catalog kind
- skill
- Gateway key
agent-work-lobehub- Source
- github.com/lobehub/lobehub