mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-19 13:45:28 +00:00
feat(agent-core-v2): add scope-typed IScopeHandle aliases
- make IScopeHandle generic over LifecycleScope and add IAppScopeHandle / ISessionScopeHandle / IAgentScopeHandle so session and agent handles cannot be passed to each other's parameters at compile time - thread the aliases through IAgentLifecycleService / ISessionLifecycleService and their consumers (sessionLegacy, messageLegacy, server-v2 mainAgent and skills), narrowing at the lifecycle boundary - dep-graph viewer: render scope-mismatch (token registered at a scope the caller cannot see) distinctly from a genuinely missing implementation
This commit is contained in:
parent
fff2babe42
commit
d4a6a20db8
14 changed files with 544 additions and 165 deletions
|
|
@ -12,12 +12,18 @@ import {
|
|||
type Viewport,
|
||||
} from '@xyflow/react';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import { useMemo } from 'react';
|
||||
import { Fragment, useMemo, useState } from 'react';
|
||||
|
||||
import type { Edge, EdgeKind, Graph, ServiceNode } from '../../analyzer/types';
|
||||
import type { Edge, EdgeKind, EdgeRef, Graph, ServiceNode } from '../../analyzer/types';
|
||||
import type { FilterState } from './Filters';
|
||||
import { layoutDagre } from './layout-dagre';
|
||||
import { EDGE_STYLE, SCOPE_STYLE } from './style';
|
||||
import {
|
||||
EDGE_STYLE,
|
||||
SCOPE_MISMATCH_COLOR,
|
||||
SCOPE_STYLE,
|
||||
UNRESOLVED_COLOR,
|
||||
} from './style';
|
||||
import { tagColor, type TagMap } from './tags';
|
||||
|
||||
/** Fixed node width so port rows have a stable horizontal box. */
|
||||
const NODE_WIDTH = 300;
|
||||
|
|
@ -27,6 +33,8 @@ const HEADER_HEIGHT = 68;
|
|||
const PORT_ROW_HEIGHT = 18;
|
||||
/** Vertical padding between the header divider and the first port row. */
|
||||
const PORTS_PAD_TOP = 4;
|
||||
/** Height reserved for the tag chip row when a node carries at least one tag. */
|
||||
const TAGS_ROW_HEIGHT = 20;
|
||||
|
||||
/**
|
||||
* Per-node method port lists. `outPorts` are methods on this service that
|
||||
|
|
@ -52,6 +60,10 @@ interface GraphViewProps {
|
|||
/** Selected `ServiceNode.id`. */
|
||||
selectedId?: string;
|
||||
onSelect: (id?: string) => void;
|
||||
/** User-authored tags, keyed by `ServiceNode.id`. */
|
||||
tags: TagMap;
|
||||
/** Replace the full tag list for a node (empty list clears the entry). */
|
||||
onEditTags: (nodeId: string, tags: string[]) => void;
|
||||
}
|
||||
|
||||
interface ServiceNodeData extends Record<string, unknown> {
|
||||
|
|
@ -65,6 +77,8 @@ interface ServiceNodeData extends Record<string, unknown> {
|
|||
matched: boolean;
|
||||
dim: boolean;
|
||||
ports: ServicePortsInfo;
|
||||
/** Tags attached to this node, in entry order. */
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
const EVENT_KINDS: Set<EdgeKind> = new Set(['publish', 'subscribe', 'emit', 'on']);
|
||||
|
|
@ -137,30 +151,35 @@ function computeServicePorts(
|
|||
return result;
|
||||
}
|
||||
|
||||
function nodeHeight(ports: ServicePortsInfo): number {
|
||||
function nodeHeight(ports: ServicePortsInfo, hasTags: boolean): number {
|
||||
const rows = Math.max(ports.inPorts.length, ports.outPorts.length);
|
||||
if (rows === 0) return HEADER_HEIGHT;
|
||||
return HEADER_HEIGHT + PORTS_PAD_TOP + rows * PORT_ROW_HEIGHT + PORTS_PAD_TOP;
|
||||
const base = rows === 0 ? HEADER_HEIGHT : HEADER_HEIGHT + PORTS_PAD_TOP + rows * PORT_ROW_HEIGHT + PORTS_PAD_TOP;
|
||||
return hasTags ? base + TAGS_ROW_HEIGHT : base;
|
||||
}
|
||||
|
||||
function ServiceNodeView({ data }: NodeProps<Node<ServiceNodeData>>): JSX.Element {
|
||||
const { service, selected, matched, dim, ports } = data;
|
||||
const { service, selected, matched, dim, ports, tags } = data;
|
||||
const bg = SCOPE_STYLE[service.scope].color;
|
||||
const rowCount = Math.max(ports.inPorts.length, ports.outPorts.length);
|
||||
// Interface-only node: the token is referenced but has no registered impl.
|
||||
// Flagged with a dashed warning border so missing bindings stand out from
|
||||
// concrete services at a glance. Selection / search-match still win so the
|
||||
// active node stays unambiguous.
|
||||
// active node stays unambiguous. Scope-mismatch nodes (token registered, but
|
||||
// at a scope the caller can't see) get a distinct amber dashed border.
|
||||
const isUnresolved = service.unresolved === true;
|
||||
const isScopeMismatch = service.scopeMismatch === true;
|
||||
const specialBorder = isUnresolved || isScopeMismatch;
|
||||
const borderColor = selected
|
||||
? '#ffdf5d'
|
||||
: matched
|
||||
? '#79c0ff'
|
||||
: isUnresolved
|
||||
? '#f85149'
|
||||
: 'rgba(0,0,0,0.4)';
|
||||
const borderWidth = selected || matched || isUnresolved ? 2 : 1;
|
||||
const borderStyle = isUnresolved && !selected && !matched ? 'dashed' : 'solid';
|
||||
? UNRESOLVED_COLOR
|
||||
: isScopeMismatch
|
||||
? SCOPE_MISMATCH_COLOR
|
||||
: 'rgba(0,0,0,0.4)';
|
||||
const borderWidth = selected || matched || specialBorder ? 2 : 1;
|
||||
const borderStyle = specialBorder && !selected && !matched ? 'dashed' : 'solid';
|
||||
const glow = selected
|
||||
? '0 0 0 3px rgba(255,223,93,0.25)'
|
||||
: matched
|
||||
|
|
@ -224,11 +243,17 @@ function ServiceNodeView({ data }: NodeProps<Node<ServiceNodeData>>): JSX.Elemen
|
|||
</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 10, opacity: 0.65, marginTop: 2, fontStyle: 'italic' }}>
|
||||
{isUnresolved ? 'no implementation registered' : service.token}
|
||||
{isUnresolved
|
||||
? 'no implementation registered'
|
||||
: isScopeMismatch
|
||||
? `registered at ${service.scope} · cross-scope ref`
|
||||
: service.token}
|
||||
</div>
|
||||
<div style={{ fontSize: 10, opacity: 0.75, marginTop: 2 }}>{service.domain}</div>
|
||||
</div>
|
||||
|
||||
{tags.length > 0 && <TagChips tags={tags} />}
|
||||
|
||||
{rowCount > 0 && (
|
||||
<div
|
||||
style={{
|
||||
|
|
@ -351,6 +376,187 @@ function BandLabelView({ data }: NodeProps<Node<{ scope: string; width: number }
|
|||
|
||||
const nodeTypes = { service: ServiceNodeView, band: BandLabelView };
|
||||
|
||||
/** Non-interactive row of tag chips, used inside a graph node. */
|
||||
function TagChips({ tags }: { tags: string[] }): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 3,
|
||||
padding: '0 8px 5px',
|
||||
}}
|
||||
>
|
||||
{tags.map((tag) => (
|
||||
<TagChip key={tag} tag={tag} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface TagChipProps {
|
||||
tag: string;
|
||||
/** When provided, renders a remove affordance. */
|
||||
onRemove?: () => void;
|
||||
}
|
||||
|
||||
function TagChip({ tag, onRemove }: TagChipProps): JSX.Element {
|
||||
const { color, bg } = tagColor(tag);
|
||||
return (
|
||||
<span
|
||||
title={tag}
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 3,
|
||||
maxWidth: onRemove ? 150 : 120,
|
||||
padding: '1px 5px',
|
||||
fontSize: 9,
|
||||
lineHeight: '14px',
|
||||
color,
|
||||
background: bg,
|
||||
border: `1px solid ${color}`,
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
{onRemove && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRemove();
|
||||
}}
|
||||
aria-label={`remove tag ${tag}`}
|
||||
style={{
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
color,
|
||||
cursor: 'pointer',
|
||||
padding: 0,
|
||||
fontSize: 11,
|
||||
lineHeight: 1,
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
interface TagEditorProps {
|
||||
tags: string[];
|
||||
/** Known tags across the graph, offered as input suggestions. */
|
||||
allTags: string[];
|
||||
onChange: (next: string[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-node tag editor rendered in the side panel. Chips remove on click; the
|
||||
* input adds on Enter or the add button, normalising whitespace and refusing
|
||||
* duplicates. `allTags` feeds a `<datalist>` so existing tags are one keystroke
|
||||
* away — keeps spelling consistent so grouping actually groups.
|
||||
*/
|
||||
function TagEditor({ tags, allTags, onChange }: TagEditorProps): JSX.Element {
|
||||
const [draft, setDraft] = useState('');
|
||||
const listId = 'tag-suggestions';
|
||||
|
||||
function commit(raw: string): void {
|
||||
const tag = raw.trim();
|
||||
if (!tag || tags.includes(tag)) {
|
||||
setDraft('');
|
||||
return;
|
||||
}
|
||||
onChange([...tags, tag]);
|
||||
setDraft('');
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 10,
|
||||
fontWeight: 600,
|
||||
textTransform: 'uppercase',
|
||||
color: '#7d8590',
|
||||
marginBottom: 4,
|
||||
letterSpacing: 0.5,
|
||||
}}
|
||||
>
|
||||
tags
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginBottom: 6 }}>
|
||||
{tags.length === 0 ? (
|
||||
<span style={{ color: '#6e7681', fontSize: 11 }}>no tags</span>
|
||||
) : (
|
||||
tags.map((tag) => (
|
||||
<TagChip
|
||||
key={tag}
|
||||
tag={tag}
|
||||
onRemove={() => onChange(tags.filter((t) => t !== tag))}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<input
|
||||
value={draft}
|
||||
list={listId}
|
||||
placeholder="add tag…"
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
commit(draft);
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
padding: '4px 7px',
|
||||
background: '#0e1116',
|
||||
color: '#e6edf3',
|
||||
border: '1px solid #30363d',
|
||||
borderRadius: 4,
|
||||
fontSize: 11,
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={() => commit(draft)}
|
||||
style={{
|
||||
padding: '4px 10px',
|
||||
background: '#21262d',
|
||||
color: '#e6edf3',
|
||||
border: '1px solid #30363d',
|
||||
borderRadius: 4,
|
||||
cursor: 'pointer',
|
||||
fontSize: 11,
|
||||
}}
|
||||
>
|
||||
add
|
||||
</button>
|
||||
<datalist id={listId}>
|
||||
{allTags
|
||||
.filter((t) => !tags.includes(t))
|
||||
.map((t) => (
|
||||
<option key={t} value={t} />
|
||||
))}
|
||||
</datalist>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the pan/zoom viewport across dev-server reloads so a source-code
|
||||
* edit (which triggers a `full-reload` from the `virtual:dep-graph` plugin)
|
||||
|
|
@ -416,6 +622,8 @@ export function GraphView({
|
|||
filters,
|
||||
selectedId,
|
||||
onSelect,
|
||||
tags,
|
||||
onEditTags,
|
||||
}: GraphViewProps): JSX.Element {
|
||||
// Compute once at mount so a re-render that adds nodes doesn't yank the
|
||||
// viewport back to the stored value while the user is panning.
|
||||
|
|
@ -451,10 +659,12 @@ export function GraphView({
|
|||
// dead weight on the node, so we compute after filter+visibility.
|
||||
const ports = computeServicePorts(visibleServices, finalEdges);
|
||||
|
||||
// Compute the two focus drivers:
|
||||
// Compute the three focus drivers:
|
||||
// • `selectedId` — the click-selected node (0 or 1 at a time).
|
||||
// • `matched` — every node whose identity or public surface hits
|
||||
// the current search string.
|
||||
// • `tagMatched` — every node carrying at least one active tag
|
||||
// (the "group by tag" view).
|
||||
// Their neighbours (nodes touched by any surviving edge) are folded in
|
||||
// so the graph keeps enough context around a hit to be readable —
|
||||
// this is the "act like a click" behaviour: nothing disappears, just
|
||||
|
|
@ -467,6 +677,16 @@ export function GraphView({
|
|||
}
|
||||
}
|
||||
|
||||
// Tag focus: every visible node carrying at least one active tag seeds
|
||||
// the focus set, so the graph reads as "the group(s) these tags pick out".
|
||||
const tagMatched = new Set<string>();
|
||||
if (filters.activeTags.size > 0) {
|
||||
for (const s of visibleServices) {
|
||||
const st = tags[s.id];
|
||||
if (st && st.some((t) => filters.activeTags.has(t))) tagMatched.add(s.id);
|
||||
}
|
||||
}
|
||||
|
||||
const focused = new Set<string>();
|
||||
const seedFocus = (id: string): void => {
|
||||
focused.add(id);
|
||||
|
|
@ -477,14 +697,21 @@ export function GraphView({
|
|||
};
|
||||
if (selectedId !== undefined) seedFocus(selectedId);
|
||||
for (const id of matched) seedFocus(id);
|
||||
for (const id of tagMatched) seedFocus(id);
|
||||
|
||||
const focusActive = selectedId !== undefined || matched.size > 0;
|
||||
const focusActive =
|
||||
selectedId !== undefined || matched.size > 0 || tagMatched.size > 0;
|
||||
|
||||
const layout = layoutDagre(visibleServices, finalEdges, {
|
||||
groupByScope: filters.groupByScope,
|
||||
nodeSize: (id) => {
|
||||
const p = ports.get(id);
|
||||
return { width: NODE_WIDTH, height: p ? nodeHeight(p) : HEADER_HEIGHT };
|
||||
const p = ports.get(id) ?? {
|
||||
inPorts: [],
|
||||
outPorts: [],
|
||||
connectedIn: new Set<string>(),
|
||||
};
|
||||
const hasTags = (tags[id]?.length ?? 0) > 0;
|
||||
return { width: NODE_WIDTH, height: nodeHeight(p, hasTags) };
|
||||
},
|
||||
});
|
||||
const pos = layout.positions;
|
||||
|
|
@ -504,6 +731,7 @@ export function GraphView({
|
|||
outPorts: [],
|
||||
connectedIn: new Set<string>(),
|
||||
},
|
||||
tags: tags[service.id] ?? [],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
|
@ -574,7 +802,7 @@ export function GraphView({
|
|||
: [];
|
||||
|
||||
return { nodes: rfNodes, edges: rfEdges, selectedService, selectedEdges };
|
||||
}, [graph, filters, selectedId]);
|
||||
}, [graph, filters, selectedId, tags]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -608,7 +836,11 @@ export function GraphView({
|
|||
if (n.id.startsWith('band::')) return 'transparent';
|
||||
const service = (n.data as ServiceNodeData | undefined)?.service;
|
||||
if (!service) return '#7d8590';
|
||||
return service.unresolved ? '#f85149' : SCOPE_STYLE[service.scope].color;
|
||||
return service.unresolved
|
||||
? UNRESOLVED_COLOR
|
||||
: service.scopeMismatch
|
||||
? SCOPE_MISMATCH_COLOR
|
||||
: SCOPE_STYLE[service.scope].color;
|
||||
}}
|
||||
/>
|
||||
<Controls showInteractive={false} style={{ background: '#151b23' }} />
|
||||
|
|
@ -619,6 +851,8 @@ export function GraphView({
|
|||
graph={graph}
|
||||
edges={selectedEdges}
|
||||
onClose={() => onSelect(undefined)}
|
||||
tags={tags}
|
||||
onEditTags={onEditTags}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
@ -630,19 +864,33 @@ interface ServicePanelProps {
|
|||
graph: Graph;
|
||||
edges: Edge[];
|
||||
onClose: () => void;
|
||||
tags: TagMap;
|
||||
onEditTags: (nodeId: string, tags: string[]) => void;
|
||||
}
|
||||
|
||||
function ServicePanel({ service, graph, edges, onClose }: ServicePanelProps): JSX.Element {
|
||||
function ServicePanel({
|
||||
service,
|
||||
graph,
|
||||
edges,
|
||||
onClose,
|
||||
tags,
|
||||
onEditTags,
|
||||
}: ServicePanelProps): JSX.Element {
|
||||
const outgoing = edges.filter((e) => e.from === service.id);
|
||||
const incoming = edges.filter((e) => e.to === service.id && e.from !== service.id);
|
||||
const byId = new Map(graph.services.map((s) => [s.id, s]));
|
||||
const nodeTags = tags[service.id] ?? [];
|
||||
const allTags = useMemo(
|
||||
() => [...new Set(Object.values(tags).flat())].sort(),
|
||||
[tags],
|
||||
);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
right: 12,
|
||||
width: 360,
|
||||
width: 420,
|
||||
maxHeight: 'calc(100vh - 24px)',
|
||||
overflowY: 'auto',
|
||||
background: 'rgba(21,27,35,0.96)',
|
||||
|
|
@ -657,16 +905,20 @@ function ServicePanel({ service, graph, edges, onClose }: ServicePanelProps): JS
|
|||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontWeight: 600, fontSize: 13 }}>{service.impl}</div>
|
||||
{service.unresolved ? (
|
||||
<div style={{ color: '#f85149', fontSize: 11, marginTop: 2 }}>
|
||||
<div style={{ color: UNRESOLVED_COLOR, fontSize: 11, marginTop: 2 }}>
|
||||
No implementation registered
|
||||
</div>
|
||||
) : service.scopeMismatch ? (
|
||||
<div style={{ color: SCOPE_MISMATCH_COLOR, fontSize: 11, marginTop: 2 }}>
|
||||
Registered at {service.scope} — not visible from the caller's scope
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ color: '#a5b0bc', fontSize: 11 }}>{service.token}</div>
|
||||
)}
|
||||
<div style={{ color: '#7d8590', fontSize: 11 }}>
|
||||
<b>{service.scope}</b> · {service.domain}
|
||||
</div>
|
||||
{!service.unresolved && (
|
||||
{!service.unresolved && !service.scopeMismatch && (
|
||||
<div style={{ color: '#7d8590', fontSize: 10, marginTop: 4, wordBreak: 'break-all' }}>
|
||||
{service.file}:{service.line}
|
||||
</div>
|
||||
|
|
@ -687,6 +939,14 @@ function ServicePanel({ service, graph, edges, onClose }: ServicePanelProps): JS
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<TagEditor
|
||||
tags={nodeTags}
|
||||
allTags={allTags}
|
||||
onChange={(next) => {
|
||||
onEditTags(service.id, next);
|
||||
}}
|
||||
/>
|
||||
|
||||
<EdgeList
|
||||
title={`out (${outgoing.length})`}
|
||||
edges={outgoing}
|
||||
|
|
@ -710,7 +970,44 @@ interface EdgeListProps {
|
|||
byId: Map<string, ServiceNode>;
|
||||
}
|
||||
|
||||
interface EdgeGroup {
|
||||
edge: Edge;
|
||||
peerLabel: string;
|
||||
peerToken?: string;
|
||||
/** Refs that have at least one attributed method — one table row each. */
|
||||
methodRefs: EdgeRef[];
|
||||
/** Refs with neither `fromMethod` nor `toMethod` (ctor param decls etc.). */
|
||||
unattributedCount: number;
|
||||
}
|
||||
|
||||
function buildEdgeGroups(
|
||||
edges: Edge[],
|
||||
direction: 'in' | 'out',
|
||||
byId: Map<string, ServiceNode>,
|
||||
): EdgeGroup[] {
|
||||
return edges.map((e) => {
|
||||
const peerId = direction === 'out' ? e.to : e.from;
|
||||
const peer = byId.get(peerId);
|
||||
const peerLabel = peer ? peer.impl : peerId;
|
||||
const peerToken = peer?.token;
|
||||
const methodRefs = e.refs.filter(
|
||||
(r) => r.toMethod !== undefined || r.fromMethod !== undefined,
|
||||
);
|
||||
const unattributedCount = e.refs.length - methodRefs.length;
|
||||
return { edge: e, peerLabel, peerToken, methodRefs, unattributedCount };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Right-panel table of edges touching the selected service. One row per
|
||||
* attributed call ref; consecutive rows belonging to the same edge share
|
||||
* `kind` / `peer` cells via `rowSpan` so the grouping is visible without
|
||||
* repeating them. The self-side method column is bold so the direction of
|
||||
* each call reads at a glance (out ⇒ `from` bold, in ⇒ `to` bold).
|
||||
*/
|
||||
function EdgeList({ title, edges, direction, byId }: EdgeListProps): JSX.Element {
|
||||
const groups = buildEdgeGroups(edges, direction, byId);
|
||||
const selfIsFrom = direction === 'out';
|
||||
return (
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<div
|
||||
|
|
@ -725,84 +1022,163 @@ function EdgeList({ title, edges, direction, byId }: EdgeListProps): JSX.Element
|
|||
>
|
||||
{title}
|
||||
</div>
|
||||
{edges.length === 0 && <div style={{ color: '#7d8590', fontSize: 11 }}>—</div>}
|
||||
{edges.map((e) => {
|
||||
const peerId = direction === 'out' ? e.to : e.from;
|
||||
const peer = byId.get(peerId);
|
||||
const label = peer ? `${peer.impl} (${peer.token})` : peerId;
|
||||
const methodRefs = e.refs.filter((r) => r.toMethod !== undefined || r.fromMethod !== undefined);
|
||||
return (
|
||||
<div key={`${e.from}::${e.kind}::${e.to}`} style={{ padding: '3px 0' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: 10,
|
||||
height: 3,
|
||||
borderTop: `${EDGE_STYLE[e.kind].dashed ? '2px dashed' : '2px solid'} ${
|
||||
EDGE_STYLE[e.kind].color
|
||||
}`,
|
||||
}}
|
||||
/>
|
||||
<span style={{ color: '#7d8590', fontSize: 10, minWidth: 62 }}>{e.kind}</span>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: 'ui-monospace, monospace',
|
||||
fontSize: 11,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
<span style={{ color: '#7d8590', fontSize: 10 }}>×{e.refs.length}</span>
|
||||
</div>
|
||||
{methodRefs.length > 0 && (
|
||||
<details style={{ marginLeft: 20, marginTop: 2 }}>
|
||||
<summary
|
||||
style={{
|
||||
fontSize: 10,
|
||||
color: '#8b949e',
|
||||
cursor: 'pointer',
|
||||
listStyle: 'revert',
|
||||
}}
|
||||
>
|
||||
{methodRefs.length} call{methodRefs.length === 1 ? '' : 's'}
|
||||
</summary>
|
||||
<div style={{ marginTop: 3 }}>
|
||||
{methodRefs.map((r, i) => (
|
||||
<div
|
||||
key={`${r.file}:${r.line}:${i}`}
|
||||
{groups.length === 0 ? (
|
||||
<div style={{ color: '#7d8590', fontSize: 11 }}>—</div>
|
||||
) : (
|
||||
<table style={tableStyle}>
|
||||
<colgroup>
|
||||
<col style={{ width: 72 }} />
|
||||
<col style={{ width: 128 }} />
|
||||
<col />
|
||||
<col style={{ width: 40 }} />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={thStyle}>kind</th>
|
||||
<th style={thStyle}>peer</th>
|
||||
<th style={thStyle}>from → to</th>
|
||||
<th style={{ ...thStyle, textAlign: 'right' }}>line</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{groups.map((g) => {
|
||||
const kindStyle = EDGE_STYLE[g.edge.kind];
|
||||
const kindCell = (
|
||||
<div style={cellClipStyle}>
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: 10,
|
||||
height: 3,
|
||||
borderTop: `${kindStyle.dashed ? '2px dashed' : '2px solid'} ${kindStyle.color}`,
|
||||
marginRight: 4,
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
/>
|
||||
<span style={{ color: '#a5b0bc' }}>{g.edge.kind}</span>
|
||||
</div>
|
||||
);
|
||||
const peerCell = (
|
||||
<div style={cellClipStyle} title={g.peerToken}>
|
||||
{g.peerLabel}
|
||||
</div>
|
||||
);
|
||||
const groupKey = `${g.edge.from}::${g.edge.kind}::${g.edge.to}`;
|
||||
if (g.methodRefs.length === 0) {
|
||||
return (
|
||||
<tr key={groupKey} style={groupBorderStyle}>
|
||||
<td style={tdStyle}>{kindCell}</td>
|
||||
<td style={tdStyle}>{peerCell}</td>
|
||||
<td
|
||||
colSpan={2}
|
||||
style={{
|
||||
fontFamily: 'ui-monospace, monospace',
|
||||
fontSize: 10,
|
||||
color: '#a5b0bc',
|
||||
padding: '1px 0',
|
||||
...tdStyle,
|
||||
color: '#6e7681',
|
||||
fontStyle: 'italic',
|
||||
}}
|
||||
>
|
||||
{direction === 'out' ? (
|
||||
<>
|
||||
<span>{r.fromMethod ?? '?'}</span>
|
||||
<span style={{ color: '#6e7681' }}>{' → '}</span>
|
||||
<span style={{ color: '#e6edf3' }}>{r.toMethod ?? '?'}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span style={{ color: '#e6edf3' }}>{r.fromMethod ?? '?'}</span>
|
||||
<span style={{ color: '#6e7681' }}>{' → '}</span>
|
||||
<span>{r.toMethod ?? '?'}</span>
|
||||
</>
|
||||
)}
|
||||
<span style={{ color: '#6e7681' }}>{` (:${r.line})`}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
— ×{g.edge.refs.length}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Fragment key={groupKey}>
|
||||
{g.methodRefs.map((r, i) => {
|
||||
const isFirst = i === 0;
|
||||
return (
|
||||
<tr
|
||||
key={`${groupKey}::${r.file}:${r.line}:${i}`}
|
||||
style={isFirst ? groupBorderStyle : undefined}
|
||||
>
|
||||
{isFirst && (
|
||||
<>
|
||||
<td rowSpan={g.methodRefs.length} style={tdStyle}>
|
||||
{kindCell}
|
||||
</td>
|
||||
<td rowSpan={g.methodRefs.length} style={tdStyle}>
|
||||
{peerCell}
|
||||
</td>
|
||||
</>
|
||||
)}
|
||||
<td style={tdCallStyle} title={`${r.fromMethod ?? '?'} → ${r.toMethod ?? '?'}`}>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: selfIsFrom ? 600 : 400,
|
||||
color: selfIsFrom ? '#e6edf3' : '#a5b0bc',
|
||||
}}
|
||||
>
|
||||
{r.fromMethod ?? '?'}
|
||||
</span>
|
||||
<span style={{ color: '#6e7681', margin: '0 4px' }}>→</span>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: !selfIsFrom ? 600 : 400,
|
||||
color: !selfIsFrom ? '#e6edf3' : '#a5b0bc',
|
||||
}}
|
||||
>
|
||||
{r.toMethod ?? '?'}
|
||||
</span>
|
||||
</td>
|
||||
<td style={tdLineStyle}>:{r.line}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const tableStyle: React.CSSProperties = {
|
||||
width: '100%',
|
||||
borderCollapse: 'collapse',
|
||||
tableLayout: 'fixed',
|
||||
fontFamily:
|
||||
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
|
||||
fontSize: 10.5,
|
||||
};
|
||||
|
||||
const thStyle: React.CSSProperties = {
|
||||
textAlign: 'left',
|
||||
fontWeight: 600,
|
||||
color: '#7d8590',
|
||||
fontSize: 9,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.5,
|
||||
padding: '3px 6px',
|
||||
borderBottom: '1px solid #30363d',
|
||||
};
|
||||
|
||||
const tdStyle: React.CSSProperties = {
|
||||
padding: '3px 6px',
|
||||
verticalAlign: 'top',
|
||||
};
|
||||
|
||||
const tdCallStyle: React.CSSProperties = {
|
||||
...tdStyle,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
};
|
||||
|
||||
const tdLineStyle: React.CSSProperties = {
|
||||
...tdStyle,
|
||||
textAlign: 'right',
|
||||
color: '#6e7681',
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
|
||||
const cellClipStyle: React.CSSProperties = {
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
|
||||
const groupBorderStyle: React.CSSProperties = {
|
||||
borderTop: '1px solid #21262d',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -22,4 +22,9 @@ export const SCOPE_STYLE: Record<ServiceScope, { color: string; badge: string }>
|
|||
Agent: { color: '#2f8a4d', badge: 'Agt' },
|
||||
};
|
||||
|
||||
/** Border / minimap color for scope-mismatch nodes (token registered elsewhere). */
|
||||
export const SCOPE_MISMATCH_COLOR = '#f0883e';
|
||||
/** Border / minimap color for unresolved nodes (token registered nowhere). */
|
||||
export const UNRESOLVED_COLOR = '#f85149';
|
||||
|
||||
export const EDGE_KINDS: EdgeKind[] = ['ctor', 'accessor', 'publish', 'subscribe', 'emit', 'on'];
|
||||
|
|
|
|||
|
|
@ -63,13 +63,20 @@ export interface ScopeOptions {
|
|||
readonly extra?: ScopeSeed;
|
||||
}
|
||||
|
||||
export interface IScopeHandle {
|
||||
export interface IScopeHandle<K extends LifecycleScope = LifecycleScope> {
|
||||
readonly id: string;
|
||||
readonly kind: LifecycleScope;
|
||||
readonly kind: K;
|
||||
readonly accessor: ServicesAccessor;
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
/** Handle to the process-root App scope. */
|
||||
export type IAppScopeHandle = IScopeHandle<LifecycleScope.App>;
|
||||
/** Handle to a Session scope (child of App). */
|
||||
export type ISessionScopeHandle = IScopeHandle<LifecycleScope.Session>;
|
||||
/** Handle to an Agent scope (child of Session). */
|
||||
export type IAgentScopeHandle = IScopeHandle<LifecycleScope.Agent>;
|
||||
|
||||
function buildCollection(kind: LifecycleScope, extra?: ScopeSeed): ServiceCollection {
|
||||
const collection = new ServiceCollection();
|
||||
for (const entry of _scopedRegistry) {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
import type { Message, PageResponse } from '@moonshot-ai/protocol';
|
||||
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { type IScopeHandle, LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
import { type IAgentScopeHandle, LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
import { IAgentLifecycleService } from '#/session/agent-lifecycle';
|
||||
import {
|
||||
IAgentContextMemoryService,
|
||||
|
|
@ -125,7 +125,7 @@ export class MessageLegacyService implements IMessageLegacyService {
|
|||
* Returns `undefined` only when a cold session's workspace is gone and the
|
||||
* session directory cannot be reconstructed (mirrors the `fork` limitation).
|
||||
*/
|
||||
private async resolveMainAgent(sessionId: string): Promise<IScopeHandle | undefined> {
|
||||
private async resolveMainAgent(sessionId: string): Promise<IAgentScopeHandle | undefined> {
|
||||
const session = await this.lifecycle.resume(sessionId);
|
||||
if (session === undefined) return undefined;
|
||||
const agents = session.accessor.get(IAgentLifecycleService);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
*/
|
||||
|
||||
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
|
||||
import type { IScopeHandle } from '#/_base/di/scope';
|
||||
import type { ISessionScopeHandle } from '#/_base/di/scope';
|
||||
import type { Event } from '#/_base/event';
|
||||
|
||||
export interface CreateSessionOptions {
|
||||
|
|
@ -32,7 +32,7 @@ export interface ForkSessionOptions {
|
|||
|
||||
export interface SessionCreatedEvent {
|
||||
readonly sessionId: string;
|
||||
readonly handle: IScopeHandle;
|
||||
readonly handle: ISessionScopeHandle;
|
||||
}
|
||||
|
||||
export interface SessionClosedEvent {
|
||||
|
|
@ -46,7 +46,7 @@ export interface SessionArchivedEvent {
|
|||
export interface SessionForkedEvent {
|
||||
readonly sourceSessionId: string;
|
||||
readonly sessionId: string;
|
||||
readonly handle: IScopeHandle;
|
||||
readonly handle: ISessionScopeHandle;
|
||||
}
|
||||
|
||||
export interface ISessionLifecycleService {
|
||||
|
|
@ -55,9 +55,9 @@ export interface ISessionLifecycleService {
|
|||
readonly onDidCloseSession: Event<SessionClosedEvent>;
|
||||
readonly onDidArchiveSession: Event<SessionArchivedEvent>;
|
||||
readonly onDidForkSession: Event<SessionForkedEvent>;
|
||||
create(opts: CreateSessionOptions): Promise<IScopeHandle>;
|
||||
get(sessionId: string): IScopeHandle | undefined;
|
||||
list(): readonly IScopeHandle[];
|
||||
create(opts: CreateSessionOptions): Promise<ISessionScopeHandle>;
|
||||
get(sessionId: string): ISessionScopeHandle | undefined;
|
||||
list(): readonly ISessionScopeHandle[];
|
||||
/**
|
||||
* Load a persisted session into the live scope tree and restore its main
|
||||
* agent from the persisted wire log. Returns the existing handle when the
|
||||
|
|
@ -70,10 +70,10 @@ export interface ISessionLifecycleService {
|
|||
* a previous process or by v1 — without requiring a prior `create` in this
|
||||
* process. Restores only the main agent; sub-agents are materialized lazily.
|
||||
*/
|
||||
resume(sessionId: string): Promise<IScopeHandle | undefined>;
|
||||
resume(sessionId: string): Promise<ISessionScopeHandle | undefined>;
|
||||
close(sessionId: string): Promise<void>;
|
||||
archive(sessionId: string): Promise<void>;
|
||||
fork(opts: ForkSessionOptions): Promise<IScopeHandle>;
|
||||
fork(opts: ForkSessionOptions): Promise<ISessionScopeHandle>;
|
||||
}
|
||||
|
||||
export const ISessionLifecycleService: ServiceIdentifier<ISessionLifecycleService> =
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { InstantiationType } from '#/_base/di/extensions';
|
|||
import { IInstantiationService } from '#/_base/di/instantiation';
|
||||
import {
|
||||
createScopedChildHandle,
|
||||
type IScopeHandle,
|
||||
type ISessionScopeHandle,
|
||||
LifecycleScope,
|
||||
registerScopedService,
|
||||
} from '#/_base/di/scope';
|
||||
|
|
@ -55,7 +55,7 @@ import {
|
|||
|
||||
export class SessionLifecycleService extends Disposable implements ISessionLifecycleService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
private readonly sessions = new Map<string, IScopeHandle>();
|
||||
private readonly sessions = new Map<string, ISessionScopeHandle>();
|
||||
private readonly _onDidCreateSession = this._register(new Emitter<SessionCreatedEvent>());
|
||||
readonly onDidCreateSession: Event<SessionCreatedEvent> = this._onDidCreateSession.event;
|
||||
private readonly _onDidCloseSession = this._register(new Emitter<SessionClosedEvent>());
|
||||
|
|
@ -67,7 +67,7 @@ export class SessionLifecycleService extends Disposable implements ISessionLifec
|
|||
/** In-flight `resume` promises, keyed by session id — de-dupes concurrent
|
||||
* cold loads so a hot read path (e.g. snapshot retry) cannot materialize
|
||||
* the same session twice and leak a handle. */
|
||||
private readonly resuming = new Map<string, Promise<IScopeHandle | undefined>>();
|
||||
private readonly resuming = new Map<string, Promise<ISessionScopeHandle | undefined>>();
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private readonly instantiation: IInstantiationService,
|
||||
|
|
@ -81,7 +81,7 @@ export class SessionLifecycleService extends Disposable implements ISessionLifec
|
|||
super();
|
||||
}
|
||||
|
||||
async create(opts: CreateSessionOptions): Promise<IScopeHandle> {
|
||||
async create(opts: CreateSessionOptions): Promise<ISessionScopeHandle> {
|
||||
const workspaceId = encodeWorkDirKey(opts.workDir);
|
||||
const sessionDir = join(this.bootstrap.sessionsDir, workspaceId, opts.sessionId);
|
||||
// Metadata lives at `<sessionDir>/state.json` (shared with v1's layout; the
|
||||
|
|
@ -112,7 +112,7 @@ export class SessionLifecycleService extends Disposable implements ISessionLifec
|
|||
...execContextSeed(execCtx),
|
||||
],
|
||||
},
|
||||
);
|
||||
) as ISessionScopeHandle;
|
||||
this.sessions.set(opts.sessionId, handle);
|
||||
await handle.accessor.get(ISessionMetadata).ready;
|
||||
void handle.accessor.get(ISessionSkillCatalog).load();
|
||||
|
|
@ -120,11 +120,11 @@ export class SessionLifecycleService extends Disposable implements ISessionLifec
|
|||
return handle;
|
||||
}
|
||||
|
||||
get(sessionId: string): IScopeHandle | undefined {
|
||||
get(sessionId: string): ISessionScopeHandle | undefined {
|
||||
return this.sessions.get(sessionId);
|
||||
}
|
||||
|
||||
resume(sessionId: string): Promise<IScopeHandle | undefined> {
|
||||
resume(sessionId: string): Promise<ISessionScopeHandle | undefined> {
|
||||
const live = this.sessions.get(sessionId);
|
||||
if (live !== undefined) return Promise.resolve(live);
|
||||
const inflight = this.resuming.get(sessionId);
|
||||
|
|
@ -134,7 +134,7 @@ export class SessionLifecycleService extends Disposable implements ISessionLifec
|
|||
return promise;
|
||||
}
|
||||
|
||||
private async doResume(sessionId: string): Promise<IScopeHandle | undefined> {
|
||||
private async doResume(sessionId: string): Promise<ISessionScopeHandle | undefined> {
|
||||
// Re-check after the serialized entry: a prior `resume` for the same id may
|
||||
// have already materialized the session while this call was queued.
|
||||
const live = this.sessions.get(sessionId);
|
||||
|
|
@ -158,7 +158,7 @@ export class SessionLifecycleService extends Disposable implements ISessionLifec
|
|||
return handle;
|
||||
}
|
||||
|
||||
list(): readonly IScopeHandle[] {
|
||||
list(): readonly ISessionScopeHandle[] {
|
||||
return [...this.sessions.values()];
|
||||
}
|
||||
|
||||
|
|
@ -179,7 +179,7 @@ export class SessionLifecycleService extends Disposable implements ISessionLifec
|
|||
this._onDidArchiveSession.fire({ sessionId });
|
||||
}
|
||||
|
||||
async fork(opts: ForkSessionOptions): Promise<IScopeHandle> {
|
||||
async fork(opts: ForkSessionOptions): Promise<ISessionScopeHandle> {
|
||||
const sourceId = opts.sourceSessionId;
|
||||
|
||||
// 1. Resolve the source: prefer a live handle, otherwise fall back to the
|
||||
|
|
@ -283,7 +283,7 @@ export class SessionLifecycleService extends Disposable implements ISessionLifec
|
|||
* sources (flush then read) and closed sources (read the persisted log).
|
||||
*/
|
||||
private async copyAgentWire(args: {
|
||||
readonly sourceHandle: IScopeHandle | undefined;
|
||||
readonly sourceHandle: ISessionScopeHandle | undefined;
|
||||
readonly sourceHomedir: string;
|
||||
readonly agentId: string;
|
||||
readonly targetSessionDir: string;
|
||||
|
|
|
|||
|
|
@ -10,9 +10,8 @@
|
|||
*/
|
||||
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { type IScopeHandle, LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
import { type IAgentScopeHandle, LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
import { IAgentLifecycleService } from '#/session/agent-lifecycle';
|
||||
import { IAuthSummaryService } from '#/app/auth';
|
||||
import { IAgentContextMemoryService, toProtocolMessage, type ContextMessage } from '#/agent/contextMemory';
|
||||
import { IAgentContextSizeService } from '#/agent/contextSize';
|
||||
import { ErrorCodes, isKimiError, KimiError } from '#/errors';
|
||||
|
|
@ -37,7 +36,6 @@ import type {
|
|||
ForkSessionRequest,
|
||||
SessionAbortResponse,
|
||||
SessionStatusResponse,
|
||||
StartBtwSessionResponse,
|
||||
UndoSessionRequest,
|
||||
UndoSessionResponse,
|
||||
} from '@moonshot-ai/protocol';
|
||||
|
|
@ -73,7 +71,6 @@ export class SessionLegacyService implements ISessionLegacyService {
|
|||
@ISessionLifecycleService private readonly lifecycle: ISessionLifecycleService,
|
||||
@ISessionIndex private readonly index: ISessionIndex,
|
||||
@IWorkspaceRegistry private readonly workspaceRegistry: IWorkspaceRegistry,
|
||||
@IAuthSummaryService private readonly auth: IAuthSummaryService,
|
||||
) {}
|
||||
|
||||
async fork(sessionId: string, body: ForkSessionRequest): Promise<SessionWireFields> {
|
||||
|
|
@ -222,16 +219,6 @@ export class SessionLegacyService implements ISessionLegacyService {
|
|||
return { aborted: true };
|
||||
}
|
||||
|
||||
async startBtw(sessionId: string): Promise<StartBtwSessionResponse> {
|
||||
if (this.lifecycle.get(sessionId) === undefined) {
|
||||
throw new KimiError(ErrorCodes.SESSION_NOT_FOUND, `session ${sessionId} does not exist`);
|
||||
}
|
||||
await this.auth.ensureReady();
|
||||
const agent = await this.resolveMainAgent(sessionId);
|
||||
const agentId = await agent.accessor.get(IAgentRPCService).startBtw({});
|
||||
return { agent_id: agentId };
|
||||
}
|
||||
|
||||
async archive(sessionId: string): Promise<ArchiveSessionResponse> {
|
||||
// Native `ISessionLifecycleService.archive` is a no-op for sessions that
|
||||
// are not live, so gate on the live handle (matches the previous route
|
||||
|
|
@ -278,7 +265,7 @@ export class SessionLegacyService implements ISessionLegacyService {
|
|||
* Resolve the session's main agent, creating it on demand (mirrors v1's
|
||||
* `resumeSession` + the server-v2 `ensureMainAgent` helper).
|
||||
*/
|
||||
private async resolveMainAgent(sessionId: string): Promise<IScopeHandle> {
|
||||
private async resolveMainAgent(sessionId: string): Promise<IAgentScopeHandle> {
|
||||
const session = this.lifecycle.get(sessionId);
|
||||
if (session === undefined) {
|
||||
throw new KimiError(ErrorCodes.SESSION_NOT_FOUND, `session ${sessionId} does not exist`);
|
||||
|
|
@ -289,7 +276,7 @@ export class SessionLegacyService implements ISessionLegacyService {
|
|||
return agents.createMain();
|
||||
}
|
||||
|
||||
private async assembleStatus(sessionId: string, agent: IScopeHandle): Promise<SessionStatusResponse> {
|
||||
private async assembleStatus(sessionId: string, agent: IAgentScopeHandle): Promise<SessionStatusResponse> {
|
||||
const session = this.lifecycle.get(sessionId);
|
||||
const profile = agent.accessor.get(IAgentProfileService);
|
||||
const contextSize = agent.accessor.get(IAgentContextSizeService);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
*/
|
||||
|
||||
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
|
||||
import type { IScopeHandle } from '#/_base/di/scope';
|
||||
import type { IAgentScopeHandle } from '#/_base/di/scope';
|
||||
import type { Event } from '#/_base/event';
|
||||
|
||||
export interface CreateAgentOptions {
|
||||
|
|
@ -22,15 +22,15 @@ export interface CreateAgentOptions {
|
|||
export interface IAgentLifecycleService {
|
||||
readonly _serviceBrand: undefined;
|
||||
/** Fires after an agent is created and registered, with its scope handle. */
|
||||
readonly onDidCreate: Event<IScopeHandle>;
|
||||
readonly onDidCreate: Event<IAgentScopeHandle>;
|
||||
/** Fires after an agent is removed, with its agent id. */
|
||||
readonly onDidDispose: Event<string>;
|
||||
create(opts: CreateAgentOptions): Promise<IScopeHandle>;
|
||||
createMain(): Promise<IScopeHandle>;
|
||||
create(opts: CreateAgentOptions): Promise<IAgentScopeHandle>;
|
||||
createMain(): Promise<IAgentScopeHandle>;
|
||||
/** Create a child agent that inherits the parent's profile and context history. */
|
||||
fork(parentAgentId: string): Promise<IScopeHandle>;
|
||||
getHandle(agentId: string): IScopeHandle | undefined;
|
||||
list(): readonly IScopeHandle[];
|
||||
fork(parentAgentId: string): Promise<IAgentScopeHandle>;
|
||||
getHandle(agentId: string): IAgentScopeHandle | undefined;
|
||||
list(): readonly IAgentScopeHandle[];
|
||||
remove(agentId: string): Promise<void>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { Emitter } from '#/_base/event';
|
|||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import {
|
||||
createScopedChildHandle,
|
||||
type IScopeHandle,
|
||||
type IAgentScopeHandle,
|
||||
LifecycleScope,
|
||||
registerScopedService,
|
||||
} from '#/_base/di/scope';
|
||||
|
|
@ -48,8 +48,8 @@ let nextAgentId = 0;
|
|||
|
||||
export class AgentLifecycleService extends Disposable implements IAgentLifecycleService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
private readonly handles = new Map<string, IScopeHandle>();
|
||||
private readonly onDidCreateEmitter = this._register(new Emitter<IScopeHandle>());
|
||||
private readonly handles = new Map<string, IAgentScopeHandle>();
|
||||
private readonly onDidCreateEmitter = this._register(new Emitter<IAgentScopeHandle>());
|
||||
private readonly onDidDisposeEmitter = this._register(new Emitter<string>());
|
||||
private mcpManager: McpConnectionManager | undefined;
|
||||
|
||||
|
|
@ -72,7 +72,7 @@ export class AgentLifecycleService extends Disposable implements IAgentLifecycle
|
|||
super();
|
||||
}
|
||||
|
||||
async create(opts: CreateAgentOptions): Promise<IScopeHandle> {
|
||||
async create(opts: CreateAgentOptions): Promise<IAgentScopeHandle> {
|
||||
const agentId = opts.agentId ?? `agent-${nextAgentId++}`;
|
||||
// Per-agent homedir → the wire-record persistence key (`hashKey(homedir)`).
|
||||
// Co-located under the session dir, mirroring v1's `<sessionDir>/agents/<id>`.
|
||||
|
|
@ -95,7 +95,7 @@ export class AgentLifecycleService extends Disposable implements IAgentLifecycle
|
|||
[IAgentExternalHooksService, new SyncDescriptor(AgentExternalHooksService, [{}], true)],
|
||||
],
|
||||
},
|
||||
);
|
||||
) as IAgentScopeHandle;
|
||||
this.handles.set(agentId, handle);
|
||||
// Record the agent in the session registry so a closed-session fork can
|
||||
// enumerate every agent and relocate its wire log.
|
||||
|
|
@ -114,7 +114,7 @@ export class AgentLifecycleService extends Disposable implements IAgentLifecycle
|
|||
return handle;
|
||||
}
|
||||
|
||||
async createMain(): Promise<IScopeHandle> {
|
||||
async createMain(): Promise<IAgentScopeHandle> {
|
||||
const handle = await this.create({ agentId: 'main' });
|
||||
// Force-instantiate the plugin session-start injector so it registers its
|
||||
// turn-cadence injection before the first turn. Main-agent only, matching
|
||||
|
|
@ -123,7 +123,7 @@ export class AgentLifecycleService extends Disposable implements IAgentLifecycle
|
|||
return handle;
|
||||
}
|
||||
|
||||
async fork(parentAgentId: string): Promise<IScopeHandle> {
|
||||
async fork(parentAgentId: string): Promise<IAgentScopeHandle> {
|
||||
const parent =
|
||||
this.handles.get(parentAgentId) ??
|
||||
(parentAgentId === 'main' ? await this.createMain() : undefined);
|
||||
|
|
@ -173,11 +173,11 @@ export class AgentLifecycleService extends Disposable implements IAgentLifecycle
|
|||
await manager.connectAll(servers);
|
||||
}
|
||||
|
||||
getHandle(agentId: string): IScopeHandle | undefined {
|
||||
getHandle(agentId: string): IAgentScopeHandle | undefined {
|
||||
return this.handles.get(agentId);
|
||||
}
|
||||
|
||||
list(): readonly IScopeHandle[] {
|
||||
list(): readonly IAgentScopeHandle[] {
|
||||
return [...this.handles.values()];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import type { ServiceIdentifier, ServicesAccessor } from '#/_base/di/instantiation';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { type IScopeHandle, LifecycleScope } from '#/_base/di/scope';
|
||||
import { type IAgentScopeHandle, type ISessionScopeHandle, LifecycleScope } from '#/_base/di/scope';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IAgentLifecycleService } from '#/session/agent-lifecycle/agentLifecycle';
|
||||
import type { ContextMessage } from '#/agent/contextMemory';
|
||||
|
|
@ -59,7 +59,7 @@ describe('RestGateway', () => {
|
|||
clear: () => {},
|
||||
};
|
||||
|
||||
const agentHandle: IScopeHandle = {
|
||||
const agentHandle: IAgentScopeHandle = {
|
||||
id: 'main',
|
||||
kind: LifecycleScope.Agent,
|
||||
accessor: makeAccessor([
|
||||
|
|
@ -79,7 +79,7 @@ describe('RestGateway', () => {
|
|||
list: () => [agentHandle],
|
||||
remove: () => Promise.resolve(),
|
||||
};
|
||||
const sessionHandle: IScopeHandle = {
|
||||
const sessionHandle: ISessionScopeHandle = {
|
||||
id: 's1',
|
||||
kind: LifecycleScope.Session,
|
||||
accessor: makeAccessor([[IAgentLifecycleService, agents]]),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import type { ServiceIdentifier, ServicesAccessor } from '#/_base/di/instantiation';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { type IScopeHandle, LifecycleScope } from '#/_base/di/scope';
|
||||
import { type IAgentScopeHandle, LifecycleScope } from '#/_base/di/scope';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IAgentLifecycleService } from '#/session/agent-lifecycle/agentLifecycle';
|
||||
import { ISessionInteractionService, type Interaction, type InteractionKind } from '#/session/interaction';
|
||||
|
|
@ -41,7 +41,7 @@ function makeAccessor(turn: IAgentTurnService): ServicesAccessor {
|
|||
};
|
||||
}
|
||||
|
||||
function handle(id: string, active: boolean): IScopeHandle {
|
||||
function handle(id: string, active: boolean): IAgentScopeHandle {
|
||||
const turn = makeTurnService(active);
|
||||
return {
|
||||
id,
|
||||
|
|
@ -51,7 +51,7 @@ function handle(id: string, active: boolean): IScopeHandle {
|
|||
};
|
||||
}
|
||||
|
||||
function lifecycle(handles: readonly IScopeHandle[]): IAgentLifecycleService {
|
||||
function lifecycle(handles: readonly IAgentScopeHandle[]): IAgentLifecycleService {
|
||||
return {
|
||||
_serviceBrand: undefined,
|
||||
onDidCreate: () => ({ dispose: () => {} }),
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|||
|
||||
import type { ServicesAccessor } from '#/_base/di/instantiation';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { type IScopeHandle, LifecycleScope } from '#/_base/di/scope';
|
||||
import { type IAgentScopeHandle, LifecycleScope } from '#/_base/di/scope';
|
||||
import { createServices, type TestInstantiationService } from '#/_base/di/test';
|
||||
import { IAgentLifecycleService } from '#/session/agent-lifecycle/agentLifecycle';
|
||||
import { IEventService } from '#/app/event';
|
||||
|
|
@ -11,7 +11,7 @@ import { SessionService } from '#/session/session/sessionService';
|
|||
import { ISessionContext } from '#/session/session-context';
|
||||
import { ISessionMetadata } from '#/session/session-metadata';
|
||||
|
||||
const handle: IScopeHandle = {
|
||||
const handle: IAgentScopeHandle = {
|
||||
id: 'main',
|
||||
kind: LifecycleScope.Agent,
|
||||
accessor: { get: () => ({}) } as unknown as ServicesAccessor,
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ import {
|
|||
ISessionLifecycleService,
|
||||
ISessionSkillCatalog,
|
||||
isKimiError,
|
||||
type IScopeHandle,
|
||||
type ISessionScopeHandle,
|
||||
type Scope,
|
||||
} from '@moonshot-ai/agent-core-v2';
|
||||
import {
|
||||
|
|
@ -98,7 +98,7 @@ const skillTailParamsSchema = z.object({
|
|||
});
|
||||
|
||||
type ResolvedSession =
|
||||
| { readonly handle: IScopeHandle }
|
||||
| { readonly handle: ISessionScopeHandle }
|
||||
| { readonly envelope: ReturnType<typeof errEnvelope> };
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -7,7 +7,11 @@
|
|||
* missing main agent is created instead of reported as `agent.not_found`.
|
||||
*/
|
||||
|
||||
import { IAgentLifecycleService, type IScopeHandle } from '@moonshot-ai/agent-core-v2';
|
||||
import {
|
||||
IAgentLifecycleService,
|
||||
type IAgentScopeHandle,
|
||||
type ISessionScopeHandle,
|
||||
} from '@moonshot-ai/agent-core-v2';
|
||||
|
||||
export const MAIN_AGENT_ID = 'main';
|
||||
|
||||
|
|
@ -15,7 +19,7 @@ export const MAIN_AGENT_ID = 'main';
|
|||
* Return the session's main agent, creating it on demand when it does not
|
||||
* exist yet.
|
||||
*/
|
||||
export async function ensureMainAgent(session: IScopeHandle): Promise<IScopeHandle> {
|
||||
export async function ensureMainAgent(session: ISessionScopeHandle): Promise<IAgentScopeHandle> {
|
||||
const agents = session.accessor.get(IAgentLifecycleService);
|
||||
const existing = agents.getHandle(MAIN_AGENT_ID);
|
||||
if (existing !== undefined) return existing;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue