agent-session_context 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +28 -0
- data/LICENSE.txt +21 -0
- data/README.md +268 -0
- data/exe/agent-session-context +6 -0
- data/lib/agent/session_context/builder.rb +146 -0
- data/lib/agent/session_context/cli/options.rb +213 -0
- data/lib/agent/session_context/cli.rb +229 -0
- data/lib/agent/session_context/config.rb +206 -0
- data/lib/agent/session_context/errors.rb +15 -0
- data/lib/agent/session_context/evidence_collector.rb +227 -0
- data/lib/agent/session_context/evidence_packet.rb +271 -0
- data/lib/agent/session_context/immutable_value.rb +71 -0
- data/lib/agent/session_context/injected_context.rb +92 -0
- data/lib/agent/session_context/injected_context_collector.rb +53 -0
- data/lib/agent/session_context/item.rb +54 -0
- data/lib/agent/session_context/prompt.rb +40 -0
- data/lib/agent/session_context/prompt_extractor.rb +31 -0
- data/lib/agent/session_context/renderers/human_display.rb +113 -0
- data/lib/agent/session_context/renderers/json.rb +13 -0
- data/lib/agent/session_context/renderers/json_lines.rb +13 -0
- data/lib/agent/session_context/renderers/markdown.rb +126 -0
- data/lib/agent/session_context/renderers/serializer.rb +124 -0
- data/lib/agent/session_context/renderers/text.rb +122 -0
- data/lib/agent/session_context/semantic_categories.rb +89 -0
- data/lib/agent/session_context/semantic_pipeline.rb +151 -0
- data/lib/agent/session_context/semantic_schema.rb +75 -0
- data/lib/agent/session_context/session_resolver.rb +147 -0
- data/lib/agent/session_context/snapshot.rb +128 -0
- data/lib/agent/session_context/source_ref.rb +46 -0
- data/lib/agent/session_context/subprocess_runner.rb +362 -0
- data/lib/agent/session_context/summarizers/claude.rb +126 -0
- data/lib/agent/session_context/summarizers/codex.rb +132 -0
- data/lib/agent/session_context/summarizers/command_execution_policy.rb +134 -0
- data/lib/agent/session_context/summarizers.rb +35 -0
- data/lib/agent/session_context/summary_parser.rb +219 -0
- data/lib/agent/session_context/transcript.rb +236 -0
- data/lib/agent/session_context/version.rb +7 -0
- data/lib/agent/session_context.rb +56 -0
- metadata +112 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Agent
|
|
4
|
+
module SessionContext
|
|
5
|
+
module Renderers
|
|
6
|
+
module HumanDisplay
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def text_inline(value)
|
|
10
|
+
sanitize_string(value, preserve_newlines: false)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def text_block(value)
|
|
14
|
+
sanitize_string(value, preserve_newlines: true).split("\n", -1).map { |line| "| #{line}" }.join("\n")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def markdown_text(value)
|
|
18
|
+
escape_markdown(text_inline(value))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def markdown_literal(value)
|
|
22
|
+
literal = text_inline(value)
|
|
23
|
+
return "<code></code>" if literal.empty?
|
|
24
|
+
return "<code>#{literal}</code>" if literal.match?(/\A +\z/)
|
|
25
|
+
|
|
26
|
+
fence = "`" * [longest_backtick_run(literal) + 1, 1].max
|
|
27
|
+
"#{fence}#{code_span_content(literal)}#{fence}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def markdown_block(value)
|
|
31
|
+
content = sanitize_string(value, preserve_newlines: true)
|
|
32
|
+
fence = "`" * [longest_backtick_run(content) + 1, 3].max
|
|
33
|
+
|
|
34
|
+
["#{fence}text", content, fence].join("\n")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def timestamp(value)
|
|
38
|
+
serialized = Serializer.serialize_timestamp(value)
|
|
39
|
+
return if serialized.nil?
|
|
40
|
+
|
|
41
|
+
serialized.to_s
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def refs(source_refs)
|
|
45
|
+
Array(source_refs).map { |ref| "#{ref.message_index}:#{ref.part_index}" }.join(", ")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def attributes(hash, inline_formatter:)
|
|
49
|
+
Serializer.normalized_hash_entries(hash).map do |key, value|
|
|
50
|
+
"#{inline_formatter.call(key)}=#{attribute_value(value, inline_formatter)}"
|
|
51
|
+
end.join(", ")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def attribute_value(value, inline_formatter)
|
|
55
|
+
return "{#{attributes(value, inline_formatter:)}}" if value.is_a?(Hash)
|
|
56
|
+
return value.map { |entry| attribute_value(entry, inline_formatter) }.join(",") if value.is_a?(Array)
|
|
57
|
+
|
|
58
|
+
inline_formatter.call(Serializer.serialize(value).to_s)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def sanitize_string(value, preserve_newlines:)
|
|
62
|
+
scrubbed = Serializer.scrub_string(value.to_s)
|
|
63
|
+
buffer = String.new(encoding: Encoding::UTF_8)
|
|
64
|
+
|
|
65
|
+
scrubbed.each_codepoint do |codepoint|
|
|
66
|
+
if preserve_newlines && codepoint == 0x0A
|
|
67
|
+
buffer << "\n"
|
|
68
|
+
next
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
visible = visible_control_escape(codepoint)
|
|
72
|
+
buffer << (visible || codepoint)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
buffer
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def escape_markdown(value)
|
|
79
|
+
escaped = value.gsub("&", "&").gsub("<", "<").gsub(">", ">")
|
|
80
|
+
escaped = escaped.gsub(/([\\`*\[\]#])/, "\\\\\\1")
|
|
81
|
+
escaped = escaped.gsub(/(^|[^[:alnum:]])_([^_]+)_([^[:alnum:]]|$)/, '\1\\_\2\\_\3')
|
|
82
|
+
escaped.gsub("-", "\\-")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def code_span_content(literal)
|
|
86
|
+
return " #{literal} " if literal.start_with?("`") || literal.end_with?("`")
|
|
87
|
+
return " #{literal} " if literal.start_with?(" ") && literal.end_with?(" ")
|
|
88
|
+
|
|
89
|
+
literal
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def longest_backtick_run(value)
|
|
93
|
+
value.scan(/`+/).map(&:length).max || 0
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def visible_control_escape(codepoint)
|
|
97
|
+
case codepoint
|
|
98
|
+
when 0x09
|
|
99
|
+
"\\t"
|
|
100
|
+
when 0x0A
|
|
101
|
+
"\\n"
|
|
102
|
+
when 0x0D
|
|
103
|
+
"\\r"
|
|
104
|
+
when 0x1B
|
|
105
|
+
"\\e"
|
|
106
|
+
when 0x00..0x08, 0x0B..0x0C, 0x0E..0x1A, 0x1C..0x1F, 0x7F, 0x80..0x9F
|
|
107
|
+
format("\\u%04X", codepoint)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Agent
|
|
4
|
+
module SessionContext
|
|
5
|
+
module Renderers
|
|
6
|
+
class Markdown
|
|
7
|
+
SECTION_ORDER = [
|
|
8
|
+
["Goal", :goals],
|
|
9
|
+
["Files", :files],
|
|
10
|
+
["Documents", :documents],
|
|
11
|
+
["Tool activity", :tool_activity],
|
|
12
|
+
["Decisions", :decisions],
|
|
13
|
+
["Terminology", :terms],
|
|
14
|
+
["Constraints", :constraints],
|
|
15
|
+
["Open questions", :open_questions],
|
|
16
|
+
["Next actions", :next_actions],
|
|
17
|
+
["Warnings", :warnings]
|
|
18
|
+
].freeze
|
|
19
|
+
private_constant :SECTION_ORDER
|
|
20
|
+
|
|
21
|
+
def call(value)
|
|
22
|
+
if value.is_a?(Snapshot)
|
|
23
|
+
render_snapshot(value)
|
|
24
|
+
else
|
|
25
|
+
render_prompts(Array(value))
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def render_snapshot(snapshot)
|
|
32
|
+
sections = [render_session(snapshot)]
|
|
33
|
+
sections << render_snapshot_prompts(snapshot.prompts) if snapshot.prompts.any?
|
|
34
|
+
sections << render_injected_context(snapshot.injected_context) if snapshot.injected_context.any?
|
|
35
|
+
|
|
36
|
+
SECTION_ORDER.each do |title, field|
|
|
37
|
+
section = render_section(title, snapshot.public_send(field))
|
|
38
|
+
sections << section if section
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
sections.join("\n\n")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def render_prompts(prompts, heading_level: 2)
|
|
45
|
+
heading = "#" * heading_level
|
|
46
|
+
prompts.map do |prompt|
|
|
47
|
+
lines = ["#{heading} Prompt #{prompt.index}"]
|
|
48
|
+
prompt_at = HumanDisplay.timestamp(prompt.at)
|
|
49
|
+
lines << "- At: #{HumanDisplay.markdown_literal(prompt_at)}" if prompt_at
|
|
50
|
+
lines << "- Refs: #{HumanDisplay.markdown_literal(HumanDisplay.refs(prompt.source_refs))}"
|
|
51
|
+
lines << ""
|
|
52
|
+
lines << HumanDisplay.markdown_block(prompt.text)
|
|
53
|
+
lines.join("\n")
|
|
54
|
+
end.join("\n\n")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def render_snapshot_prompts(prompts)
|
|
58
|
+
["## User prompts", render_prompts(prompts, heading_level: 3)].join("\n\n")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def render_injected_context(contexts)
|
|
62
|
+
entries = contexts.map do |context|
|
|
63
|
+
lines = [
|
|
64
|
+
"### Injected #{HumanDisplay.markdown_literal(context.kind)}",
|
|
65
|
+
"- Bytes: #{HumanDisplay.markdown_literal(context.bytes)}",
|
|
66
|
+
"- Occurrences: #{HumanDisplay.markdown_literal(context.occurrences)}",
|
|
67
|
+
"- Refs: #{HumanDisplay.markdown_literal(HumanDisplay.refs(context.source_refs))}"
|
|
68
|
+
]
|
|
69
|
+
if context.text
|
|
70
|
+
lines << ""
|
|
71
|
+
lines << HumanDisplay.markdown_block(context.text)
|
|
72
|
+
end
|
|
73
|
+
lines.join("\n")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
["## Injected context", entries.join("\n\n")].join("\n\n")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def render_session(snapshot)
|
|
80
|
+
lines = [
|
|
81
|
+
"## Session",
|
|
82
|
+
"- UID: #{HumanDisplay.markdown_literal(snapshot.session_uid)}",
|
|
83
|
+
"- Agent: #{HumanDisplay.markdown_literal(snapshot.agent)}"
|
|
84
|
+
]
|
|
85
|
+
lines << "- Project path: #{HumanDisplay.markdown_literal(snapshot.project_path)}" if snapshot.project_path
|
|
86
|
+
captured_at = HumanDisplay.timestamp(snapshot.captured_at)
|
|
87
|
+
lines << "- Captured at: #{HumanDisplay.markdown_literal(captured_at)}" if captured_at
|
|
88
|
+
lines << "- Message count: #{HumanDisplay.markdown_literal(snapshot.message_count)}"
|
|
89
|
+
if snapshot.summary_metadata.any?
|
|
90
|
+
lines << "- Summary metadata: #{HumanDisplay.markdown_literal(attributes(snapshot.summary_metadata))}"
|
|
91
|
+
end
|
|
92
|
+
lines.join("\n")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def render_section(title, items)
|
|
96
|
+
return if items.empty?
|
|
97
|
+
|
|
98
|
+
if title == "Warnings"
|
|
99
|
+
return (["## #{title}"] + items.map { |warning| "- #{HumanDisplay.markdown_text(warning)}" }).join("\n")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
(["## #{title}"] + items.map { |item| "- #{item_line(item)}" }).join("\n")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def item_line(item)
|
|
106
|
+
fragments = [base_item_text(item)]
|
|
107
|
+
attrs = attributes(item.attributes)
|
|
108
|
+
fragments << "(#{HumanDisplay.markdown_literal(attrs)})" unless attrs.empty?
|
|
109
|
+
fragments << HumanDisplay.markdown_literal("[#{item.evidence}]")
|
|
110
|
+
fragments << "refs #{HumanDisplay.markdown_literal(HumanDisplay.refs(item.source_refs))}"
|
|
111
|
+
fragments.join(" ")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def base_item_text(item)
|
|
115
|
+
return "#{HumanDisplay.markdown_text(item.label)}: #{HumanDisplay.markdown_text(item.detail)}" if item.detail
|
|
116
|
+
|
|
117
|
+
HumanDisplay.markdown_text(item.label)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def attributes(hash)
|
|
121
|
+
HumanDisplay.attributes(hash, inline_formatter: HumanDisplay.method(:text_inline))
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Agent
|
|
4
|
+
module SessionContext
|
|
5
|
+
module Renderers
|
|
6
|
+
module Serializer
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def serialize(value)
|
|
10
|
+
case value
|
|
11
|
+
when nil, true, false, Integer
|
|
12
|
+
value
|
|
13
|
+
when Float
|
|
14
|
+
serialize_float(value)
|
|
15
|
+
when String
|
|
16
|
+
scrub_string(value)
|
|
17
|
+
when Symbol
|
|
18
|
+
scrub_string(value.to_s)
|
|
19
|
+
when Time
|
|
20
|
+
value.iso8601
|
|
21
|
+
when Array
|
|
22
|
+
value.map { |entry| serialize(entry) }
|
|
23
|
+
when Hash
|
|
24
|
+
serialize_hash(value)
|
|
25
|
+
else
|
|
26
|
+
return serialize_data(value) if data_object?(value)
|
|
27
|
+
|
|
28
|
+
scrub_string(value.to_s)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def scrub_string(value)
|
|
33
|
+
value.encode("UTF-8", invalid: :replace, undef: :replace)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def data_object?(value)
|
|
37
|
+
defined?(Data) && value.is_a?(Data)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def serialize_data(value)
|
|
41
|
+
value.members.to_h do |member|
|
|
42
|
+
[member.to_s, serialize_member(member, value.public_send(member))]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def serialize_hash(value)
|
|
47
|
+
normalized_hash_entries(value).each_with_object({}) do |(key, nested_value), serialized|
|
|
48
|
+
serialized[key] = serialize(nested_value)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def normalized_hash_entries(value)
|
|
53
|
+
entries = value.each_with_object([]) do |(key, nested_value), collected|
|
|
54
|
+
collected << [normalized_key(key), nested_value]
|
|
55
|
+
end
|
|
56
|
+
detect_duplicate_keys!(entries)
|
|
57
|
+
entries.sort_by(&:first)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def detect_duplicate_keys!(entries)
|
|
61
|
+
entries.group_by(&:first).sort_by(&:first).each do |key, grouped_entries|
|
|
62
|
+
next unless grouped_entries.length > 1
|
|
63
|
+
|
|
64
|
+
raise ArgumentError, "duplicate serialized key: #{safe_dump(key)}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def serialize_member(member, value)
|
|
69
|
+
return serialize_timestamp(value) if %i[at captured_at].include?(member.to_sym)
|
|
70
|
+
|
|
71
|
+
serialize(value)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def serialize_timestamp(value)
|
|
75
|
+
case value
|
|
76
|
+
when nil
|
|
77
|
+
nil
|
|
78
|
+
when Time
|
|
79
|
+
value.iso8601
|
|
80
|
+
when String
|
|
81
|
+
scrub_string(value)
|
|
82
|
+
else
|
|
83
|
+
raise ArgumentError, "unsupported timestamp value: #{unsupported_value_label(value)}"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def serialize_float(value)
|
|
88
|
+
raise ArgumentError, "unsupported numeric value: #{unsupported_value_label(value)}" unless value.finite?
|
|
89
|
+
|
|
90
|
+
value
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def normalized_key(key)
|
|
94
|
+
scrub_string(key.to_s)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def safe_dump(value)
|
|
98
|
+
case value
|
|
99
|
+
when String
|
|
100
|
+
scrub_string(value).dump
|
|
101
|
+
when Symbol
|
|
102
|
+
scrub_string(value.to_s).dump
|
|
103
|
+
when Float
|
|
104
|
+
return "NaN" if value.nan?
|
|
105
|
+
return "Infinity" if value.infinite? == 1
|
|
106
|
+
return "-Infinity" if value.infinite? == -1
|
|
107
|
+
|
|
108
|
+
value.to_s
|
|
109
|
+
else
|
|
110
|
+
scrub_string(value.inspect)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def unsupported_value_label(value)
|
|
115
|
+
klass = value.class
|
|
116
|
+
name = klass.name
|
|
117
|
+
return "(anonymous class)" if name.nil? || name.empty?
|
|
118
|
+
|
|
119
|
+
scrub_string(name)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Agent
|
|
4
|
+
module SessionContext
|
|
5
|
+
module Renderers
|
|
6
|
+
class Text
|
|
7
|
+
SECTION_ORDER = [
|
|
8
|
+
["Goal", :goals],
|
|
9
|
+
["Files", :files],
|
|
10
|
+
["Documents", :documents],
|
|
11
|
+
["Tool activity", :tool_activity],
|
|
12
|
+
["Decisions", :decisions],
|
|
13
|
+
["Terminology", :terms],
|
|
14
|
+
["Constraints", :constraints],
|
|
15
|
+
["Open questions", :open_questions],
|
|
16
|
+
["Next actions", :next_actions],
|
|
17
|
+
["Warnings", :warnings]
|
|
18
|
+
].freeze
|
|
19
|
+
private_constant :SECTION_ORDER
|
|
20
|
+
|
|
21
|
+
def call(value)
|
|
22
|
+
if value.is_a?(Snapshot)
|
|
23
|
+
render_snapshot(value)
|
|
24
|
+
else
|
|
25
|
+
render_prompts(Array(value))
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def render_snapshot(snapshot)
|
|
32
|
+
sections = [render_session(snapshot)]
|
|
33
|
+
sections << render_snapshot_prompts(snapshot.prompts) if snapshot.prompts.any?
|
|
34
|
+
sections << render_injected_context(snapshot.injected_context) if snapshot.injected_context.any?
|
|
35
|
+
|
|
36
|
+
SECTION_ORDER.each do |title, field|
|
|
37
|
+
section = render_section(title, snapshot.public_send(field))
|
|
38
|
+
sections << section if section
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
sections.join("\n\n")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def render_snapshot_prompts(prompts)
|
|
45
|
+
["User prompts", render_prompts(prompts)].join("\n\n")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def render_injected_context(contexts)
|
|
49
|
+
entries = contexts.map do |context|
|
|
50
|
+
lines = [
|
|
51
|
+
"Injected #{HumanDisplay.text_inline(context.kind)}",
|
|
52
|
+
"- Bytes: #{context.bytes}",
|
|
53
|
+
"- Occurrences: #{context.occurrences}",
|
|
54
|
+
"- Refs: #{HumanDisplay.refs(context.source_refs)}"
|
|
55
|
+
]
|
|
56
|
+
if context.text
|
|
57
|
+
lines << "- Text:"
|
|
58
|
+
lines << HumanDisplay.text_block(context.text)
|
|
59
|
+
end
|
|
60
|
+
lines.join("\n")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
["Injected context", entries.join("\n\n")].join("\n\n")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def render_prompts(prompts)
|
|
67
|
+
prompts.map do |prompt|
|
|
68
|
+
lines = ["Prompt #{prompt.index}"]
|
|
69
|
+
prompt_at = HumanDisplay.timestamp(prompt.at)
|
|
70
|
+
lines << "- At: #{HumanDisplay.text_inline(prompt_at)}" if prompt_at
|
|
71
|
+
lines << "- Refs: #{HumanDisplay.refs(prompt.source_refs)}"
|
|
72
|
+
lines << HumanDisplay.text_block(prompt.text)
|
|
73
|
+
lines.join("\n")
|
|
74
|
+
end.join("\n\n")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def render_session(snapshot)
|
|
78
|
+
lines = [
|
|
79
|
+
"Session",
|
|
80
|
+
"- UID: #{HumanDisplay.text_inline(snapshot.session_uid)}",
|
|
81
|
+
"- Agent: #{HumanDisplay.text_inline(snapshot.agent)}"
|
|
82
|
+
]
|
|
83
|
+
lines << "- Project path: #{HumanDisplay.text_inline(snapshot.project_path)}" if snapshot.project_path
|
|
84
|
+
captured_at = HumanDisplay.timestamp(snapshot.captured_at)
|
|
85
|
+
lines << "- Captured at: #{HumanDisplay.text_inline(captured_at)}" if captured_at
|
|
86
|
+
lines << "- Message count: #{snapshot.message_count}"
|
|
87
|
+
lines << "- Summary metadata: #{attributes(snapshot.summary_metadata)}" if snapshot.summary_metadata.any?
|
|
88
|
+
lines.join("\n")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def render_section(title, items)
|
|
92
|
+
return if items.empty?
|
|
93
|
+
|
|
94
|
+
if title == "Warnings"
|
|
95
|
+
return ([title] + items.map { |warning| "- #{HumanDisplay.text_inline(warning)}" }).join("\n")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
([title] + items.map { |item| "- #{item_line(item)}" }).join("\n")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def item_line(item)
|
|
102
|
+
fragments = [base_item_text(item)]
|
|
103
|
+
attrs = attributes(item.attributes)
|
|
104
|
+
fragments << "(#{attrs})" unless attrs.empty?
|
|
105
|
+
fragments << "[#{item.evidence}]"
|
|
106
|
+
fragments << "refs #{HumanDisplay.refs(item.source_refs)}"
|
|
107
|
+
fragments.join(" ")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def base_item_text(item)
|
|
111
|
+
return "#{HumanDisplay.text_inline(item.label)}: #{HumanDisplay.text_inline(item.detail)}" if item.detail
|
|
112
|
+
|
|
113
|
+
HumanDisplay.text_inline(item.label)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def attributes(hash)
|
|
117
|
+
HumanDisplay.attributes(hash, inline_formatter: HumanDisplay.method(:text_inline))
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Agent
|
|
4
|
+
module SessionContext
|
|
5
|
+
class SemanticCategories
|
|
6
|
+
Category = Data.define(
|
|
7
|
+
:external_key,
|
|
8
|
+
:internal_kind,
|
|
9
|
+
:snapshot_field,
|
|
10
|
+
:prompt_description,
|
|
11
|
+
:item_shape
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
ALL = [
|
|
15
|
+
Category.new(
|
|
16
|
+
external_key: "goals",
|
|
17
|
+
internal_kind: :goal,
|
|
18
|
+
snapshot_field: :goals,
|
|
19
|
+
prompt_description: "stated goals or desired outcomes from the conversation",
|
|
20
|
+
item_shape: :text
|
|
21
|
+
),
|
|
22
|
+
Category.new(
|
|
23
|
+
external_key: "decisions",
|
|
24
|
+
internal_kind: :decision,
|
|
25
|
+
snapshot_field: :decisions,
|
|
26
|
+
prompt_description: "decisions the participants have already made",
|
|
27
|
+
item_shape: :text
|
|
28
|
+
),
|
|
29
|
+
Category.new(
|
|
30
|
+
external_key: "terms",
|
|
31
|
+
internal_kind: :term,
|
|
32
|
+
snapshot_field: :terms,
|
|
33
|
+
prompt_description: "project-specific terms with their definitions",
|
|
34
|
+
item_shape: :term
|
|
35
|
+
),
|
|
36
|
+
Category.new(
|
|
37
|
+
external_key: "constraints",
|
|
38
|
+
internal_kind: :constraint,
|
|
39
|
+
snapshot_field: :constraints,
|
|
40
|
+
prompt_description: "limits, requirements, or non-negotiables",
|
|
41
|
+
item_shape: :text
|
|
42
|
+
),
|
|
43
|
+
Category.new(
|
|
44
|
+
external_key: "open_questions",
|
|
45
|
+
internal_kind: :open_question,
|
|
46
|
+
snapshot_field: :open_questions,
|
|
47
|
+
prompt_description: "questions that are still unresolved",
|
|
48
|
+
item_shape: :text
|
|
49
|
+
),
|
|
50
|
+
Category.new(
|
|
51
|
+
external_key: "next_actions",
|
|
52
|
+
internal_kind: :next_action,
|
|
53
|
+
snapshot_field: :next_actions,
|
|
54
|
+
prompt_description: "concrete follow-up actions that someone should take",
|
|
55
|
+
item_shape: :text
|
|
56
|
+
)
|
|
57
|
+
].freeze
|
|
58
|
+
EXTERNAL_KEYS = ALL.map(&:external_key).freeze
|
|
59
|
+
SNAPSHOT_FIELDS = ALL.map(&:snapshot_field).uniq.freeze
|
|
60
|
+
EXTERNAL_INDEX = ALL.to_h { |category| [category.external_key, category] }.freeze
|
|
61
|
+
INTERNAL_INDEX = ALL.to_h { |category| [category.internal_kind, category] }.freeze
|
|
62
|
+
|
|
63
|
+
private_constant :EXTERNAL_KEYS, :SNAPSHOT_FIELDS, :EXTERNAL_INDEX, :INTERNAL_INDEX
|
|
64
|
+
|
|
65
|
+
class << self
|
|
66
|
+
def all
|
|
67
|
+
ALL
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def external_keys
|
|
71
|
+
EXTERNAL_KEYS
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def snapshot_fields
|
|
75
|
+
SNAPSHOT_FIELDS
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def lookup(identifier)
|
|
79
|
+
case identifier
|
|
80
|
+
when String
|
|
81
|
+
EXTERNAL_INDEX[identifier]
|
|
82
|
+
when Symbol
|
|
83
|
+
INTERNAL_INDEX[identifier]
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|