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
12 changes: 12 additions & 0 deletions .tegami/2026-08-05-agentic-skill-beta.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
packages:
orgmemory: minor
subject: Use governed Skills in Assistant answers
---

## Features

The Assistant can now discover an authorized Skill, load its exact released
instructions, and read bounded supporting text while preparing a grounded
answer. Skill content never grants tools or permissions, and OrgMemory does not
execute package scripts, binaries, or shell commands.
16 changes: 14 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,10 +217,12 @@ object key.

The closed `core.assetregistry.skill` nested module owns bounded package
inspection and validation, GitHub acquisition orchestration, API-facing Skill
operations, and install-manifest construction. Its exact public top-level
operations, install-manifest construction, and read-only runtime projection.
Its exact public top-level
surface is `SkillPackageOperations`, `SkillGitHubOperations`,
`SkillDistributionOperations`, `SkillGitHubSourcePort`,
`SkillPackageInspection`, `SkillInstallManifest`, and `SkillPackageContent`;
`SkillPackageInspection`, `SkillInstallManifest`, `SkillPackageContent`, and
`SkillRuntimeOperations`;
all implementations and package semantics remain package-private. The child
consumes the parent only through `assetregistry::skill-package` and
`assetregistry::skill-delivery`, while the parent never depends on the child.
Expand Down Expand Up @@ -415,6 +417,16 @@ and retains the ordinary bounded conversation-memory advisor. Deployment
defaults remain synthetic and read-only, and other AI workloads cannot enter
this Assistant-only exact-route authority path.

On that exact Assistant route, the gateway registers three fixed request-local
Skill tools for actor-scoped search, exact-release activation, and bounded text
resource reads. A bounded Spring AI streaming tool advisor performs progressive
disclosure without a second registry or filesystem mirror. Each operation
re-enters live Asset authorization and immutable package integrity checks;
stored object keys and denied identities never enter model context. Skill
content is untrusted, `allowed-tools` grants no runtime authority, and the API
does not execute scripts, binaries, shell commands, or package code. Empty
authorized retrieval still terminates before model or Skill-tool invocation.

The pure-Java GraphRAG core defines canonical entity/relation identity,
evidence-level contributions and provenance, structured extraction contracts,
authorization-scoped graph read ports, atomic revision replacement, and one
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.orgmemory.core.ai.ChatModelPort;
import com.orgmemory.core.assistant.AssistantAssetToolService;
import com.orgmemory.core.assistant.AssistantAgentModelPort;
import com.orgmemory.core.assistant.AssistantAssetTraceRecorder;
import com.orgmemory.core.assistant.AssistantService;
import com.orgmemory.core.assistant.observability.AssistantStageEventSink;
Expand Down Expand Up @@ -80,12 +81,14 @@ PermissionAwareKnowledgeSearch permissionAwareKnowledgeSearch(
AssistantService assistantService(
PermissionAwareKnowledgeSearch retrieval,
ChatModelPort chat,
AssistantAgentModelPort agent,
ObservationRegistry observations,
AssistantProperties properties,
AssistantStageEventSink stages) {
return new AssistantService(
retrieval,
chat,
agent,
observations,
observedEngine(properties),
stages);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.orgmemory.api.security.CurrentActorProvider;
import com.orgmemory.core.assistant.AssistantAnswerFeedbackView;
import com.orgmemory.core.assistant.AssistantAnswerSentiment;
import com.orgmemory.core.assistant.AssistantAgentActivity;
import com.orgmemory.core.assistant.AssistantCitation;
import com.orgmemory.core.assistant.AssistantConversationMessageView;
import com.orgmemory.core.assistant.AssistantConversationService;
Expand Down Expand Up @@ -400,6 +401,9 @@ Flux<AssistantStreamPart> parts(AssistantTurn turn) {
AssistantStreamPart.Activity.Phase.GENERATION,
AssistantStreamPart.Activity.State.ACTIVE,
null));
Flux<AssistantStreamPart> live = Flux.merge(
turn.activities().map(AssistantController::activityPart),
text);
return Flux.concat(
Flux.just(new AssistantStreamPart.Activity(
AssistantStreamPart.Activity.Phase.RETRIEVAL,
Expand All @@ -408,7 +412,7 @@ Flux<AssistantStreamPart> parts(AssistantTurn turn) {
generation,
Flux.fromIterable(turn.citations())
.map(AssistantController::sourcePart),
text,
live,
Flux.just(new AssistantStreamPart.FinishStep()));
}

Expand Down Expand Up @@ -447,4 +451,12 @@ private static AssistantStreamPart sourcePart(AssistantCitation citation) {
title,
citation.number());
}

private static AssistantStreamPart activityPart(
AssistantAgentActivity activity) {
return new AssistantStreamPart.Activity(
AssistantStreamPart.Activity.Phase.valueOf(activity.phase().name()),
AssistantStreamPart.Activity.State.valueOf(activity.state().name()),
activity.resultCount());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,16 @@ record Activity(

enum Phase {
RETRIEVAL,
GENERATION
GENERATION,
SKILL_DISCOVERY,
SKILL_ACTIVATION,
SKILL_RESOURCE
}

enum State {
ACTIVE,
COMPLETE
COMPLETE,
FAILED
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import com.orgmemory.core.ai.AiWorkload;
import com.orgmemory.core.ai.ChatGenerationRequest;
import com.orgmemory.core.ai.ChatModelPort;
import com.orgmemory.core.assistant.AssistantAgentModelPort;
import com.orgmemory.core.assetregistry.api.AssetConflictException;
import com.orgmemory.core.assetregistry.api.AssetNotFoundException;
import com.orgmemory.core.assetregistry.api.AssetRole;
Expand Down Expand Up @@ -200,7 +201,7 @@ class AssetRegistryIntegrationTests {
@MockitoBean
KnowledgeCatalogQuery knowledgeCatalog;

@MockitoBean
@MockitoBean(extraInterfaces = AssistantAgentModelPort.class)
ChatModelPort chat;

@MockitoBean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,25 @@ void emitsAiSdkUiMessageFramesInOrder() {
"[DONE]");
}

@Test
void emitsTransientSkillToolActivityWithoutPersistingToolPayloads() {
List<String> data = UiMessageStream.encode(
Flux.just(new AssistantStreamPart.Activity(
AssistantStreamPart.Activity.Phase.SKILL_ACTIVATION,
AssistantStreamPart.Activity.State.COMPLETE,
1)),
MESSAGE_ID,
json,
Duration.ofHours(1),
Duration.ofMinutes(1))
.map(ServerSentEvent::data)
.collectList()
.block();

assertThat(data).contains(
"{\"type\":\"data-assistantActivity\",\"data\":{\"phase\":\"SKILL_ACTIVATION\",\"state\":\"COMPLETE\",\"evidenceCount\":1},\"transient\":true}");
}

@Test
void heartbeatIsAnSseComment() {
StepVerifier.withVirtualTime(() -> UiMessageStream.encode(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ sourceRefs:
- docs/specs/domains/asset-registry.md
- docs/specs/domains/assistant-and-mcp.md
- docs/specs/domains/secure-retrieval.md
- core/src/main/java/com/orgmemory/core/assetregistry/skill/SkillRuntimeOperations.java
- integrations/ai-model-gateways/src/main/java/com/orgmemory/integrations/ai/gateway/SpringAiChatModelAdapter.java
- apps/web/src/features/assistant/components/assistant-page.tsx
- apps/web/src/features/assets/components/asset-catalog-page.tsx
- apps/web/src/features/assets/components/asset-detail-page.tsx
- apps/web/src/features/assets/components/pack-journey-page.tsx
- apps/web/test/e2e/asset-registry-golden-poc.spec.ts
lastReviewed: 2026-07-29
lastReviewed: 2026-08-05
---

This journey begins after an organization administrator has given you access
Expand Down Expand Up @@ -92,6 +95,13 @@ State-changing actions and external model calls require explicit confirmation.
The Assistant cannot approve, publish, withdraw, change permissions, or execute
arbitrary tools on your behalf.

During a grounded answer, the Assistant may discover a Skill you are currently
allowed to use, load the instructions from its exact release, and read a bounded
supporting text file when needed. These reads do not install or execute the
Skill, and package metadata cannot grant the Assistant additional tools or
permissions. If no authorized Knowledge evidence is available, the Assistant
does not start this Skill flow.

## 4. Complete a Capability Pack

A Capability Pack joins exact Knowledge versions, Prompt templates, Work
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ sourceRefs:
- docs/specs/domains/asset-registry.md
- docs/specs/domains/assistant-and-mcp.md
- docs/specs/domains/secure-retrieval.md
- core/src/main/java/com/orgmemory/core/assetregistry/skill/SkillRuntimeOperations.java
- integrations/ai-model-gateways/src/main/java/com/orgmemory/integrations/ai/gateway/SpringAiChatModelAdapter.java
- apps/web/src/features/assistant/components/assistant-page.tsx
- apps/web/src/features/assets/components/asset-catalog-page.tsx
- apps/web/src/features/assets/components/asset-detail-page.tsx
- apps/web/src/features/assets/components/pack-journey-page.tsx
- apps/web/test/e2e/asset-registry-golden-poc.spec.ts
lastReviewed: 2026-07-30
lastReviewed: 2026-08-05
---

Hành trình này bắt đầu sau khi administrator của tổ chức cấp quyền cho bạn và có
Expand Down Expand Up @@ -96,6 +99,13 @@ Hành động thay đổi trạng thái và lần gọi mô hình bên ngoài c
ràng. Assistant không thể thay bạn phê duyệt, publish, withdraw, thay đổi quyền
hoặc thực thi công cụ tùy ý.

Trong lúc chuẩn bị câu trả lời có grounding, Assistant có thể tìm một Skill mà
bạn hiện được phép sử dụng, tải instruction từ đúng bản phát hành và đọc một file
text hỗ trợ có giới hạn khi cần. Các thao tác đọc này không cài đặt hay thực thi
Skill, và metadata trong package không thể cấp thêm công cụ hoặc quyền cho
Assistant. Nếu không có bằng chứng Knowledge được cấp quyền, Assistant sẽ không
bắt đầu luồng Skill này.

## 4. Hoàn thành một Capability Pack

Capability Pack kết hợp đúng phiên bản Knowledge, Prompt Template, Work
Expand Down
38 changes: 38 additions & 0 deletions apps/web/src/features/assistant/assistant-activity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export interface AssistantActivity {
phase:
| "RETRIEVAL"
| "GENERATION"
| "SKILL_DISCOVERY"
| "SKILL_ACTIVATION"
| "SKILL_RESOURCE"
state: "ACTIVE" | "COMPLETE" | "FAILED"
evidenceCount?: number | null
}

export function activityLabel(activity: AssistantActivity | null) {
if (!activity) return "Connecting to the Assistant…"
if (activity.phase === "RETRIEVAL" && activity.state === "ACTIVE") {
return "Searching permitted knowledge…"
}
if (activity.phase === "RETRIEVAL") {
const count = activity.evidenceCount ?? 0
return count === 1 ? "Found 1 permitted source" : `Found ${count} permitted sources`
}
if (activity.phase === "SKILL_DISCOVERY") {
if (activity.state === "ACTIVE") return "Looking for a relevant skill…"
if (activity.state === "FAILED") return "Skill search unavailable — continuing…"
const count = activity.evidenceCount ?? 0
return count === 1 ? "Found 1 available skill" : `Found ${count} available skills`
}
if (activity.phase === "SKILL_ACTIVATION") {
if (activity.state === "ACTIVE") return "Loading skill instructions…"
if (activity.state === "FAILED") return "Skill unavailable — continuing…"
return "Skill instructions ready"
}
if (activity.phase === "SKILL_RESOURCE") {
if (activity.state === "ACTIVE") return "Reading a skill reference…"
if (activity.state === "FAILED") return "Skill reference unavailable — continuing…"
return "Skill reference ready"
}
return "Preparing the grounded answer…"
}
24 changes: 24 additions & 0 deletions apps/web/src/features/assistant/components/assistant-page.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { describe, expect, it } from "vitest"

import { activityLabel } from "@/features/assistant/assistant-activity"

describe("assistant activity labels", () => {
it("describes progressive Skill disclosure without exposing tool payloads", () => {
expect(
activityLabel({ phase: "SKILL_DISCOVERY", state: "ACTIVE" }),
).toBe("Looking for a relevant skill…")
expect(
activityLabel({
phase: "SKILL_DISCOVERY",
state: "COMPLETE",
evidenceCount: 2,
}),
).toBe("Found 2 available skills")
expect(
activityLabel({ phase: "SKILL_ACTIVATION", state: "COMPLETE" }),
).toBe("Skill instructions ready")
expect(
activityLabel({ phase: "SKILL_RESOURCE", state: "FAILED" }),
).toBe("Skill reference unavailable — continuing…")
})
})
32 changes: 12 additions & 20 deletions apps/web/src/features/assistant/components/assistant-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ import { Suggestion, Suggestions } from "@/components/ai-elements/suggestion"
import { Button } from "@/components/ui/button"
import { InputGroupButton } from "@/components/ui/input-group"
import { createAssistantTransport } from "@/features/assistant/api/chat-transport"
import {
activityLabel,
type AssistantActivity,
} from "@/features/assistant/assistant-activity"
import { AssistantAnswer } from "@/features/assistant/components/assistant-answer"
import { AssistantThinkingIndicator } from "@/features/assistant/components/assistant-thinking-indicator"
import {
Expand Down Expand Up @@ -81,12 +85,6 @@ import type {

type AnswerSentiment = "HELPFUL" | "NOT_HELPFUL"

interface AssistantActivity {
phase: "RETRIEVAL" | "GENERATION"
state: "ACTIVE" | "COMPLETE"
evidenceCount?: number | null
}

function textFor(message: UIMessage) {
return message.parts
.filter((part) => part.type === "text")
Expand Down Expand Up @@ -223,8 +221,14 @@ function isAssistantActivity(value: unknown): value is AssistantActivity {
if (!value || typeof value !== "object") return false
const activity = value as Record<string, unknown>
return (
(activity.phase === "RETRIEVAL" || activity.phase === "GENERATION") &&
(activity.state === "ACTIVE" || activity.state === "COMPLETE") &&
(activity.phase === "RETRIEVAL" ||
activity.phase === "GENERATION" ||
activity.phase === "SKILL_DISCOVERY" ||
activity.phase === "SKILL_ACTIVATION" ||
activity.phase === "SKILL_RESOURCE") &&
(activity.state === "ACTIVE" ||
activity.state === "COMPLETE" ||
activity.state === "FAILED") &&
(activity.evidenceCount === undefined ||
activity.evidenceCount === null ||
(typeof activity.evidenceCount === "number" &&
Expand All @@ -233,18 +237,6 @@ function isAssistantActivity(value: unknown): value is AssistantActivity {
)
}

function activityLabel(activity: AssistantActivity | null) {
if (!activity) return "Connecting to the Assistant…"
if (activity.phase === "RETRIEVAL" && activity.state === "ACTIVE") {
return "Searching permitted knowledge…"
}
if (activity.phase === "RETRIEVAL") {
const count = activity.evidenceCount ?? 0
return count === 1 ? "Found 1 permitted source" : `Found ${count} permitted sources`
}
return "Preparing the grounded answer…"
}

function CitationHydration({
message,
actorKey,
Expand Down
15 changes: 13 additions & 2 deletions apps/web/test/e2e/assistant-pipeline.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,12 @@ function citedAnswerFrames() {
activityFrame("RETRIEVAL", "ACTIVE"),
activityFrame("RETRIEVAL", "COMPLETE", 3),
activityFrame("GENERATION", "ACTIVE"),
activityFrame("SKILL_DISCOVERY", "ACTIVE"),
activityFrame("SKILL_DISCOVERY", "COMPLETE", 1),
activityFrame("SKILL_ACTIVATION", "ACTIVE"),
activityFrame("SKILL_ACTIVATION", "COMPLETE"),
activityFrame("SKILL_RESOURCE", "ACTIVE"),
activityFrame("SKILL_RESOURCE", "COMPLETE"),
sourceFrame(1, FIRST_CHUNK_ID, "Employee Handbook"),
sourceFrame(2, SECOND_CHUNK_ID, "Expense Policy"),
sourceFrame(3, THIRD_CHUNK_ID, "Security Policy"),
Expand Down Expand Up @@ -850,8 +856,13 @@ function textOnlyFrames(text: string) {
}

function activityFrame(
phase: "RETRIEVAL" | "GENERATION",
state: "ACTIVE" | "COMPLETE",
phase:
| "RETRIEVAL"
| "GENERATION"
| "SKILL_DISCOVERY"
| "SKILL_ACTIVATION"
| "SKILL_RESOURCE",
state: "ACTIVE" | "COMPLETE" | "FAILED",
evidenceCount?: number,
) {
return frame({
Expand Down
Loading